Autenticación mutua de cliente y servidor con certificados

Escrito por el , actualizado el .
blog-stack java planeta-codigo planeta-linux programacion seguridad
Comentarios

OpenSSL

Los certificados no solo sirven para autenticar a un servidor o acceder solo a aquellos en los que confiamos. El servidor también puede autenticar a los clientes mediante un certificado como alternativa a usar un usuario y contraseña ya sea una autenticación BASIC o un formulario personalizado. Al igual que en el cliente usa el certificado de la autoridad de certificación en la que confía para validar el que presenta el servidor, el servidor puede requerir que el cliente también proporcione un certificado que el servidor valida según las autoridades de certificación en las que confía, en ambos casos el servidor o cliente usan su clave privada para iniciar la conexión segura con el handsake del protocolo TLS.

Para el ejemplo usaré un servidor web nginx ejecutado como un contenedor de Docker configurado de tal manera que requiere autenticación para el cliente con certificados.

Inicialmente deberemos generar tres parejas de claves privadas y públicas, una para nuestra propia autoridad de certificación, una clave para el servidor y otra para el cliente. Al mismo tiempo generaré otras tres parejas de claves privadas y públicas para comprobar que cuando se proporciona un certificado incorrecto la autenticación falla.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ openssl genrsa -out ca.key 8192
Generating RSA private key, 8192 bit long modulus
............................................................................................................................................++
.........................................................................................................................................................................................................................................................................................................................................++
e is 65537 (0x010001)

$ openssl rsa -in ca.key -pubout > ca.pub
$ openssl genrsa -out server.key 8192
$ openssl rsa -in server.key -pubout > server.pub
$ openssl genrsa -out client.key 8192
$ openssl rsa -in ca.key -pubout > client.pub
1
2
3
4
5
6
7
-----BEGIN CERTIFICATE-----
MIIJeTCCBWGgAwIBAgIJAMmS/dYrpwDnMA0GCSqGSIb3DQEBBQUAMDExCzAJBgNV
BAYTAkVTMQ4wDAYDVQQIEwVTcGFpbjESMBAGA1UEChMJbG9jYWxob3N0MB4XDTE3
MDQwNzA2NTcxNVoXDTIyMDQwNjA2NTcxNVowMTELMAkGA1UEBhMCRVMxDjAMBgNV
BAgTBVNwYWluMRIwEAYDVQQKEwlsb2NhbGhvc3QwggQiMA0GCSqGSIb3DQEBAQUA
...
-----END CERTIFICATE-----
1
2
3
4
5
6
7
-----BEGIN RSA PRIVATE KEY-----
MIISJwIBAAKCBAEAyq7VfFt8LapGTtrN4zPAp5KdiHc3raAhs7MSGmrmtqYszheS
AGok/xx9RlUrLSgzjhQ22s28OgfKnqKOK1bzcjTj5Uwjc5Tr7RY724924amECHXc
ldJGc3c/BpdbyYboxsTau8BbAk45c61QKeoTGtQ+K4a2/X0oArroTtHOlRFFUB9t
yKSD20Vj80Ks4op/Q7ucEcZ8mr9zAXzhfokK72PLRGWmmvd1NBoHUzbrkNH9A8he
...
-----END RSA PRIVATE KEY-----
1
2
3
4
5
6
7
-----BEGIN PUBLIC KEY-----
MIIEIjANBgkqhkiG9w0BAQEFAAOCBA8AMIIECgKCBAEAyq7VfFt8LapGTtrN4zPA
p5KdiHc3raAhs7MSGmrmtqYszheSAGok/xx9RlUrLSgzjhQ22s28OgfKnqKOK1bz
cjTj5Uwjc5Tr7RY724924amECHXcldJGc3c/BpdbyYboxsTau8BbAk45c61QKeoT
GtQ+K4a2/X0oArroTtHOlRFFUB9tyKSD20Vj80Ks4op/Q7ucEcZ8mr9zAXzhfokK
...
-----END PUBLIC KEY-----
1
2
3
4
5
6
$ openssl genrsa -out ca-unknown.key 8192
$ openssl rsa -in ca-unknown.key -pubout > ca-unknown.pub
$ openssl genrsa -out server-unknown.key 8192
$ openssl rsa -in server-unknown.key -pubout > server-unknown.pub
$ openssl genrsa -out client-unknown.key 8192
$ openssl rsa -in ca-unknown.key -pubout > client-unknown.pub

