TLS Pinning con Java y Spring

1
4777

En este tutorial vamos a ver qué es el TLS Pinning y cómo se puede implementar en Java mediante Spring.

Índice de contenidos

1. Introducción

Muchas veces, en las aplicaciones que desarrollamos, es necesario que nos comuniquemos con un servicio de terceros para realizar ciertas operaciones. Este tipo de comunicación podemos realizarla, típicamente, a través de servicios web securizados mediante https. Este tipo de comunicación, aunque segura ya que la comunicación se encuentra cifrada, no nos asegura que el servidor con el que nos estamos comunicando sea el que debe ser (pueden existir ataques en el que redirijan nuestra petición a otro servidor). TLS Pinning es un mecanismo mediante el cual lucharemos contra este tipo de ataques.

TLS Pinning, o también llamado Certificate Pinning, es un mecanismo de seguridad que permite autenticar al servidor con el que nos estamos comunicando. De esta forma, sabremos que el servidor con el que nos estamos comunicando es el servidor con el que nos queremos comunicar.

Normalmente, cuando se establece la comunicación con algún servidor, y ésta se realiza mediante SSL, se comprueba si el certificado del servidor es un certificado de confianza. Pero, ¿qué es un certificado de confianza?, un certificado de confianza es un certificado en el cual nosotros (como lado cliente) confiamos o un certificado que está firmado por una autoridad certificadora (CA) en la que nosotros también confiamos. Una autoridad certificadora (CA) es una entidad de confianza que emite certificados digitales. Se puede, por tanto, establecer que, si confiamos en la autoridad certificadora, confiaremos en todos los certificados que hayan sido emitidos por dicha entidad. Así, simplemente admitiendo el certificado de la autoridad certificadora admitiremos todos los certificados de cada uno de los servidores cuyo certificado haya sido emitido por dicha autoridad certificadora sin necesidad de confiar en todos los certificados uno a uno.

El objetivo de TLS Pinning es no confiar en las entidades certificadoras sino únicamente en el certificado propio del servidor. De esta forma, podremos saber que, efectivamente, estamos estableciendo la conexión con el servidor correcto en lugar de con otro servidor (a no ser que le hayan robado el certificado al servidor).

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 X El Capitan 10.10
  • Entorno de desarrollo: Eclipse Mars
  • Java 7
  • Spring 3.2.2

3. Implementando TLS Pinning

3.1. Generando el almacén de claves de confianza

Lo primero que deberemos hacer es obtener el certificado público del servidor con el que queremos comunicarnos de forma segura (nos lo deberán proporcionar). Una vez obtenido, tendremos que crear un almacén de certificados de confianza (conocido en Java como trustStore) que contenga únicamente dicho certificado. Normalmente, el almacén de certificados de confianza, por defecto, tiene una serie de certificados de las autoridades certificadoras más comunes. Como nosotros lo que queremos hacer es confiar única y exclusivamente en un único certificado (en el del servidor al que nos conectemos) tendremos que dejar este almacén con sólo dicho certificado.

Para gestionar todo el tema de almacenes, tanto keyStore como trustStore, emplearemos la herramienta keytool. En este caso, para generar un almacén de claves de confianza a partir de un certificado emplearemos el siguiente comando (importante no incluir { ni } en la ejecución del comando):

keytool -import -file {serverCertificate.cer} -alias {name} -keystore {trustStoreName}

Donde:

  • serverCertificate.cer: es el certificado público del servidor al cual nos queremos conectar.
  • name: es el nombre que le queremos dar a la entrada de ese certificado dentro de nuestro almacén de certificados de confianza.
  • trustStoreName: nombre del almacén de certificados de confianza al que añadiremos la nueva entrada. En caso de no existir se creará uno nuevo. Durante el proceso de creación se nos pedirá introducir una contraseña para el almacén de claves.

3.2. Generando nuestro PinningSSLSocketFactory

