Autenticación de certificado de cliente Java HTTPS

Soy bastante nuevo en HTTPS / SSL / TLS y estoy un poco confundido sobre qué se supone que los clientes deben presentar al autenticarse con certificados.

Estoy escribiendo un cliente Java que necesita hacer un simple POST de datos a una URL en particular. Esa parte funciona bien, el único problema es que se supone que debe hacerse a través de HTTPS. La parte HTTPS es bastante fácil de manejar (ya sea con HTTPclient o con el soporte HTTPS incorporado de Java), pero estoy atascado en la autenticación con certificados de cliente. Me di cuenta de que ya hay una pregunta muy similar aquí, que aún no probé con mi código (lo haré lo suficientemente pronto). Mi problema actual es que, haga lo que haga, el cliente de Java nunca envía el certificado (puedo verificarlo con volcados de PCAP).

Me gustaría saber qué es exactamente lo que se supone que el cliente debe presentar al servidor cuando se autentica con certificados (específicamente para Java, si eso importa en absoluto). ¿Es este un archivo JKS, o PKCS # 12? Lo que se supone que debe estar en ellos; solo el certificado del cliente, o una clave? Si es así, ¿qué tecla? Existe una gran confusión sobre los diferentes tipos de archivos, tipos de certificados y demás.

Como ya he dicho antes, soy nuevo en HTTPS / SSL / TLS, por lo que agradecería algo de información de fondo (no tiene que ser un ensayo, me conformaré con enlaces a buenos artículos).

Finalmente logré resolver todos los problemas, así que responderé mi propia pregunta. Estas son las configuraciones / archivos que he usado para resolver mis problemas particulares;

El almacén de claves del cliente es un archivo de formato PKCS # 12 que contiene

  1. El certificado público del cliente (en este caso, firmado por una CA autofirmada)
  2. La clave privada del cliente

Para generarlo pkcs12 comando pkcs12 de OpenSSL, por ejemplo;

 openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever" 

Consejo: asegúrese de obtener el último OpenSSL, no la versión 0.9.8h, ya que parece sufrir un error que no le permite generar correctamente los archivos PKCS # 12.

Este archivo PKCS # 12 será utilizado por el cliente de Java para presentar el certificado del cliente al servidor cuando el servidor haya solicitado explícitamente al cliente que se autentique. Consulte el artículo de Wikipedia sobre TLS para obtener una descripción general de cómo funciona realmente el protocolo de autenticación de certificados de clientes (también explica por qué necesitamos la clave privada del cliente aquí).

El almacén de confianza del cliente es un archivo de formato JKS sencillo que contiene los certificados de CA raíz o intermedia . Estos certificados de CA determinarán con qué puntos extremos se le permitirá comunicarse, en este caso, le permitirá a su cliente conectarse a cualquier servidor que presente un certificado que haya sido firmado por una de las CA’s del almacén de confianza.

Para generarlo, puede usar la herramienta de claves Java estándar, por ejemplo;

 keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca 

Al usar este almacén de confianza, su cliente intentará realizar un protocolo de enlace SSL completo con todos los servidores que presenten un certificado firmado por la CA identificada por myca.crt .

Los archivos anteriores son estrictamente solo para el cliente. Cuando quiera configurar un servidor también, el servidor necesita sus propios archivos de claves y de almacén de confianza. En este sitio web se puede encontrar una guía práctica excelente para configurar un ejemplo completamente funcional para un cliente y servidor Java (utilizando Tomcat).

Problemas / Observaciones / Consejos

  1. La autenticación del certificado de cliente solo puede ser aplicada por el servidor.
  2. ( ¡Importante! ) Cuando el servidor solicita un certificado de cliente (como parte del intercambio de información de TLS), también proporciona una lista de CA de confianza como parte de la solicitud de certificado. Cuando el certificado del cliente que desea presentar para la autenticación no está firmado por una de estas CA, no se presentará en absoluto (en mi opinión, este comportamiento es extraño, pero estoy seguro de que hay una razón para ello). Esta fue la causa principal de mis problemas, ya que la otra parte no había configurado su servidor correctamente para aceptar mi certificado de cliente autofirmado y supusimos que el problema estaba en mi extremo por no proporcionar correctamente el certificado del cliente en la solicitud.
  3. Obtener Wireshark. Tiene un gran análisis de paquetes SSL / HTTPS y será una gran ayuda para la eliminación de fallas y para encontrar el problema. Es similar a -Djavax.net.debug=ssl pero es más estructurado y (posiblemente) más fácil de interpretar si no se siente cómodo con la salida de depuración Java SSL.
  4. Es perfectamente posible utilizar la biblioteca httpclient de Apache. Si desea usar httpclient, simplemente reemplace la URL de destino con el equivalente HTTPS y agregue los siguientes argumentos de JVM (que son los mismos para cualquier otro cliente, independientemente de la biblioteca que desee usar para enviar / recibir datos a través de HTTP / HTTPS) :

     -Djavax.net.debug=ssl -Djavax.net.ssl.keyStoreType=pkcs12 -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStorePassword=whatever -Djavax.net.ssl.trustStoreType=jks -Djavax.net.ssl.trustStore=client-truststore.jks -Djavax.net.ssl.trustStorePassword=whatever 