El siguiente paso es generar los certificados y firmar con la clave y certificado de la autoridad de certificado los certificados del servidor y cliente. Como paso previo a que la autoridad de certificación emita los certificados del servidor y cliente hay que generar una petición de firma de certificado, los archivos .csr.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ openssl req -new -x509 -days 1825 -key ca.key -out ca.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:ES
State or Province Name (full name) [Some-State]:Spain
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Blog Bitix Certiticate Authority
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

$ openssl req -new -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:ES
State or Province Name (full name) [Some-State]:Spain
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Blog Bitix
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

$ openssl req -new -key client.key -out client.csr

$ openssl x509 -req -days 1825 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
Signature ok
subject=C = ES, ST = Spain, O = Blog Bitix, CN = localhost
Getting CA Private Key
$ openssl x509 -req -days 1825 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out client.crt
1
2
3
4
5
$ openssl req -new -x509 -days 1825 -key ca-unknown.key -out ca-unknown.crt
$ openssl req -new -key server-unknown.key -out server-unknown.csr
$ openssl req -new -key client-unknown.key -out client-unknown.csr
$ openssl x509 -req -days 1825 -in server-unknown.csr -CA ca-unknown.crt -CAkey ca-unknown.key -set_serial 01 -out server-unknown.crt
$ openssl x509 -req -days 1825 -in client-unknown.csr -CA ca-unknown.crt -CAkey ca-unknown.key -set_serial 02 -out client-unknown.crt

Con la misma herramienta de OpenSSL es posible comprobar si un certificado es válido para una autoridad de certificación en la que se confía, para ello se usa el certificado raiz de la autoridad.

1
2
3
4
$ openssl verify -CAfile ca.crt server.crt
server.crt: OK
$ openssl verify -CAfile ca.crt client.crt
client.crt: OK
1
2
3
4
5
6
7
8
$ openssl verify -CAfile ca.crt server-unknown.crt
C = ES, ST = Spain, O = Unknown, CN = localhost
error 20 at 0 depth lookup: unable to get local issuer certificate
error server-unknown.crt: verification failed
$ openssl verify -CAfile ca.crt client-unknown.crt
C = ES, ST = Spain, O = Unknown Client
error 20 at 0 depth lookup: unable to get local issuer certificate
error client-unknown.crt: verification failed

Para hacer que el servidor nginx requiera autenticación mediante certificados para el cliente hay que añadir un poco de configuración mediante las directivas ssl donde se indica el certificado del servidor, la clave privada del servidor, el certificado de la autoridad de certificación contra la que se validarán los certificados de los clientes y finalmente la directiva que establece que se ha de verificar a los clientes mediante certificados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
http {
    ...
    server {
        listen       443;
        server_name  localhost;

        ssl                     on;
        ssl_certificate         /etc/nginx/server.crt;
        ssl_certificate_key     /etc/nginx/server.key;
        ssl_client_certificate  /etc/nginx/ca.crt;
        ssl_verify_client       on;
        ssl_verify_depth 5;

        ...
    }
}

Con el siguiente archivo descriptor de Docker Compose y comando se inicia el servidor web nginx.

1
$ docker-compose up
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
nginx:
  image: nginx:alpine
  volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf:ro
    - ./server.key:/etc/nginx/server.key:ro
    - ./server.crt:/etc/nginx/server.crt:ro
    - ./ca.crt:/etc/nginx/ca.crt:ro
  ports:
    - "80:80"
    - "443:443"

