11.3.14

Autenticación de cliente con certificado, usando un Provider propio

Realizar conexiones a un servidor HTTPS, únicamente con las clases que proporciona el JDK, es bastante sencillo. Hay mucha información, tanto en la red como en la propia documentación de Oracle. Si necesitamos autenticarnos con un certificado de cliente, que tengamos en un keystore, es también bastante sencillo. Con una simple búsqueda en Google averiguaremos que tenemos que ajustar determinadas propiedades del sistema, bien en el arranque de nuestra aplicación (con los parámetros -D), bien en nuestro código (con la clase System). Por ejemplo, si nuestra pareja de clave privada y certificado está en fichero PKCS#12, en la ruta /ruta/credenciales.p12, y la contraseña del mismo es «clave», tendríamos que ajustarlas así:


javax.net.ssl.keyStore = /ruta/credenciales.p12
javax.net.ssl.keyStorePassword = clave
javax.net.ssl.keyStoreType = PKCS12

Esto nos valdría tanto para un keystore de Java (tipo JKS) como para un PKCS#12 (tipo PKCS12). La cosa se complica un poco si no tenemos nuestra clave privada en un fichero accesible, sino en un dispositivo criptográfico. Si tenemos una implementación PKCS#11 para el mismo, tampoco habría demasiado problema. Necesitamos configurar adecuadamente el Provider SunPKCS11, tal y como nos explica la documentación de Oracle, y ajustar las siguientes propiedades de sistema:


javax.net.ssl.keyStore = NONE
javax.net.ssl.keyStorePassword = clave
javax.net.ssl.keyStoreType = PKCS11

Pero ¿qué tenemos que hacer en casos más particulares? Concretamente, ¿cómo usaríamos un Provider diferente a los del JDK?

Imaginemos la siguiente situación real: las claves y certificados están en una máquina remota, a la que se accede con un protocolo de red, y que tiene un API que permite obtener una lista de las claves y certificados, el certificado en sí, y realizar una firma digital de los datos que se le pasen. Es decir, una máquina que a todos los efectos se comporte como un hardware criptográfico (las claves privadas nunca salen de allí), pero que no tiene un interfaz PKCS#11. Para poder usar esta infraestructura desde Java, se implementó un Provider propio, que proporcionaba implementaciones de KeyStore, PrivateKey y Signature. No entraré en los detalles de cómo implementar un Provider, ya que está bastante bien documentado en Java Cryptography Architecture (JCA) Reference Guide.

¿Cómo usar esta implementación para autenticarnos en una conexión HTTPS? Fácil. Tenemos que actuar de forma similar al PKCS#11, pero utilizando los datos de nuestra implementación:


javax.net.ssl.keyStore = NONE
javax.net.ssl.keyStorePassword = clave (si fuera necesaria)
javax.net.ssl.keyStoreType = Nombre de nuestro nuevo tipo de KeyStore
javax.net.ssl.keyStoreProvider = Nombre de nuestro Provider

Hay unas consideraciones a tener en cuenta. Por un lado, si en nuestro Provider hemos definido un nuevo tipo de KeyStore, cuyo nombre no coincida con los de algún otro Provider, no necesitamos especificar la propiedad javax.net.ssl.keyStoreProvider, puesto que el nuestro será el único que lo proporcione.

Más importante. Debemos asegurarnos de que nuestra implementación acepte el algoritmo de firma NONEwithRSA. Como sabréis, una autenticación con certificado se basa en la firma de un token que genera la parte contraria. Pues bien, el JRE utiliza NONEwithRSA como algoritmo de firma en una conexión SSL o TLS. Si nuestra implementación no lo soporta, no podremos usarla.

Compliquemos un poco las cosas. Supongamos ahora que el KeyStore de nuestro Provider, no tiene una única entrada, sino varias. ¿Cómo sabe el JRE qué clave utilizar? Bueno, pues no lo sabe, y simplemente elige la primera que encaja (no necesariamente la primera que encuentra, ya que el certificado debe estar emitido por una CA que el servidor confíe). Si queremos especificar una clave concreta entre varias, debemos además implementar nuestro propio KeyManager. Para ello, Oracle recomienda heredar de la clase X509ExtendedKeyManager. El método clave es chooseClientAlias, que es al que se llama cuando el JRE necesita saber cuál de todas las entradas del KeyStore debe utilizar.

Pero ojo. No es suficiente con esto. Tal y como está diseñado todo el tinglado, debemos decirle de forma más explícita al JRE que use nuestro KeyManager. Para ello, debemos obtener una instancia de KeyManagerFactory (también una implementación propia que deberemos configurar en nuestro Provider, y que devolverá nuestra implementación de KeyManager), usarla para inicializar un SSLContext, obtener de él una SSLSocketFactory, y pasársela a la HttpsURLConnection que respresenta nuestra conexión HTTPS, y que es la implementación que nos devolverá URL.openConnection() si usamos el protocolo HTTPS.

Os dejo un pequeño ejemplo:


  KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("MyKeyManager", "MyProvider");
  KeyStore keyStore = KeyStore.getInstance("MyKeyStore");
  keyStore.load(null, null);
  keyManagerFactory.init(keyStore, new char[0]);

  SSLContext sslContext = SSLContext.getInstance("TSL");
  sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
  SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
  
  URL url = new URL("https://server.com/example");

  HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
  httpsURLConnection.setSSLSocketFactory(sslSocketFactory());

He obviado todo lo relacionado con la implementación del Provider y su configuración, y he supuesto que no es necesaria ninguna contraseña.