Como la comunicación que vamos a establecer con el servidor es mediante RestTemplate, tenemos que saber que RestTemplate por debajo llama a una factoría de creación de sockets SSL para la comunicación vía https. Por defecto, se usa una factoría que obtiene el almacén de certificados de confianza por defecto. Nosotros no queremos ese comportamiento, nosotros lo que queremos es usar una factoría que use el almacén de claves que hemos creado en el paso anterior. Para ello, extenderemos la funcionalidad de SSLSocketFactory para indicarle una ruta donde encontrar el almacén de certificados de confianza que tiene que utilizar y la contraseña para acceder a dicho almacén.

public class PinningSSLSocketFactory extends SSLSocketFactory {

    private static final Logger LOGGER = LoggerFactory.getLogger(PinningSSLSocketFactory.class);

    public PinningSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException,
            KeyStoreException, UnrecoverableKeyException {
        super(truststore);
    }

    public static SSLSocketFactory getSocketFactory(String trustStorePath, String trustStorePassword) {
        try {
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            final FileInputStream trustStoreFile = new FileInputStream(trustStorePath);
            final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());

            trustStore.load(trustStoreFile, trustStorePassword.toCharArray());
            trustStoreFile.close();

            tmf.init(trustStore);

            X509TrustManager trustManager = null;
            for (TrustManager tm : tmf.getTrustManagers()) {
                if (tm instanceof X509TrustManager) {
                    trustManager = (X509TrustManager)tm;
                    break;
                }
            }
            sslContext.init(null, new TrustManager[] { trustManager }, null);
            return new SSLSocketFactory(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
                | KeyManagementException e) {
            LOGGER.error("Error creating SSLSocketFactory: {}", e);
        }

        return null;
    }
}

Con esto, tendremos ya nuestra factoría a la que le indicaremos una ruta y una contraseña y nos cargará los certificados de dicho almacén de certificados de confianza.

Si existiese alguna duda con este comportamiento se recomienda acceder a la implementación propia de SSLSocketFactory de Apache.

3.3. Generando nuestro PinningHttpComponentsClientHttpRequest

RestTemplate en lugar de comunicarse directamente con la factoría de creación de sockets, se comunica mediante una clase de componentes. Por lo tanto, necesitaremos extender también esta clase para que, en su creación, como factoría de creación de sockets SSL use la que hemos creado anteriormente.

public class PinningHttpComponentsClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {

    private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;

    private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5;

    private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);

    public PinningHttpComponentsClientHttpRequestFactory(String trustStorePath, String trustStorePassword) {
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
        schemeRegistry.register(new Scheme("https", 443, PinningSSLSocketFactory.getSocketFactory(trustStorePath,
                trustStorePassword)));

        PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(schemeRegistry);
        connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
        connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);

        this.setHttpClient(new DefaultHttpClient(connectionManager));
        setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS);
    }
}

3.4. Uniendo las piezas

Ya tenemos todas las piezas que necesitamos para realizar TLS Pinning, lo único que nos queda es unirlas para poder crear nuestro RestTemplate con esta funcionalidad. Esta configuración la realizaremos mediante fichero .xml:

<bean name="pinningHttpComponentClientHttpRequestFactory" class="com.autentia.example.PinningHttpComponentsClientHttpRequestFactory">
    <constructor-arg value="${trustStorePath}" />
    <constructor-arg value="${trustStorePassword}" />
</bean>

<bean id="pinningRestTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg ref="pinningHttpComponentClientHttpRequestFactory" />
</bean>

Como podemos observar, tanto la ruta al fichero de almacén de certificados de confianza como la contraseña las indicamos mediante propiedades.

3.5. Usando nuestro PinningRestTemplate

Para usar nuestro RestTemplate lo único que necesitaremos será instanciar nuestro pinningRestTemplate.

@Qualifier("pinningRestTemplate") RestTemplate restTemplate;

4. Conclusiones

Como hemos visto, de esta forma se puede implementar TLS Pinning en nuestras aplicaciones Java con Spring. Así, se conseguirá un plus más de seguridad al autenticar el servidor al que estamos llamando con nuestros servicios.

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad