Autenticación de cliente SSL en web services con Axis2

Normalmente uso Axis2 para crear mis clientes de web services; es bastante sencillo ya que solamente hay que ejecutar el script wsdl2java pasando el WSDL del web service y se genera una clase que contiene todo lo necesario para invocar los métodos publicados en el servicio.

Recientemente, me topé con un web service que en pruebas funcionó muy bien pero resulta que en producción había que activar SSL (o sea en el URL en vez de solamente ) y además tuve que implementar un esquema muy poco usual (aunque sea un estándar): autenticación de cliente con PKI (Public Key Infrastructure - Infraestructura de Llave Pública).

El funcionamiento de SSL por lo general es que solamente el servidor se autentica con el cliente, y luego el cliente se autentica de alguna forma específica a la aplicación. La manera en que funciona la autenticación del servidor en una conexión SSL es via PKI; el servidor utiliza un certificado X509 que contiene su información, una fecha de expiración, una llave pública (RSA por lo general) y algunos datos del emisor de dicho certificado. En PKI se maneja el concepto de cadena de confianza, que consiste en que para poder aceptar el certificado de alguien más como válido, es necesario conocer al emisor o emisores de dicho certificado, hasta llegar al certificado raíz; si todos son considerados válidos y conocidos por el cliente, entonces se puede considerar válido el certificado del servidor.

Con Axis2 o cualquier otra herramienta para web services, todo este mecanismo de autenticar al servidor se maneja ya de forma automática. De hecho quien se encarga es el mismo JRE, porque con el ambiente Java ya vienen incluidos los certificados raíz más populares, por lo que cualquier certificado emitido por ellos será aceptado como válido (siempre y cuando cumpla las otras condiciones, como que esté vigente, etc).

Pero en SSL también el servidor puede autenticar a los clientes de la misma forma, es decir, solicitando al cliente su certificado X509 para validarlo. Esto ya no se maneja de manera tan transparente en Java; una opción para lograr esto es tener el certificado cliente y la correspondiente llave privada, almacenados en el KeyStore del usuario que corre el programa que quiere hacer la conexión que requiere autenticación de cliente. Pero entonces hay que estar configurando el KeyStore de cada cliente, y eso puede ser problemático, o bien puede ser que ni siquiera tengamos permiso de modificar ese KeyStore y por lo tanto hay que usar un KeyStore privado.

Esto último fue mi caso: tengo una aplicación que invoca distintos web services y solamente uno de ellos me pide este esquema, por lo que no quiero modificar todo el KeyStore porque hay que hacerlo en desarrollo y luego en producción y cualquier otra persona que quiera invocar el web service tiene que hacer lo mismo. De modo que preferí hacer un KeyStore privado para este web service. En Axis2, necesitamos primero que nada crear una fábrica de sockets para el HttpClient que usa el cliente del web service. De modo que hay que implementar la interfaz  . En mi caso, recibí el certificado y la llave privada en un archivo de tipo PKCS#12, un formato que permite almacenar precisamente una serie de llaves privadas y sus correspondientes certificados, protegido todo por un password. Afortunadamente, Java permite leer este tipo de archivos como un KeyStore. Simplemente hay que hacer lo siguiente:

 

Ya que tenemos el KeyStore, necesitamos crear una KeyManagerFactory con el mismo:

 

Finalmente, ya que tenemos el KeyManagerFactory, debemos crear un SSLContext que es con el que vamos a crear los sockets que necesita el web service para enviar y recibir datos:

 

El objeto SSLContext es el que se debe usar en los métodos para crear sockets de nuestra implementación de SecureProtocolSocketFactory. La implementación es bastante directa, simplemente en cada método hay que devolver un socket creado por el SSLContext con los parámetros recibidos, por ejemplo:

 

Con esto apenas tenemos lo necesario para poder crear sockets SSL con un contexto que permite autenticar al cliente si el servidor lo requiere. Ahora tenemos que registrar este SecureProtocolSocketFactory con nuestro web service, para el protocolo https (podríamos ponerle el nombre que sea al protocolo; siempre y cuando usemos ese protocolo en el URL, se va a usar nuestra fábrica de sockets). Si el web service lo generamos usando Axis2 ADB con el programa wsdl2java (yo usé Axis2 1.5) entonces:

 

Con lo anterior, si le pasamos al web service un URL que comience con https:// entonces se va a usar nuestra fábrica de sockets, la cual tiene el SSLContext que contiene el certificado y llave privada del archivo PKCS12 para poder autenticarse con el server. Si el servidor tiene un certificado válido emitido por una de las autoridades certificadas reconocidas por nuestra instalación de Java, ya estamos listos para invocar el web service via SSL.

