TrustManager y certificados auto-firmados (Android)

TEORÍA

En Android tenemos una figura conocida como «TrustManager» que contiene un almacén (KeyStore) con las CA’s/claves públicas en las que debe confiar la aplicación a la hora de realizar una conexión segura (HTTPS). Podemos añadir CA’s de confianza que nosotros querámos al TrustManager desde código.

Para conseguir esto, se debe «incrustar» el certificado público no confiable del servidor al que nos vamos a conectar dentro de nuestro proyecto Android, para así poder trabajar con él, añadirlo al almacén de claves (KeyStore) e inicializar así el TrustManager. Esta forma de usar certificados se conoce como «pinned certificates» (certificados incrustados/fijados). Básicamente lo que harémos será copiar el certificado en la carpeta de recursos del proyecto («res/raw«) y luego desde código leerlo para trabajar con ello.

Normalmente el TrustManager se usará para generar Sockets de conexión segura a través de una fábrica de Sockets SSL (SSLSocketFactory), o bien se usará como un «repositorio» de certificados de confianza incrustados con los que comparar con el certificado recibido del servidor (aquél con el que querémos comunicarnos de forma segura).

Un ejemplo, imaginémos que usamos la biblioteca Volley de Android para realizar peticiones HTTPS, entonces el diagrama de funcionamiento sería algo muy similar a esto:

El TrustManager genera una fábrica de sockets SSL que usará Volley para crear una conexión segura (SSLSocket) con el servidor en cada petición HTTPS.

Para poder trabajar con el certificado desde el código y añadirlo al TrustManager hay dos técnicas principalmente:

1.- Transformar el certificado X.509 del servidor, al que nos queremos conectar, en un almacén de clave pública en formato BKS, un tipo de formato creado por un proveedor de APIs criptográficas para Java (y otros lenguajes) llamado «Bouncy Castle» y trabajar con ese formato de archivo.

2.- Trabajar directamente con el certificado PEM/DER (base64 ó binario) en formato x.509 que usa el servidor para la conexión segura.

NOTA: Es importante destacar que, ya que el TrustManager va a ser un repositorio para usar en más de una ocasión dentro de nuestra aplicación, este objeto debería implementar el patrón «singleton» (esto es, que existe una sola instancia de él en todo el código y que no debe ser destruído nunca).

PRÁCTICA

El certificado SSL incrustado que se va a leer desde el código, debe almacenarse en la carpeta «res/raw» de nuestro proyecto, que por defecto se encuentra en:

«C:\Users\<TU_USUARIO>\Documents\Android studio\projects\<NOMBRE_PROYECTO>\app\src\main\res\raw»

Ahora, pasamos a ver las dos posibles formas de añadir el certificado al TrustManager, esto es, trabajar con el certificado en formato BKS o directamente con el certificado en formato X.509:

MÉTODO 1: TRABAJAR CON BKS (Formato BouncyCastle)

He de decir que este método es el más conocido (o al menos es el primero que podemos encontrar en la documentación) y es el más absurdo de usar si podemos trabajar directamente con el certificado X.509, ya que la biblioteca criptográfica que se usa para generar el almacén de claves públicas BKS (del grupo BouncyCastle) es externa a Java: primero hay que conseguir una distribución para poder transformarlo y luego, en el código Android, pueden existir incompatibilidades a la hora de leer el archivo, pues en cada distribución de Android viene con una versión concreta de las bibliotecas BouncyCastle. La versión de BouncyCastle usada para generar el archivo BKS puede ser diferente de la versión que venga con la API de Android y por tanto provocar un error en la interpretación del archivo (ocurre, por ejemplo, al usar la distribución «bcprov-jdk15on-159.jar» del bouncy castle para generar el archivo e intentar leerlo en Android 4.1).

Se puede incluir el mismo .jar de BouncyCastle que hemos usado para generar el archivo BKS, dentro del proyecto Android, pero existen ciertas incompatibilidades también con esto, es por ello que se ha creado una biblioteca independiente a Java llamada SpongyCastle, que precisamente aglutina y maneja un conjunto de distribuciones de BouncyCastle para evitar imcompatibilidades… Pero no siempre cuenta con la última distribución de BouncyCastle (no actualiza a la misma velocidad que ellos) y para mayor complicación, dependerás de 2 bibliotecas externas (Bouncy y Spongy Castle). Todo esto parece una especie de «bomba de relojería» y solo hay que esperar a que algo falle en alguna actualización.

En cualquier caso, el método a seguir es el siguiente (tras generar el archivo BKS y haberlo almacenado en «res/raw«), la forma de añadir nuestro certificado al TrustManager sería la siguiente:

try {
  // Almacén de claves (KeyStore) en formato Bouncy Castle
  KeyStore trusted = KeyStore.getInstance("BKS");
  // Leer el certificado en BKS que hemos almacenado en res/raw
  InputStream in = <Context>.getResources()
    .openRawResource(R.raw.<NombreCertificadoBKS>);
  try {
    // Inicializar el almacén de claves con nuestro certificado
    // (debemos indicar la contraseña con la que generamos el archivo BKS)
    trusted.load(in, "contraseña_del_archivo_BKS".toCharArray());
  } finally {
    in.close();
  }

  // Se genera el TrustManager con el almacen de claves
  // "confiables"que hemos creado antes
  String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
  tmf.init(trusted);
} catch (Exception e) {
  throw new AssertionError(e);
}

MÉTODO 2: TRABAJAR CON EL CERTIFICADO X.509

En contraposición al primer método, este me parece el más acertado ya que no dependemos de bibliotecas externas.

En este caso, el método sería muy similar al anterior; la principal diferencia es que primero debemos generar el objeto «Certificate» que contiene, por así decirlo, el certificado X.509 en memoria, y luego incluirlo en el almacén de claves (KeyStore) que se usa en el TrustManager:

//source https://gist.github.com/erickok/7692592
try {
  // Leer el certificado X.509
  CertificateFactory cf = CertificateFactory.getInstance("X.509");
  InputStream is = <Context>.getResources()
    .openRawResource(R.raw.<CERTIFICADO_X.509>);
  InputStream caInput = new BufferedInputStream(is);
  Certificate ca;
  try {
    ca = cf.generateCertificate(caInput);
  } finally {
    caInput.close();
  }

  // Crear el almacén de claves (KeyStore) e insertar
  // la clave pública del certificado que acabamos de leer
  String keyStoreType = KeyStore.getDefaultType();
  KeyStore keyStore = KeyStore.getInstance(keyStoreType);
  keyStore.load(null, null);
  // Cada certificado se identifica con un string
  // (podemos añadir varios)
  keyStore.setCertificateEntry("ca", ca);

  // Crear el TrustManager que confia en las CA's incluidas
  // en el KeyStore creado.
  String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
  tmf.init(keyStore);
} catch (NoSuchAlgorithmException | KeyStoreException
  | KeyManagementException| CertificateException
  | IOException e)
{
  e.printStackTrace();
}

AÑADIR MÚLTIPLES CERTIFICADOS AL TRUSTMANAGER

La idea es sencilla, crear un objeto «Certificate» por cada uno de los certificados incrustados que tengamos en el proyecto y añadirlos al «KeyStore» que usará luego el «TrustManager«:

// Tras leer los certificados ca1, ca2, ... caN
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);

// Añadir los diferentes certificados que hemos leído
keyStore.setCertificateEntry("ca1", ca1);
keyStore.setCertificateEntry("ca2", ca2);
...
keyStore.setCertificateEntry("caN", caN);

// Crear el TrustManager con el KeyStore que contiene los certificados
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

Créditos de fuentes externas:

Iconos:

  • Server by Chanut is Industries from the Noun Project

Referencias:

Deja una respuesta