Iniciado el servidor web ya se pueden realizar peticiones y el servidor y el cliente se autenticarán mutuamente. El servidor devolverá el código HTML de la página de bienvenida por defecto con las cabeceras del protocolo HTTP después de realizar el handsake donde se valida el certificado del servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
$ curl -v --cacert ca.crt --cert client.crt --key client.key "https://localhost/"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: ca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.0 (IN), TLS handshake, Certificate (11):
* TLSv1.0 (IN), TLS handshake, Server key exchange (12):
* TLSv1.0 (IN), TLS handshake, Request CERT (13):
* TLSv1.0 (IN), TLS handshake, Server finished (14):
* TLSv1.0 (OUT), TLS handshake, Certificate (11):
* TLSv1.0 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.0 (OUT), TLS handshake, CERT verify (15):
* TLSv1.0 (OUT), TLS change cipher, Client hello (1):
* TLSv1.0 (OUT), TLS handshake, Finished (20):
* TLSv1.0 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.0 / ECDHE-RSA-AES256-SHA
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=ES; ST=Spain; O=Blog Bitix; CN=localhost
*  start date: Jun 16 22:16:18 2017 GMT
*  expire date: Jun 15 22:16:18 2022 GMT
*  common name: localhost (matched)
*  issuer: C=ES; ST=Spain; O=Blog Bitix Certiticate Authority
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.13.0
< Date: Fri, 16 Jun 2017 22:30:06 GMT
< Content-Type: text/html
< Content-Length: 612
< Last-Modified: Tue, 25 Apr 2017 17:23:03 GMT
< Connection: keep-alive
< ETag: "58ff85f7-264"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host localhost left intact

Si se intenta realizar una petición sin certificado de cliente o con un certificado de cliente en el que no confié el servidor (que no esté firmado por la autoridad de certificación en la que confía) se devolverá un código de estado 400 que indica que la petición se ha rechazado. También el cliente advertirá si la autoridad de certificación en la que confía no valida el certificado del servidor con un error 400 y título 400 The SSL certificate error.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
$ curl -v --cacert ca.crt "https://localhost/"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: ca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.0 (IN), TLS handshake, Certificate (11):
* TLSv1.0 (IN), TLS handshake, Server key exchange (12):
* TLSv1.0 (IN), TLS handshake, Request CERT (13):
* TLSv1.0 (IN), TLS handshake, Server finished (14):
* TLSv1.0 (OUT), TLS handshake, Certificate (11):
* TLSv1.0 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.0 (OUT), TLS change cipher, Client hello (1):
* TLSv1.0 (OUT), TLS handshake, Finished (20):
* TLSv1.0 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.0 / ECDHE-RSA-AES256-SHA
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=ES; ST=Spain; O=Blog Bitix; CN=localhost
*  start date: Jun 16 22:16:18 2017 GMT
*  expire date: Jun 15 22:16:18 2022 GMT
*  common name: localhost (matched)
*  issuer: C=ES; ST=Spain; O=Blog Bitix Certiticate Authority
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx/1.13.0
< Date: Fri, 16 Jun 2017 22:30:49 GMT
< Content-Type: text/html
< Content-Length: 253
< Connection: close
<
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.13.0</center>
</body>
</html>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, Client hello (1):

$ curl -v --cacert ca.crt --cert client-unknown.crt --key client-unknown.key "https://localhost/"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: ca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.0 (IN), TLS handshake, Certificate (11):
* TLSv1.0 (IN), TLS handshake, Server key exchange (12):
* TLSv1.0 (IN), TLS handshake, Request CERT (13):
* TLSv1.0 (IN), TLS handshake, Server finished (14):
* TLSv1.0 (OUT), TLS handshake, Certificate (11):
* TLSv1.0 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.0 (OUT), TLS handshake, CERT verify (15):
* TLSv1.0 (OUT), TLS change cipher, Client hello (1):
* TLSv1.0 (OUT), TLS handshake, Finished (20):
* TLSv1.0 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.0 / ECDHE-RSA-AES256-SHA
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=ES; ST=Spain; O=Blog Bitix; CN=localhost
*  start date: Jun 16 22:16:18 2017 GMT
*  expire date: Jun 15 22:16:18 2022 GMT
*  common name: localhost (matched)
*  issuer: C=ES; ST=Spain; O=Blog Bitix Certiticate Authority
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx/1.13.0
< Date: Fri, 16 Jun 2017 22:32:55 GMT
< Content-Type: text/html
< Content-Length: 231
< Connection: close
<
<html>
<head><title>400 The SSL certificate error</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The SSL certificate error</center>
<hr><center>nginx/1.13.0</center>
</body>
</html>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, Client hello (1):

$ curl -v --cacert ca-unknown.crt --cert client.crt --key client.key "https://localhost/"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: ca-unknown.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.0 (IN), TLS handshake, Certificate (11):
* TLSv1.0 (OUT), TLS alert, Server hello (2):
* SSL certificate problem: unable to get local issuer certificate
* stopped the pause stream!
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.
HTTPS-proxy has similar options --proxy-cacert and --proxy-insecure.