Adicionalmente, si el servidor usa un certificado auto-firmado, al invocar el web service vamos a obtener un error porque Java no reconoce el certificado del servidor; en este caso, hay que hacer algo de trabajo adicional. Necesitamos obtener el certificado del servidor, y guardarlo en un archivo de KeyStore. Por seguridad, no recomiendo usar el KeyStore default del usuario, sino usar uno separado. Para ello necesitamos un archivo con el certificado y luego lo importamos a un nuevo KeyStore con la herramienta keytool (esto no es tan relevante para este post, simplemente hay que ver la documentación del programa keytool; al final tendremos un archivo tipo JKS protegido por un password).

Una vez que tenemos el archivo, debemos crear un TrustManagerFactory, de preferencia en el mismo lugar donde creamos el KeyManagerFactory (dentro de nuestra fábrica de sockets, antes de crear el SSLContext):

 

Con esto, ya tenemos un SSLContext que crea sockets para conexiones SSL donde se reconoce únicamente el certificado auto-firmado del servidor y que se van a autenticar con el certificado y llave privada del archivo PKCS12.

Estuve peleándome un buen rato con esto porque no hay mucha documentación, espero le sirva a alguien en el futuro. Aplica para Axis2, lo hice con 1.5 pero supongo que funciona con algunas versiones anteriores.

De hecho esto funciona para autenticar a un cliente con cualquier servidor SSL; en el caso de web services hay que encapsular todo esto en la implementación de SecureProtocolSocketFactory pero en caso de usar SSL puro, el código será muy similar aunque vaya encapsulado en algún componente distinto.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de tuxin39

Muy buen tutorial ya que

Muy buen tutorial ya que como bien dices no hay mucha documentaciòn y sobre todo aterrizarla. Gracias estoy seguro que me va a servir en un futuro no tan lejano.

Problema

Me ha servido mucho el tutorial pero tengo un problema, cuando lanzo la peticion al servidor me da nullPointer. Te añado lo que he hecho,

muchas gracias

********************
**** Mi llamada ***
********************
rbm_ServerStub = new RBM_ServerStub(config.getValor("edoc.webService.urlImporges"));
String strTestConexion = "Cadena para el Test de Conexion";

TestConexion testConexion = new TestConexion();
testConexion.setDatoTest(strTestConexion);

SocketFactory fabrica = new SocketFactory();
fabrica.createSocket("79.148.117.159", 443);
Protocol proto = new Protocol("https", fabrica, 443);
//El protocolo tiene el nombre, la fabrica de sockets, y el puerto default a donde deben conectarse
rbm_ServerStub._getServiceClient().getOptions().setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER, proto);

TestConexionResponse testConexionResponse = rbm_ServerStub.TestConexion(testConexion);

********************
**** Mi fabrica ****
********************
public class SocketFactory implements SecureProtocolSocketFactory {

public SocketFactory() {

}

public Socket createSocket(Socket arg0, String arg1, int arg2, boolean arg3)
throws IOException, UnknownHostException {
// TODO Auto-generated method stub
return null;
}

public Socket createSocket(String host, int port) throws IOException,
UnknownHostException {
// Generacion de logs
Logger logger = CCLogger.getLogger(this.getClass());
//
SSLContext ssl = null;

try {
File fich = new File("/aplicaciones/escuchadores/edoc-server-2.0/certificados/ClienteRbmPrueba.p12");
byte[] bBuf = null;

if (fich.exists()) {
FileInputStream fis = new FileInputStream(fich);
bBuf = new byte[(int) fich.length()];
fis.read(bBuf);
fis.close();
}

// Lectura de los archivos PKCS12 como un KeyStore
String password = "Jorobado1";
KeyStore ks = KeyStore.getInstance("PKCS12");
InputStream stream = new ByteArrayInputStream(bBuf);
ks.load(stream, password.toCharArray());
stream.close();

// Ya que tenemos el KeyStore, necesitamos crear una KeyManagerFactory con el mismo
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());

// ya que tenemos el KeyManagerFactory, debemos crear un SSLContext que es con el que vamos a crear
// los sockets que necesita el web service para enviar y recibir datos
ssl = SSLContext.getInstance("SSL");
ssl.init(kmf.getKeyManagers(), null, null);
} catch (Exception e) {
logger.error("Error",e);
throw new IOException(e.getMessage());
}

return ssl.getSocketFactory().createSocket(host, port);
}

public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
throws IOException, UnknownHostException {
return null;
}

public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3, HttpConnectionParams arg4)
throws IOException, UnknownHostException, ConnectTimeoutException {
// TODO Auto-generated method stub
return null;
}

}

Ssl cliente axis2 adb plugin maven eclipse

Hola,
Antes de nada enhorabuena por el blog.
Soy nuevo en este mundo de web services con axis2. He creado un cliente axis2 adb con el pluging maven de eclipse y necesito comunicar via ssl y verificar los siguientes puntos del certificado del servidor:
Caducidad
Verificar si el certificado esta revocado
Y el cn del certificado.

¿Podrias ayudarme con este asunto?
Muchas gracias