Otras respuestas muestran cómo configurar globalmente certificados de clientes. Sin embargo, si desea definir programáticamente la clave de cliente para una conexión en particular, en lugar de definirla globalmente en cada aplicación que se ejecute en su JVM, entonces puede configurar su propio SSLContext de la siguiente manera:

 String keyPassphrase = ""; KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray()); SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(keyStore, null) .build(); HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build(); HttpResponse response = httpClient.execute(new HttpGet("https://example.com")); 

El archivo JKS es solo un contenedor para certificados y pares de claves. En un escenario de autenticación del lado del cliente, las diversas partes de las claves se ubicarán aquí:

  • La tienda del cliente contendrá el par de claves pública y privada del cliente. Se llama almacén de claves .
  • La tienda del servidor contendrá la clave pública del cliente. Se llama un almacén de confianza .

La separación de truststore y keystore no es obligatoria, pero se recomienda. Pueden ser el mismo archivo físico.

Para establecer las ubicaciones del sistema de archivos de las dos tiendas, use las siguientes propiedades del sistema:

 -Djavax.net.ssl.keyStore=clientsidestore.jks 

y en el servidor:

 -Djavax.net.ssl.trustStore=serversidestore.jks 

Para exportar el certificado del cliente (clave pública) a un archivo, para que pueda copiarlo en el servidor, use

 keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks 

Para importar la clave pública del cliente en el almacén de claves del servidor, utilice (como el cartel mencionado, esto ya lo han hecho los administradores del servidor)

 keytool -import -file publicclientkey.cer -store serversidestore.jks 

Maven pom.xml:

 < ?xml version="1.0" encoding="UTF-8"?>  4.0.0 some.examples sslcliauth 1.0-SNAPSHOT jar sslcliauth   org.apache.httpcomponents httpclient 4.4    

Código Java:

 package some.examples; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContexts; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.http.entity.InputStreamEntity; public class SSLCliAuthExample { private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName()); private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS"; private static final String CA_KEYSTORE_PATH = "./cacert.jks"; private static final String CA_KEYSTORE_PASS = "changeit"; private static final String CLIENT_KEYSTORE_TYPE = "PKCS12"; private static final String CLIENT_KEYSTORE_PATH = "./client.p12"; private static final String CLIENT_KEYSTORE_PASS = "changeit"; public static void main(String[] args) throws Exception { requestTimestamp(); } public final static void requestTimestamp() throws Exception { SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory( createSslCustomContext(), new String[]{"TLSv1"}, // Allow TLSv1 protocol only null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) { HttpPost req = new HttpPost("https://changeit.com/changeit"); req.setConfig(configureRequest()); HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin")); req.setEntity(ent); try (CloseableHttpResponse response = httpclient.execute(req)) { HttpEntity entity = response.getEntity(); LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine()); EntityUtils.consume(entity); LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString()); } } } public static RequestConfig configureRequest() { HttpHost proxy = new HttpHost("changeit.local", 8080, "http"); RequestConfig config = RequestConfig.custom() .setProxy(proxy) .build(); return config; } public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException { // Trusted CA keystore KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE); tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray()); // Client keystore KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE); cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray()); SSLContext sslcontext = SSLContexts.custom() //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate .build(); return sslcontext; } } 

Para aquellos de ustedes que simplemente desean configurar una autenticación bidireccional (certificados de servidor y cliente), una combinación de estos dos enlaces lo llevará hasta allí:

Configuración de autenticación bidireccional:

https://linuxconfig.org/apache-web-server-ssl-authentication

No necesita usar el archivo de configuración de openssl que mencionan; Solo usa

  • $ openssl genrsa -des3 -out ca.key 4096

  • $ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

para generar su propio certificado de CA, y luego generar y firmar las claves del servidor y del cliente a través de:

  • $ openssl genrsa -des3 -out server.key 4096

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

  • $ openssl x509 -req -days 365 -en server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

y

  • $ openssl genrsa -des3 -out client.key 4096

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

  • $ openssl x509 -req -days 365 -en client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

Para el rest siga los pasos en el enlace. Administrar los certificados para Chrome funciona igual que en el ejemplo de Firefox que se menciona.

A continuación, configure el servidor a través de:

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04

Tenga en cuenta que ya ha creado el servidor .crt y .key por lo que ya no tiene que hacer ese paso.

Creo que la solución aquí fue el tipo de almacén de claves, pkcs12 (pfx) siempre tiene clave privada y el tipo JKS puede existir sin clave privada. A menos que especifique en su código o seleccione un certificado a través del navegador, el servidor no tiene manera de saber que representa a un cliente en el otro extremo.