Autenticación SSL con certificado de cliente

8
13945

En este tutorial vamos a ver cómo autenticarnos contra un servidor con autenticación por certificado de cliente.

Índice de contenidos

1. Introducción

A menudo existe confusión cuando nos autenticamos contra un servidor usando SSL, y buscando por internet cuando tenemos dudas vemos que esa confusión es extendida y que los términos no están del todo claros. En este tutorial vamos a tratar de explicar al detalle, pero sin abrumar, el protocolo, la terminología y los puntos clave para entender y conseguir autenticarnos de forma correcta usando certificado de cliente. Lo vamos a hacer con Java pero podría extenderse a cualquier lenguaje, lo importante es tener claro el concepto.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.2 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.5
  • Entorno de desarrollo: Eclipse Luna
  • JDK 1.7
  • Spring Framework versión: 3.2.2.RELEASE

3. Terminología, aclarando los conceptos

Esta es la parte que todos nos saltamos hasta que llevamos tres horas y no sale, así que intentaré ser breve y claro.

Certificado de servidor: El certificado del servidor sirve para autenticar a un servidor, es decir, para que nosotros sepamos con quién estamos hablando. Tiene que estar firmado por una autoridad certificadora (conocida normalmente como CA) de confianza como por ejemplo VeriSign. Si este es nuestro caso, que estamos tratando de contactar con un servidor cuyo certificado lo firma VeriSign normalmente no tendremos que hacer nada. Por el contrario, si el certificado no está firmado por una autoridad certificadora reconocida internacionalmente, sino que lo hemos firmado por ejemplo nosotros mismos, sería lo denominado como certificado auto-firmado(self-signed) entonces tendremos que añadir este certificado a nuestro almacén de certificados de confianza (donde por defecto está VeriSign y otros muchos reconocidos internacionalmente).

Almacén de certificados de confianza: Más conocido en Java como Truststore o cacerts. Aquí como decía en el parrafo anterior tenemos las entidades certificadoras. Es normalmente un almacén de claves públicas de confianza. Es lo que utiliza el cliente para autenticar o confiar en el servidor. Para localizarlo exactamente podemos usar el siguiente comando si tenemos definida la variable JAVA_HOME: cd $JAVA_HOME/jre/lib/security. Aquí tenemos el fichero cacerts. Para listar su contenido podemos usar el siguiente comando: keytool -list -v -keystore $JAVA_HOME/jre/lib/security/cacerts. La contraseña por defecto es «changeit». Te recomiendo que lo hagas ahora mismo así sabrás un poco mejor de que estoy hablando.

Almacén de claves: Aquí es donde viene la confusión de la mayoría de la gente así que me voy a extender algo más para que quede claro. Conocido en Java como Keystore es un almacén de certificados (compuestos de clave pública y privada) que utiliza el cliente para identificarse él contra el servidor. No todos los servidores requieren que el cliente se autentique, pero sí que el protocolo SSL obliga a un servidor a estar firmado por un certificado en el que el cliente confíe. Este es el principal motivo de que Java sí tenga un trustore (cacerts) por defecto con el que funciona el protocolo SSL, porque es obligatorio por el propio protocolo tenerlo. En cambio la parte de autenticación de cliente contra el servidor, que es otro paso más del handshake inicial no es necesaria, es opcional, por lo que si quieres usarlo, tienes que configurarlo. Y la confusión en internet está en que mucha gente utiliza los nombres trustore y keystore como si fueran sinónimos cuando no lo son. Son cosas distintas y las clases que usa Java para manejar uno u otro de forma interna también son distintas. La confusión en gran parte viene de que muchísima gente opta por usar el trustore de Java (cacerts) como Keystore. Al fin y al cabo es un almacén de certificados, y meten ahí tanto las claves públicas de confianza, como los pares de clave pública-privada que se usan para autenticar al cliente. Mi recomendación es que esto no se haga. Yo recomiendo tener un keystore aparte, con los certificados que sean necesarios y mis razones son las siguientes:

  • Si lo metes en el cacerts, el cacerts es compartido entre aplicaciones, ya que va a nivel de máquina virtual de Java y vas a tener certificados para que un cliente se autentique contra un servidor disponible en todas tus aplicaciones, cuando seguramente sea solo una la que realmente lo va a usar.
  • Al diferenciarlos estamos entendiendo que son dos conceptos diferentes, que van separados, y evitamos confusiones futuras tanto a compañeros como a nuestro yo del futuro, además soy partidario de llamar siempre a todo por su nombre, ya sean almacenes, nombres de métodos o clases.
  • Creo que es más mantenible si tenemos cinco aplicaciones distintas que se autentican contra servidores distintos tener un keystore por aplicación, así sabemos exactamente qué keystore se usa en cada aplicación y qué certificado se está usando en cada aplicación, y no lo tenemos todo junto y mezclado en el cacerts como si fuera un cajón de sastre, con otros muchos certificados que pueden dificultar incluso el ver si lo hemos metido o no.

