Conexiones HTTPS con certificado de cliente desde una aplicación Java

En Java el establecimiento de una conexión segura con certificado de cliente puede realizarse de varias maneras. La manera más sencilla y que nos permite hacer pruebas rápidas es modificando el keystore por defecto estableciendo las variables correspondientes en el arranque de la máquina virtual.

En caso de que el servidor tuviera un certificado autofirmado o que el certificado estuviera firmado por una entidad certificadora no incluida en el almacén de claves que viene con la máquina virtual, tendríamos que  modificar también el truststore por defecto de la conexión, ya que si no lo hacemos, el programa fallará con un javax.net.ssl.SSLPeerUnverifiedException.

Como ejemplo utilizaremos la siguiente clase que realiza una conexión HTTPS y muestra por consola algunos detalles del certificado del servidor al que nos estamos conectando y después imprime la respuesta:

import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import java.net.MalformedURLException;
import java.security.cert.Certificate;
import java.util.logging.Logger;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.net.ssl.SSLPeerUnverifiedException;
import java.io.IOException;

public class HTTPSClient {

    private static String HTTPS_URL = "https://www.google.com";

    private final static Logger log = Logger.getLogger(HTTPSClient.class
            .getName());

    public static void main(String[] args) {
        new HTTPSClient().connect();
    }

