Socket SSL con Java
Veamos un par de ejemplos sencillos de cómo establecer una conexión client/servidor con socket seguros SSL en java.
Los certificados[editar]
Cuando dos socket SSL intentan establecer conexión (un socket cliente y un socket servidor), lo primero que hacen es "presentarse" el uno al otro y cada uno de ellos comprueba que el otro es de "confianza". Si todo va bien, la conexión se establece. Si uno no confía en el otro, la conexión no se establece.
¿Cómo se presentan el uno al otro?. Debemos crear para cada socket un certificado. Este certificado no es más que un fichero que generaremos con la herramienta keytool que viene con java. Necesitaremos crear dos certificados, uno para el servidor y otro para el cliente. Veremos más adelante el detalle de cómo crearlo,
¿Cómo confía uno en el otro?. Cada uno de los socket tiene un fichero almacén donde tiene almacenados los certificados que son de confianza para él. Por ello, en el almacén del cliente debemos meter el certificado del servidor y en el almacén del servidor debemos meter el certificado del cliente. Estos almacenes no son más que ficheros nuevamente creados con la herramienta keytool de java.
Creación del certificado de cliente/servidor[editar]
Vamos a crear el certificado del servidor. Para el cliente será igual, únicamente cambiaremos los datos y nombres de fichero a nuestro gusto. El certificado de servidor se genera con una ventana de comandos (cmd de windows o bash de linux) con el siguiente comando
keytool -genkey -keyalg RSA -alias serverKay -keystore serverKey.jks -storepass servpass
- keytool está en el directorio bin de donde tengamos instalado java.
- Con la opción -genkey le estamos diciendo que genere un certificado.
- -keyalg RSA le indicamos que lo queremos encriptado con el algorítmo RSA
- -alias serverKey. El certificado se meterá en un fichero de almacén de certificados que podrá contener varios certificados. Este alias es el nombre con el que luego podremos identificar este certificado concreto dentro del almacén. Podemos poner cualquier nombre que nos de una pista de qué es ese certificado.
- -keystore serverKey.jks. Este es el fichero que hará de almacén de certificados. Si no existe se crea, si ya existe se añade el certificado con el alias que se haya indicado.
- -storepass servpass. El almacén está protegido con contraseña, para acceder a él necesitamos la contraseña. Si el almacén no existe, se crea usando esta contraseña, por lo que deberemos recordarla. Si ya existe, debemos proporcionar la contraseña que tuviera ese almacén.
Al ejecutar el comando, nos pedirá una serie de datos como nombre, apellidos, etc. Normalmente un certificado va asociado a alguna persona u organización, por lo que aquí nos pide esos datos. En nuestro caso, va asociado al servidor y ponemos datos inventados. La siguiente es la secuencia de datos que se nos pedirá
c:>keytool -genkey -keyalg RSA -alias serverKay -keystore serverKey.jks -storepass servpass ¿Cuáles son su nombre y su apellido? [Unknown]: Server ¿Cuál es el nombre de su unidad de organización? [Unknown]: Unidad ¿Cuál es el nombre de su organización? [Unknown]: Organizacion ¿Cuál es el nombre de su ciudad o localidad? [Unknown]: Ciudad ¿Cuál es el nombre de su estado o provincia? [Unknown]: Estado ¿Cuál es el código de país de dos letras de la unidad? [Unknown]: ES [no]: si Introduzca la contraseña de clave para <serverKay> (INTRO si es la misma contraseña que la del almacén de claves): c:>
Al final se nos pide una contraseña para proteger al certificado en sí mismo. Esta contraseña podría ser distinta de la que hemos puesto al almacén de claves, pero java luego nos exigirá que sean iguales, así que en la última pregunta pulsamos INTRO para que sea la misma.
Listo, con esto se genera el fichero serverKey.jks, que es un almacén de certificados y dentro tiene el certificado de nuestro servidor. Deberíamos hacer ahora lo mismo para generar un certificado de cliente en un almacén distinto (con otro nombre).
Una vez que tenemos el almacén con el certificado del servidor, vamos a generar el almacén de confianza para el cliente, que debe contener este mismo certificado de servidor. Como el certificado del servidor está dentro de un almacén, tenemos que sacarlo de ahí a un fichero. El comando es keytool
keytool -export -keystore serverkey.jks -alias serverKey -file ServerPublicKey.cer
- -export es para exportar el certificado
- -keystore serverkey.jks indica en qué almacén está el certificado que queremos exportar
- -alias serverKey es el identificador del certificado dentro del almacén. Debe ser el mismo alias que pusimos cuando lo creamos.
- -file ServerPublicKey.cer es el nombre del fichero donde queremos que se guarde el certificado que vamos a extraer.
Al ejecutar este comando nos pedirá la password del almacén/certificado, que recordamos y que habíamos puesto iguales porque java luego nos lo exige así.
Una vez que tenemos el fichero ServerPublicKey.cer con el certificado, debemos meterlo dentro de un almacén de certificados de confianza del cliente. Cómo no, el comando a utilizar es keytool
keytool -import -alias serverKey -file ServerPublicKey.cer -keystore clientTrustedCerts.jks -keypass clientpass -storepass clientpass
- -import para indicar que queremos meter un certificado existente en un almacén.
- -alias serverKey es el identificador que queremos dar al servidor dentro del almacén de certificados de confianza del cliente. Hemos puesto otra vez serverKey, pero al ser un almacén distinto de el del servidor, podríamos poner otro nombre.
- -file ServerPublicKey.cer El certificado que queremos importar
- -keystore clientTrustedCerts.jks el almacén de certificados de confianza del cliente, se creará si no existe.
- -keypass clientpass Clave para proteger el certificado dentro del almacén. Debe ser la misma que la del almacén.
- -storepass clientpass Clave para el almacén, si el almacén existe debe ser la que diéramos en el momento de crearlo. Si no existe, el almacén se creará protegido con esta clave. Esta clave debe coincidir además con la que pongamos en la opción -keypass porque luego el código java lo exigirá así.
Listo, ya tenemos el almacén de certificados de confianza clientTrustedCerts.jks. Debemos repetir todo el proceso para crear el almacén de certificados de confianza para el servidor, metiendo en él el certificado del cliente.
Algunas consideraciones[editar]
Para una prueba rápida podemos crear un único certificado dentro de un único almacén. Usaremos ese almacén tanto como almacén del servidor, del cliente, de almacén de certificados de confianza del servidor y del cliente. Esto funcionará bien y es la forma más sencilla, pero no sería una implementación seria. El motivo es que los certificados sirven para identificar a cada una de las partes y cada parte puede confiar en unos u otros. Si usamos el mismo fichero para todo, el cliente y el servidor se identifican con el mismo certificado y van a confiar en los mismos. Una implementación seria debe tener los cuatro almacenes separados, de forma que cliente y servidor tengan sus propios certificados y la posibilidad de decidir en quienes confían, que no tienen por qué ser necesariamente los mismos.
El socket SSL fácil[editar]
Ahora vamos a la parte java. La forma fácil de crear los socket SSL es usar las factorías de socket SSL por defecto que nos proporciona java. Para el lado del servidor, el código sería
SSLServerSocketFactory serverFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); ServerSocket serverSocket = serverFactory.createServerSocket(port);
Donde port es el puerto que queremos que escuche nuestro socket servidor y serverSocket ya un socket servidor que se maneja de la forma habitual.
En cuanto al cliente, el código sería
SSLSocketFactory clientFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); Socket client = clientFactory.createSocket(server, port);
Donde server y port son la dirección IP y puerto donde está el servidor. Obtenemos un socket client que se maneja de la forma habitual.
La pega aquí es ... ¿cómo indicamos dónde están los almacenes de certificados y certificados de confianza?. Lo hacemos con propiedades de System, o bien con opciones -D al arrancar nuestra aplicación java. Las propiedades a fijar son
- javax.net.ssl.keyStore para indicar el almacén donde está el certificado que nos identifica.
- javax.net.ssl.keyStorePassword La clave para acceder a ese almacén y para acceder al certificado dentro del almacén. Este el motivo por el que ambas claves deben ser iguales.
- javax.net.ssl.trustStore para indicar el almacén donde están los certificados en los que se confía.
- javax.net.ssl.trustStorePassword La clave para acceder a dicho almacén y a los certificados dentro del almacén.
Si lo hacemos con System.setProperty(), quedaría así
System.setProperty("javax.net.ssl.keyStore", "src/main/certs/server/serverKey.jks"); System.setProperty("javax.net.ssl.keyStorePassword","servpass"); System.setProperty("javax.net.ssl.trustStore", "src/main/certs/server/serverTrustedCerts.jks"); System.setProperty("javax.net.ssl.trustStorePassword", "servpass");
Esto sería en la parte del servidor y se deben ejecutar estas líneas antes de crear el socket con SSLServerSocketFactory.
Si lo hicieramos con opciones -D en el arranque de java, sería
java -Djavax.net.ssl.keyStore=src/main/certs/server/serverKey.jks -Djavax.net.ssl.keyStorePassword=servpass -Djavax.net.ssl.trustStore=src/main/certs/server/serverTrustedCerts.jks -Djavax.net.ssl.trustStorePassword=servpass ...
¿Cual es la pega de este mecanismo?. Que estas variables afectan a todo el programa java. Todos los socket que abramos presentarán el mismo certificado y confiarán en los mismos certificados. Si queremos poder establecer varios sockets con distintos certificados y distintos certificados de confianza, necesitamos algo más a medida. Vamos a ello
Indicar los certificados de los socket SSL desde código Java[editar]
Para indicar los almacenes desde código y específicos para cada socket que queramos abrir, debemos crear un SSLContext al que le indiquemos donde están los almacenes. Es a este SSLContext al que luego pediremos que cree los socket servidor o cliente. El esqueleto de código para servidor es este
TrustManager[] trustManagers = ... KeyManager[] keyManagers = ... SSLContext sc = SSLContext.getInstance("TLS"); sc.init(keyManagers, trustManagers, null); SSLServerSocketFactory ssf = sc.getServerSocketFactory(); ServerSocket serverSocket = ssf.createServerSocket(port);
o bien, para el cliente
TrustManager[] trustManagers = ... KeyManager[] keyManagers = ... SSLContext sc = SSLContext.getInstance("TLS"); sc.init(keyManagers, trustManagers, null); SSLSocketFactory ssf = sc.getSocketFactory(); SSLSocket client = (SSLSocket) ssf.createSocket(address, port); client.startHandshake();
SSLSocket es una clase hija de Socket, por lo que una vez establecida la conexión y el "handshake" o comprobación de certificados, se puede usar como un socket cliente normal.
Si te fijas en el código, nos faltan los TrustManager y los KeyManager, que de alguna forma son los almacenes de confianza y de certificados, ¿cómo los obtenemos?. El código para obtener el almacén de certificados para el servidor es este
KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream("src/main/certs/server/serverKey.jks"),"servpass".toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, "servpass".toCharArray()); KeyManager[] keyManagers = kmf.getKeyManagers();
para el cliente sería igual, pero cambiando el path/nombre del almacén y la clave.
En cuanto a los almacenes de certificados de confianza
KeyStore trustedStore = KeyStore.getInstance("JKS"); trustedStore.load(new FileInputStream("src/main/certs/server/serverTrustedCerts.jks"), "servpass".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustedStore); TrustManager[] trustManagers = tmf.getTrustManagers();
Nuevamente, para el cliente el código es igual pero cambiando path/nombre de almacén y clave de acceso.
Listo, con esto ya tenemos nuestro ServerSocket y Socket cliente habituales para envío y recepción de mensajes, pero de forma segura con SSL.
Código de ejemplo[editar]
Tienes el código completo de este ejemplo en https://github.com/chuidiang/chuidiang-ejemplos-google-code/tree/master/ejemplo-socket-ssl
En el directorio src/main/certs tienes los certificados y almacenes generados para la prueba. En el subdirectorio server los del servidor, en el subdirectorio client los del cliente.
Las clases src/main/java/com/chuidiang/ejemplos/SSLDefault*java usan las propiedades de java. AppDefault contiene el main para arrancar estas clases. Si te fijas en el código, están comentadas y las que están descomentadas mezclan cliente y servidor. Esto es así para que puedan arrancar cliente y servidor en un mismo ejecutable, ya que como se comentó antes, estas propiedades son compartidas y hay que buscar una configuración compatible, por ejemplo, usando el certificado del servidor y como de confianza el almacén del cliente, que es el que contiene el certificado del servidor.
Las clases src/main/java/com/chuidiang/ejemplos/SSLCustom*java son las que configuran los almacenes desde código. AppCustom contiene el main que arranca estas dos clases. Aquí no hay problemas como en el caso anterior y cada socket, aunque estén en el mismo ejecutable, usan configuraciones distintas.
La clase src/main/java/com/chuidiang/ejemplos/Util.java sólo contiene métodos para enviar y recibir mensajes por el socket y hacer que la aplicación envíe y reciba cosas si se la arranca.