4. Configuración del Keystore con Spring

Si estás usando Spring en tu aplicación entonces te recomiendo que indiques el keystore en un fichero de configuración de Spring, y que cojas los parámetros de un fichero .properties, para en un momento dado poder cambiar el almacén, reiniciar la aplicación y que todo siga funcionando sin necesidad de sacar una nueva versión. Yo lo hago de la siguiente manera:

applicationContext.xml

Si no estás usando Spring puedes indicar el keystore con variables de java de la siguiente forma: -Djavax.net.ssl.keyStore=/path/to/keystore -Djavax.net.ssl.keyStorePassword=yourpasswordgoeshere -Djavax.net.ssl.keyStoreType=pkcs12

Yo he puesto que el tipo de almacén es pkcs12 pero tu puedes poner cualquier otro, como por ejemplo jks.

5. Una prueba rápida para ver si funciona

SSLClientAuthenticationWithCertificate.java

Primero estoy seteando la propiedad javax.net.debug, a ssl y handshake para que la ejecución de esta clase me proporcione información del inicio de la comunicación. Esto es conveniente cuando estás debugeando y contactando con el servidor para ver en qué punto nos está fallando (si es que nos está fallando claro). Si no nos está fallando nos servirá para enterarnos mejor de qué está pasando y en qué orden.

En segundo lugar estoy metiendo las propiedades antes mencionadas del keystore. Para esta prueba las podemos meter así y nos olvidamos del servidor y de spring.

En último lugar tan solo configuro la url donde voy a hacer un GET, y abro la conexión. A continuación tengo un método para que nos saque por consola la respuesta y saber si estamos haciendo bien el get o no.

Si ejecutamos esta pequeña aplicación nos tiene que salir un log que nos dice paso a paso qué ha ido ocurriendo. Lo vamos a analizar en el siguiente apartado.

5.1. Analizando los logs de ssl en modo debug

En primer lugar tendremos que ver algo del estilo de lo siguiente:

Debug de la prueba. Parte 1.

Lo primero que observamos es que el keystore se ha cargado de forma correcta. Además nos dice que ha encontrado un certificado, que es justo el único que he metido en mi almacén p12. A continuación pasa a logar dicho certificado. Donde pongo algunas XXXXXX es para ocultar mi certificado, pero ahí deberían verse los datos del certificado.

Debug de la prueba. Parte 2.

En la segunda parte del log vemos cómo se nos dice dónde está el almacén de confianza que estamos usando en la comunicación y qué entidades son en las que confiamos, esto es, el cacerts. Un error muy común suele ser añadir certificados autofirmados en los que vamos a confiar, en un almacén que luego no está usando Java. Aquí podemos ver que almacén exactamente es el trustore. Como veis tenemos entidades certificadoras conocidas internacionalmente entre las que está SwissSign, VeriSign … A modo de apunte diré que java separa esta parte en dos. Seamos nosotros también consecuentes, llamemos a cada cosa por su nombre.

Debug de la prueba. Parte 3.

Este es el Client Hello y para no extenderme más de la cuenta diré solamente que el cliente propone la versión del protocolo que se va a usar, en este caso TLSv1, y una serie de cifradores que puede usar, entre otras cosas que para el alcance del tutorial no nos interesan.

Debug de la prueba. Parte 4.

El servidor responde con el Server Hello indicando el protocolo que se va a utilizar, TLSv1 en este caso, y con el algoritmo de cifrado, en este caso TLS_RSA_WITH_AES_128_CBC_SHA

Debug de la prueba. Parte 5.

En esta parte he borrado mucha paja para poder ver lo que nos interesa. Esta parte es donde el servidor se identifica, dice quién es él. Y este quién es, es una cadena de certificados, en este caso de dos certificados. El servidor primero me dice que es XXXX y que VeriSign Class 3 International Server CA – G3 afirma que XXXXX es efectivamente quien dice ser. Yo no me fío de XXXXXX, pero como VeriSign es quien le firma el certificado, y yo sí que me fío de VeriSign por extensión me fío de XXX. Perdón por la redundancia pero quería que quedara muy muy claro.

Debug de la prueba. Parte 6.

En esta parte vemos lo que comentaba anteriormente. Nosotros confiamos en quien ha firmado el certificado del servidor al que nos estamos conectando por lo que confiamos en él. Hasta este punto, tenemos lo que viene siendo la parte común y obligatorio por así decirlo en toda comunicación SSL. A partir de ahora vamos a ver la parte de autenticación del cliente. Nosotros confiamos en el servidor hasta aquí, pero ahora es el servidor quien tiene que confiar en nosotros.