    private void connect() {

        try {
            URL url = new URL(HTTPS_URL);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.connect();
            Certificate[] certs = conn.getServerCertificates();

            if (conn != null) {
                for (Certificate cert : certs) {
                    log.info("Cert Type: " + cert.getType());
                    log.info("Cert Hash Code: " + cert.hashCode());
                    log.info("Cert Algorithm: "
                            + cert.getPublicKey().getAlgorithm());
                    log.info("Cert Format: " + cert.getPublicKey().getFormat());
                }
            }

            BufferedReader br = new BufferedReader(new InputStreamReader(
                    conn.getInputStream()));
            String line;

            while ((line = br.readLine()) != null) {
                log.info(line);
            }

            br.close();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (SSLPeerUnverifiedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Compilamos el código:

mmartin@debian:~$ javac HTTPSClient.java

Como hemos mencionado anteriormente, para realizar una conexión utilizando nuestro certificado personal, ejecutaríamos lo siguiente.

mmartin@debian:~$ java -Djavax.net.ssl.keyStore=/home/mmartin/certs/mmartin.pfx 
-Djavax.net.ssl.keyStoreType=PKCS12 
-Djavax.net.ssl.keyStorePassword=mypassword 
HTTPSClient

Si la CA que firma no está en el truststore que viene por defecto o el certificado de servidor es autofirmado tendremos que utilizar un truststore que contenga dicha certificado de CA o el certificado autofirmado del propio servidor.

Para importar en nuestro truststore cualquiera de los certificados mencionados anteriormente utilizaríamos la herramienta keytool que viene con la máquina virtual:

mmartin@debian:~$ keytool -importcert -file /home/mmartin/certs/myserver.pem 
-keystore /home/mmartin/certs/mycacerts 
-storepass changeit -alias myserver

Con lo que el comando de ejecución utilizando nuestro nuevo truststore sería:

mmartin@debian:~$ java -Djavax.net.ssl.keyStore=/home/mmartin/certs/mmartin.pfx 
-Djavax.net.ssl.keyStoreType=PKCS12 
-Djavax.net.ssl.keyStorePassword=mypassword 
-Djavax.net.ssl.trustStore=/home/mmartin/certs/mycacerts 
-Djavax.net.ssl.trustStorePassword=changeit 
-Djavax.net.ssl.trustStoreType=JKS 
HTTPSClient

La aproximación anterior está bien para hacer pruebas rápidas pero tiene el inconveniente de que todas las aplicaciones que se ejecuten sobre esa máquina virtual utilizarán esos mismos almacenes de certificados, y nos puede interesar que una aplicación o un módulo concreto utilice otro certificado distinto.

Un ejemplo de cómo personalizar tanto nuestro certificado de cliente como el almacén que aloja los certificados de CA en los que confiamos, se muestra a continuación. Ni que decir tiene que, en una aplicación real, todas las variables privadas se establecerían en un fichero de configuración en lugar de hacerlo en el código.

import java.net.URL;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import java.net.MalformedURLException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.logging.Logger;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;

import javax.net.ssl.SSLPeerUnverifiedException;
import java.io.IOException;

public class HTTPSClient {

    private String HTTPS_URL = "https://www.google.com";
    private String KEY_STORE_FILE="/home/mmartin/certs/mmartin.pfx";
    private String KEY_STORE_PASS="mypassword";
    private String TRUST_STORE_FILE="/home/mmartin/certs/mycacerts";
    private String TRUST_STORE_PASS="changeit";

    //Documented in security guides (Under "Standard Names") on both Java 7 VMs
    /** Oracle VM valid values
     *  - SunX509
     *  IBM VM valid values
     *  - IbmX509
     */
    private String KEY_MANAGER_ALGORITHM = "SunX509";
    /** Oracle VM valid values
     *  - PKCS12, JKS
     *  IBM VM valid values
     *  - PKCS12, JKS, JCEKS
     */
    private String KEY_STORE_FORMAT = "PKCS12";
    private String TRUST_STORE_FORMAT = "JKS";

    /** Oracle VM valid values
     *  - PKIX
     *  IBM VM valid values
     *  - PKIX or IbmPKIX or IbmX509
     */
    private String TRUST_MANAGER_ALGORITHM="PKIX";

    /** Oracle VM valid values
     *  - SSL SSLv2 SSLv3 TLS TLSv1 TLSv1.1 TLSv1.2
     *  IBM VM valid values
     *  - SSL SSLv3 TLS_SSL TLS TLSv1
     */
    // Oracle VM
    private String SSL_CONTEXT_ALGORITHM = "TLS";

    private final static Logger logger = Logger.getLogger(HTTPSClient.class.getName());

    public static void main(String[] args) {
        new HTTPSClient().connect();
    }

    private void connect() {

        try {
            URL url = new URL(HTTPS_URL);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setSSLSocketFactory(getFactory(new File(KEY_STORE_FILE), KEY_STORE_PASS, new File(TRUST_STORE_FILE), TRUST_STORE_PASS));
            conn.connect();
            Certificate[] certs = conn.getServerCertificates();

            if (conn != null) {
                for (Certificate cert : certs) {
                    logger.info("Cert Type: " + cert.getType());
                    logger.info("Cert Hash Code: " + cert.hashCode());
                    logger.info("Cert Algorithm: " + cert.getPublicKey().getAlgorithm());
                    logger.info("Cert Format: " + cert.getPublicKey().getFormat());
                }
            }

            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;

            while ((line=br.readLine()) != null) {
                logger.info (line);
            }

            br.close();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (SSLPeerUnverifiedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private SSLSocketFactory getFactory(File pKeyFile, String pKeyPassword, File pTrustStoreFile, String pTrustStorePassword) {

        SSLSocketFactory socketFactory = null;

        try {

            KeyManagerFactory keyManagerFactory;
            keyManagerFactory = KeyManagerFactory.getInstance(KEY_MANAGER_ALGORITHM);
            KeyStore keyStore;
            keyStore = KeyStore.getInstance(KEY_STORE_FORMAT);
            InputStream keyInput = new FileInputStream(pKeyFile);
            keyStore.load(keyInput, pKeyPassword.toCharArray());
            keyInput.close();
            keyManagerFactory.init(keyStore, pKeyPassword.toCharArray());

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TRUST_MANAGER_ALGORITHM);
            KeyStore trustStore;
            trustStore = KeyStore.getInstance(TRUST_STORE_FORMAT);
            InputStream trustStoreInput = new FileInputStream(pTrustStoreFile);
            trustStore.load(trustStoreInput, pTrustStorePassword.toCharArray());
            trustStoreInput.close();
            trustManagerFactory.init(trustStore);

            SSLContext context = SSLContext.getInstance(SSL_CONTEXT_ALGORITHM);
            context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
            socketFactory=context.getSocketFactory();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return socketFactory;
    }

}