WebView y HTTPS con certificado auto-firmado (Android)

TEORÍA

La idea es utilizar la técnica de los «pinned certificates» (certificados fijados), esto es, dentro de nuestro proyecto Android tendrémos nuestro certificado con la clave pública del servidor con el que vayamos a crear una conexión segura HTTPS.

El objeto WebViewClient tiene un «event listener» que se ejecuta cuando se produce un error en la comunicación segura (por ejemplo, encontrarse con un certificado en el que no se confía) que se llama «onReceivedSslError«. La idea es sobreescribir este método y realizar un tratamiento del certificado recibido del servidor para saber si se confía en él comparándolo con nuestros certificados fijados.

Hay dos formas de hacerlo:

1.- Leer nuestro certificado fijado para tenerlo en memoria como un objeto «Certificate» y compararlo con el certificado del servidor.

2.- Crear un objeto TrustManager que confíe en nuestro certificado fijado y comprobar si el certificado recibido del servidor se encuentra entre los certificados confiables de nuestro TrustManager.

PRÁCTICA

MÉTODO 1: COMPARAR CON EL CERTIFICADO FIJADO

Tras haber copiado el certificado X.509 (En DER binario o base64 según la documentación de Java) en nuestra carpeta «res/raw» del proyecto Android, leemos el certificado para conseguir tenerlo en memoria como un objeto de tipo «Certificate«:

// LEER EL CERTIFICADO
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = getResources().openRawResource(R.raw.<TU_CERTIFICADO_X509>);
final Certificate my_certificate = cf.generateCertificate(caInput);
caInput.close();

En el event listener «onReceivedSslError» del objeto «WebViewClient«, obtendrémos el certificado recibido del servidor, el cual debemos transformar a objeto «Certificate» para poder compararlo con nuestro certificado fijado. Esta tarea la vamos a realizar en un método llamado «getX509Certificate()«:

// TRANSFORMAR EL CERTIFICADO RECIBIDO DEL SERVIDOR
// EN UN OBJETO DE TIPO "Certificate"
private Certificate getX509Certificate(SslCertificate sslCertificate){
  Bundle bundle = SslCertificate.saveState(sslCertificate);
  byte[] bytes = bundle.getByteArray("x509-certificate");
  if (bytes == null) {
    return null;
  } else {
    try {
      CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
      return certFactory.generateCertificate(new ByteArrayInputStream(bytes));
    } catch (CertificateException e) {
      return null;
    }
  }
}

Y ahora comparamos los dos certificados en el event listener «onReceivedSslError«:

webView.setWebViewClient(new WebViewClient() {
  ...
  @Override
  public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
    // Leer en memoria el certificado SSL recibido del servidor
    SslCertificate sslCertificate = error.getCertificate();
    Certificate cert = getX509Certificate(sslCertificate);
    if (cert != null && my_certificate != null) {
      try {
        //Comparar los certificados
        cert.verify(my_certificate.getPublicKey());
        //Si todo está OK continúa la lectura de la página, sino se lanza una excepción
        handler.proceed();
      } catch (CertificateException
          | NoSuchAlgorithmException
          | InvalidKeyException
          | NoSuchProviderException
          | SignatureException e)
      {
        handler.cancel();
        e.printStackTrace();
      }
    } else {
      // Si no se ha conseguido recuperar alguno de los certificados
      // se cancela la navegación
      handler.cancel();
    }
  }
  ...
});

MÉTODO 2: COMPROBAR SI TRUSTMANAGER CONFÍA EN EL CERTIFICADO DEL SERVIDOR

Si no sabes qué es el TrustManager de Java y cómo se relaciona este con nuestros certificados fijados, puedes leer ESTA entrada.

La idea de este método es que, tras haber incluido nuestro certificado fijado en nuestro TrustManager (el cual debe seguir el patrón singleton para ser reutilizado sin tener que volver a leer los certificados), comparemos el certificado recibido del servidor con cada uno de los certificados en los que confía nuestro TrustManager.

Entonces, tras haber preparado el TrustManager, en el event listener «onReceivedSslError» tendremos algo como esto:

webView.setWebViewClient(new WebViewClient() {
	...
	@Override
	public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
		boolean passVerify = false;
		// El error recogido es debido a un certificado no confiable
		if(error.getPrimaryError() == SslError.SSL_UNTRUSTED){
			SslCertificate cert = error.getCertificate();
			//String subjectDN = cert.getIssuedTo().getDName();
			try{
				// Obtener la parte del certificado correspondiente
				// al campo mX509Certificate
				Field f = cert.getClass().getDeclaredField("mX509Certificate");
				f.setAccessible(true);
				X509Certificate x509 = (X509Certificate)f.get(cert);

				X509Certificate[] chain = {x509};
				// Recorremos los TrustManagers de nuestro TrustManagerFactory
				for (TrustManager trustManager: AppSingleton.getTrustManagerFactory().getTrustManagers()) {
					if (trustManager instanceof X509TrustManager) {
						X509TrustManager x509TrustManager = (X509TrustManager)trustManager;
						try{
							// Comparar los dos certificados
							x509TrustManager.checkServerTrusted(chain, "generic");
							passVerify = true;
							break;
						}catch(Exception e){
							// La verificación ha fallado, se lanza una excepción
							passVerify = false;
						}
					}
				}
			}catch(Exception e){
				// Error al leer el certificado del servidor
				e.printStackTrace();
			}
		}
		if(passVerify == true)handler.proceed();
		else handler.cancel();
	}
	...
});

 

Deja una respuesta