El siguiente script escrito en lenguaje Groovy muestra como desde un programa para la plataforma Java se realiza autenticación mutua y que error da cuando alguno de los certificados es inválido ya sea el del cliente o el del servidor. Generando previamente los keystores de la autoridad de certificado y del cliente introduciendo como clave en el ejemplo password cuando se solicita.

1
2
3
4
5
6
7
$ keytool -importcert -keystore ca.jks -trustcacerts -alias ca -file ca.crt
$ openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt
$ keytool -importkeystore -destkeystore client.jks -srckeystore client.p12 -srcstoretype pkcs12

$ keytool -importcert -keystore ca-unknown.jks -trustcacerts -alias ca -file ca-unknown.crt
$ openssl pkcs12 -export -out client-unknown.p12 -inkey client-unknown.key -in client-unknown.crt
$ keytool -importkeystore -destkeystore client-unknown.jks -srckeystore client-unknown.p12 -srcstoretype pkcs12
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Grab(group='javax.activation', module='activation', version='1.1.1') 
@Grab(group='javax', module='javaee-api', version='7.0')
@Grab(group='org.glassfish.jersey.core', module='jersey-client', version='2.25.1')

import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.ws.rs.client.Client
import javax.ws.rs.client.ClientBuilder
import javax.ws.rs.client.Entity
import javax.ws.rs.core.Response
import java.security.KeyStore

import org.glassfish.jersey.SslConfigurator

class Main {

    void get() {
        Response response = buildSslClient()
                .target("https://localhost").path("/")
                .request()
                .header("Accept", "text/html")
                .get()

        println(response.getStatus())
        println(response.readEntity(String.class))
    }

    private static Client buildSslClient() {
        return ClientBuilder.newBuilder().sslContext(buildSslContext()).build()
    }

    private static SSLContext buildSslContext() {
        return SslConfigurator.newInstance()
                .trustStoreFile("ca.jks")
                //.trustStoreFile("ca-unknown.jks")
                .trustStorePassword("password")
                .keyStoreFile("client.jks")
                //.keyStoreFile("client-unknown.jks")
                .keyPassword("password")
                .createSSLContext();
    }
}

new Main().get()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ groovy MutualCertAuth.groovy
200
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

En caso de que al usar un keystore con un certificado de una autoridad que no valida el certificado del servidor se producirán un error, también cuando el certificado del cliente no sea válido para el servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ groovy MutualCertAuth.groovy # with code change .trustStoreFile("ca-unknown.jks")
Caught: javax.ws.rs.ProcessingException: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
javax.ws.rs.ProcessingException: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:287)
    at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:252)
    at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:684)
    at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:681)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:444)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:681)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:411)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:311)
    at Main.get(MutualCertAuth.groovy:19)
    at Main$get.call(Unknown Source)
    at MutualCertAuth.run(MutualCertAuth.groovy:43)
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at org.glassfish.jersey.client.internal.HttpUrlConnector._apply(HttpUrlConnector.java:399)
    at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:285)
    ... 13 more
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    ... 15 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    ... 15 more

$ groovy MutualCertAuth.groovy # with code change .keyStoreFile("client-unknown.jks")
400
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.13.0</center>
</body>
</html>

Lo anterior es usando la herramienta curl o un un programa en la plataforma Java, en el caso de querer realizar autenticación mutua con un navegador web como Firefox hay que instalar el certificado del cliente y si es necesario el certificado de la autoridad de certificación para que el candado indicativo de la seguridad del protocolo HTTPS se muestre en verde y no indique ningún problema de seguridad en la autenticación del servidor. En Firefox los certificados se añaden en el menú Preferencias > Avanzado > Ver certficados. En la pestaña Sus certificados hay que importar el certificado del cliente en formato PKCS12 y en la pestaña Autoridades el certificado de la autoridad que haya firmado el certificado del servidor, con el botón Importar se selecciona el archivo crt de la autoridad. Al introducir la URL y realizar la petición Firefox solicita mediante un diálogo seleccionar el certificado a usar para realizar la autenticación en el servidor.

Autenticación mutua de cliente y servidor con el navegador web Firefox

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando docker-compose up, groovy MutualCertAuth.groovy.