Debug de la prueba. Parte 7.

Ahora el servidor nos presenta una lista de certificados que el cliente puede presentar para autenticarse. Uno de estos deberá estar en nuestro keystore para que la comunicación continue.

Debug de la prueba. Parte 8.

Como vemos en la primera línea tenemos una coincidencia. De la lista de certificados que envió el servidor en nuestro keystore tenemos uno que lo satisface, y se nos dice cuál es. A continuación lo que se hace es que firmamos con la clave privada un challenge o desafío aleatorio que nos manda el servidor. Este se descifrará con nuestra clave pública, y si es correcto entonces el servidor se asegura de que nosotros somos quien decimos ser, pues la clave privada solo la conocemos nosotros.

Debug de la prueba. Parte 9.

Si todo va bien deberíamos ver algo como lo de arriba. Al final viene en mi caso en formato XML la respuesta del servidor.

6. Conclusiones

Tenemos que tener claro este tipo de comunicaciones y cómo funcionan para poder saber exactamente qué es lo que estamos haciendo. Esto nos ayudará a la hora de buscar ayuda en google y saber apuntar mejor el tiro cuando tratemos de solventar un error. Espero que el tutorial te haya aclarado un poco los términos y el protocolo y te haya servido como a mí, para entender mejor SSL.

7. Referencias

8 Comentarios

  1. Hola, gracias por compartir tus conocimientos sobre este tema, muy interesantes.
    Lo cierto es que estoy un paso antes, tratando de configurar un certificado de Godaddy sobre tomcat7 en un CentOS y no terminamos de configurarlo.
    Os dejo mi correo, no se si admitís consultas.
    Los conceptos que tu propones los tenia claros, debí de entendlos en la documentación de tomcat correctamente, pero por alguna cosa que se me escapa no consigo configurar y que me funcione.
    Si alguien está dispuesto a ayudar me podéis dar una respuesta al correo que os dejo.

    Gracias.

  2. Gracias por el tutorial, es muy cierto que si no tienes los conceptos básicos claros se puede volver todo un infierno y perderte entre tutoriales, foros y configuración.

  3. Hola me podrian ayudar necesito que en una aplicacion java sin contenedores(jboss,tomcat,etc) inserte un certificado https, para que cuando la aplicacion consuma el web services el certificado vaya incluido.

  4. Buenas, con el codigo de ejemplo obtengo este error: unable to find valid certification path to requested target

    http-nio-8080-exec-1, handling exception: 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.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

    Alguien podría darme una mano?

    paso mi código:

    System.setProperty(«javax.net.debug»,»ssl,handshake»);

    System.setProperty(«javax.net.ssl.keyStoreType», «PKCS12»);
    System.setProperty(«javax.net.ssl.keyStore», «C:/XX.p12»);
    System.setProperty(«javax.net.ssl.keyStorePassword», «XXXX»);

    URL url = new URL(«https://XXXXXXXX»);
    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

    connection.setDoOutput(true);
    connection.setRequestMethod(«POST»);
    connection.setRequestProperty(«Authorization», «Basic XXXXXXXXXXXXXXX»);
    connection.setRequestProperty(«content-type», «application/x-www-form-urlencoded»);

    String body = «MENSAJE»;

    connection.setRequestProperty(«Content-Length», Integer.toString(body.length()));

    connection.getOutputStream().write(body.getBytes());

    final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    String inputLine;
    final StringBuffer response = new StringBuffer();
    while ((inputLine = bufferedReader.readLine()) != null) {
    response.append(inputLine);
    }
    bufferedReader.close();
    System.out.println(response.toString());

  5. Hola,

    disculpen por mi desconocimiento, pero si estás poniendo el keystore con el certificado en el lado del cliente ¿no es vulnerable a que otra persona lo use para hacerse pasar por el servidor? O ¿simplemente creo que estoy equivocado?

    Disculpen mi ignorancia.
    Un saludo.

    • Diego,

      en el keystore estás poniendo el par de claves privada/pública que utiliza el CLIENTE para identificarse. Esto le va a permitir identificarse contra cualquier servidor que se lo solicite, y validará cuando en el servidor se haya añadido la clave pública del cliente.

      En el truststore estás metiendo el certificado del servidor en el que vas a confiar.

      Tanto servidor como cliente tienen que firmar enviar la información cifrada con su clave privada. Ningún servidor te va a poder engañar si no tienen esa clave privada, lo cual no está en tu mano

Dejar respuesta

Please enter your comment!
Please enter your name here