Seguridad con HTTPS y SSL

La capa de conexión segura (SSL), ahora conocida técnicamente como seguridad de la capa de transporte (TLS), es un componente fundamental para las comunicaciones encriptadas entre clientes y servidores. Una aplicación podría usar SSL de forma incorrecta, con lo cual los datos de una app podrían ser interceptados por entidades maliciosas a través de la red. Para ayudarte a garantizar que esto no le ocurra a tu app, en este artículo, se destacan los inconvenientes comunes que pueden presentarse cuando se usan protocolos de red seguros; además, se abordan algunas inquietudes más amplias sobre el uso de la infraestructura de clave pública (PKI).

También te recomendamos leer los artículos Descripción general de la seguridad de Android y Descripción general de permisos.

Conceptos

En un caso de uso de SSL típico, el servidor se configura con un certificado que contiene una clave pública y una privada coincidente. Como parte del protocolo de enlace entre un servidor y un cliente SSL, el servidor confirma que tiene la clave privada mediante la firma de su certificado con criptografía de clave pública.

No obstante, cualquiera puede generar claves privadas y certificados propios, de modo que un protocolo de enlace simple no comprueba respecto del servidor más que el hecho de que este último reconoce la clave privada que coincide con la clave pública del certificado. Una manera de resolver este problema es exigir que el cliente tenga un conjunto de uno o más certificados en los que confíe. Si el certificado no se encuentra en ese conjunto, no se confiará en el servidor.

Este enfoque simple tiene varias desventajas. Los servidores deberían poder actualizar a claves más seguras con el tiempo ("rotación de claves"), que reemplacen la clave pública en el certificado por una nueva. Lamentablemente, ahora se debe actualizar la app cliente debido a lo que en esencia representa un cambio de configuración del servidor, lo cual resulta problemático si el servidor no está bajo el control del desarrollador de la app; por ejemplo, si se trata de un servicio web de terceros. Este enfoque también presenta problemas si la app debe comunicarse con servidores arbitrarios, como un navegador web o una app de correo electrónico.

Para poder abordar estos inconvenientes, los servidores generalmente se configuran con certificados de emisores conocidos, llamados autoridades de certificación (CA). Por lo general, la plataforma host contiene una lista de CA conocidas en las que confía. A partir de la versión 4.2 (Jelly Bean), Android posee más de 100 CA que se actualizan en cada nueva versión. Al igual que un servidor, una CA contiene un certificado y una clave privada. Al emitir un certificado para un servidor, la CA firma el certificado del servidor mediante su clave privada. El cliente luego puede verificar que el servidor tenga un certificado emitido por una CA conocida por la plataforma.

No obstante, si bien se resuelven algunos problemas, el uso de CA presenta otro. Dado que la CA emite certificados para muchos servidores, debes asegurarte de alguna manera de que la comunicación se realice con el servidor que desees. Para abordar este problema, el certificado emitido por la CA identifica el servidor con un nombre específico, como gmail.com, o un conjunto de hosts de carácter comodín, como *.google.com.

En el siguiente ejemplo, se aclararán un poco más estos conceptos. En el siguiente fragmento de una línea de comandos, el comando s_client de la herramienta openssl examina la información del certificado del servidor de Wikipedia. Especifica el puerto 443 porque ese es el valor predeterminado para HTTPS. El comando envía el resultado de openssl s_client a openssl x509, que aplica formato a la información sobre los certificados conforme a la norma X.509. Específicamente, el comando solicita el tema (que contiene información del nombre del servidor) y el emisor (que identifica la CA).

    $ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer
    subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org
    issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA
    

Puedes ver que la CA RapidSSL emitió el certificado para servidores que coinciden con *.wikipedia.org.

Un ejemplo de HTTPS

Suponiendo que tienes un servidor web con un certificado emitido por una CA reconocida, puedes realizar una solicitud segura con código tan simple como el siguiente:

Kotlin

    val url = URL("https://wikipedia.org")
    val urlConnection: URLConnection = url.openConnection()
    val inputStream: InputStream = urlConnection.getInputStream()
    copyInputStreamToOutputStream(inputStream, System.out)
    

Java

    URL url = new URL("https://wikipedia.org");
    URLConnection urlConnection = url.openConnection();
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

Sí, puede ser así de fácil. Si deseas personalizar la solicitud HTTP, puedes realizar la transmisión a un elemento HttpURLConnection. En la documentación de Android para HttpURLConnection, encontrarás más ejemplos sobre cómo trabajar con encabezados de solicitud y respuesta, publicación de contenido, control de cookies, uso de proxies y obtención de respuestas, entre otros aspectos. Sin embargo, en términos de detalles para la verificación de certificados y nombres de hosts, el marco de trabajo de Android se ocupa del tema mediante estas API. Si es posible, debes apuntar a esto. Dicho esto, a continuación abordamos otras consideraciones.

Problemas comunes de la verificación de certificados del servidor

Supongamos que, en lugar de recibir el contenido de getInputStream(), se genera una excepción:

    javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
            at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
            at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
            at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
            at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
            at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
            at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
            at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
    

Puede producirse por diferentes motivos, entre los cuales se incluyen los siguientes:

  1. La CA que emitió el certificado del servidor no es conocida.
  2. El certificado del servidor no está firmado por una CA, sino que está autofirmado.
  3. Falta una CA intermedia en la configuración del servidor.

En las siguientes secciones, te explicamos la manera de abordar estos problemas sin perder la conexión segura con el servidor.

Autoridad certificada desconocida

En este caso, la excepción SSLHandshakeException se produce porque hay una CA en la cual el sistema no confía. Esto podría deberse a la presencia de un certificado de una CA nueva en la que Android aún no confía o a que tu app se usa en una versión anterior sin la CA. Con frecuencia, una CA es desconocida porque no es una CA pública, sino una privada que una entidad (como un Gobierno, una empresa o una institución educativa) emite para uso propio.

Afortunadamente, puedes indicar a HttpsURLConnection que confíe en un conjunto específico de CA. El procedimiento puede ser complejo. Por ello, a continuación, te mostramos un ejemplo que toma una CA específica desde un elemento InputStream y la usa para crear un elemento KeyStore, el cual luego se emplea para crear e inicializar un elemento TrustManager. Un elemento TrustManager es lo que usa el sistema para validar certificados del servidor y, al crear uno desde un elemento KeyStore con una o más CA, las CA en cuestión serán las únicas en las que confiará ese TrustManager.

Dado el nuevo TrustManager, el ejemplo inicializa un nuevo SSLContext que proporciona un elemento SSLSocketFactory que puedes usar para anular el elemento SSLSocketFactory predeterminado de HttpsURLConnection. De esta manera, la conexión usará tus CA para validar certificados.

A continuación, te mostramos un ejemplo completo del uso de una CA institucional de la Universidad de Washington:

Kotlin

    // Load CAs from an InputStream
    // (could be from a resource or ByteArrayInputStream or ...)
    val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
    // From https://www.washington.edu/itconnect/security/ca/load-der.crt
    val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
    val ca: X509Certificate = caInput.use {
        cf.generateCertificate(it) as X509Certificate
    }
    System.out.println("ca=" + ca.subjectDN)

    // Create a KeyStore containing our trusted CAs
    val keyStoreType = KeyStore.getDefaultType()
    val keyStore = KeyStore.getInstance(keyStoreType).apply {
        load(null, null)
        setCertificateEntry("ca", ca)
    }

    // Create a TrustManager that trusts the CAs inputStream our KeyStore
    val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
        init(keyStore)
    }

    // Create an SSLContext that uses our TrustManager
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    val url = URL("https://certs.cac.washington.edu/CAtest/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.sslSocketFactory = context.socketFactory
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

Java

    // Load CAs from an InputStream
    // (could be from a resource or ByteArrayInputStream or ...)
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    // From https://www.washington.edu/itconnect/security/ca/load-der.crt
    InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
    Certificate ca;
    try {
        ca = cf.generateCertificate(caInput);
        System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
    } finally {
        caInput.close();
    }

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);

    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    URL url = new URL("https://certs.cac.washington.edu/CAtest/");
    HttpsURLConnection urlConnection =
        (HttpsURLConnection)url.openConnection();
    urlConnection.setSSLSocketFactory(context.getSocketFactory());
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

Con un TrustManager personalizado que reconozca tus CA, el sistema puede validar la confiabilidad del emisor de tu certificado de servidor.

Precaución: Muchos sitios web describen una solución alternativa deficiente que consiste en instalar un TrustManager que no hace nada. Si recurres a esa solución, es posible que omitas la encriptación de tu comunicación y que cualquier intruso pueda atacar a tus usuarios en un hotspot de Wi-Fi público mediante trucos de DNS para enviar el tráfico de tus usuarios por un proxy ajeno que se haga pasar por tu servidor. Luego, el atacante puede registrar contraseñas y otros datos personales. Esto funciona porque el atacante puede generar un certificado; sin un TrustManager que valide la confiabilidad de la fuente de la que proviene ese certificado, tu app podría establecer comunicaciones de manera indiscriminada. Por lo tanto, ni siquiera debes hacer esto a modo de solución temporal. Siempre puedes hacer que tu app confíe en el emisor del certificado del servidor. Te recomendamos que lo hagas.

Certificado del servidor autofirmado

El segundo caso de SSLHandshakeException se produce como consecuencia de un certificado autofirmado, lo cual significa que el servidor se comporta como su propia CA. Esto es similar a una autoridad de certificación desconocida, por lo cual puedes usar el mismo enfoque de la sección anterior.

Puedes crear tu propio TrustManager; esta vez confiando directamente en el certificado del servidor. Esto presenta los inconvenientes ya analizados respecto de la vinculación directa de tu app con un certificado, pero se puede hacer de forma segura. No obstante, debes asegurarte de que tu certificado autofirmado tenga una clave suficientemente segura. A partir de 2012, se acepta una firma RSA de 2,048 bits con un exponente de 65,537 de caducidad anual. Al rotar claves, debes comprobar la presencia de recomendaciones de una autoridad (como NIST) respecto de lo que es aceptable.

Falta de autoridad de certificación intermedia

El tercer caso de SSLHandshakeException se produce como consecuencia de la falta de una CA intermedia. La mayoría de las CA públicas no firman certificados del servidor de manera directa. En su lugar, usan su certificado de CA principal, al que se hace referencia como CA raíz, para firmar CA intermedias. Hacen esto para que la CA raíz pueda almacenarse sin conexión a fin de reducir el riesgo de compromiso. No obstante, los sistemas operativos como Android generalmente confían solo en CA raíz de forma directa, lo cual genera una pequeña brecha entre el certificado del servidor (firmado por la CA intermedia) y el verificador de certificados, que reconoce la CA raíz. Para resolver esto, el servidor no envía al cliente solo su certificado durante el acuerdo SSL, sino una cadena de certificados de la CA del servidor a través de los intermediarios que sean necesarios para lograr una CA raíz de confianza.

Para ver cómo funciona en la práctica, a continuación, te mostramos una cadena de certificados mail.google.com tal como se ve en el comando openssls_client:

    $ openssl s_client -connect mail.google.com:443
    ---
    Certificate chain
     0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=mail.google.com
       i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
     1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
       i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
    ---
    

Esto muestra que el servidor envía un certificado para mail.google.com emitido por la CA Thawte SGC, que es una CA intermedia, y un segundo certificado para la CA Thawte SGC emitido por una CA Verisign, que es la CA principal en la que Android confía.

Sin embargo, no es común configurar un servidor para que no incluya las CA intermedias necesarias. Por ejemplo, aquí hay un servidor que puede ocasionar un error en los navegadores de Android y excepciones en apps para Android.

    $ openssl s_client -connect egov.uscis.gov:443
    ---
    Certificate chain
     0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
       i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
    ---
    

Es interesante tener en cuenta que, al visitar este servidor en la mayoría de los navegadores de escritorio, no se produce un error como el que provocaría una CA completamente desconocida o un certificado del servidor autofirmado. Esto se debe a que la mayoría de las cachés de los navegadores de escritorio confiaron en las CA intermedias con el paso del tiempo. Una vez que el navegador visite y asimile la CA intermedia de un sitio, la próxima vez que lo visite no necesitará tener una CA intermedia en la cadena de certificados.

Algunos sitios hacen esto de manera intencional con servidores web secundarios empleados para proporcionar recursos. Por ejemplo, su página HTML principal podría provenir de un servidor con una cadena de certificados completa, pero los servidores que se usan para recursos (como imágenes, CSS o JavaScript) podrían no incluir la CA, presuntamente para ahorrar ancho de banda. Lamentablemente, a veces, es posible que estos servidores proporcionen un servicio web al que intentas llamar desde tu app de Android, lo cual no proporciona tanta flexibilidad.

Los siguientes dos enfoques pueden solucionar este problema:

  • Configurar el servidor para que incluya CA intermedias en la cadena de servidores. La mayoría de las CA proporcionan documentación sobre cómo hacer esto para todos los servidores web más comunes. Este es el único enfoque que debes usar si necesitas que el sitio funcione con navegadores Android predeterminados, al menos hasta Android 4.2.
  • Como alternativa, puedes tratar la CA intermedia como a cualquier otra CA desconocida y crear un elemento TrustManager para que confíe en ella directamente, como se hizo en las dos secciones anteriores.

Problemas comunes de la verificación del nombre del host

Como mencionamos al comienzo de este artículo, la verificación de una conexión SSL consta de dos partes clave. La primera implica verificar si el certificado proviene de una fuente de confianza, que es el tema central de la sección anterior. La segunda parte representa el tema central de esta sección: asegurarte de que el servidor con el cual te comunicas presente el certificado correcto. Cuando esto no suceda, generalmente, verás un error como el siguiente:

    java.io.IOException: Hostname 'example.com' was not verified
            at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446)
            at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
            at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
            at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
            at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
            at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
    

Uno de los motivos por los cuales puede ocurrir esto es la existencia de un error de configuración del servidor. El servidor está configurado con un certificado que no tiene campos de nombre de tema ni de nombre de tema alternativo que coincidan con el servidor con el cual intentas comunicarte. Es posible usar un solo certificado con muchos servidores diferentes. Por ejemplo, si observas el certificado google.com con openssl s_client -connect google.com:443 | openssl x509 -text, podrás ver un tema que admite *.google.com y también nombres de temas alternativos para *.youtube.com, *.android.com y otros. El error se produce únicamente cuando el nombre del servidor al cual te conectas no se encuentra entre los elementos enumerados como aceptables en el certificado.

Lamentablemente, esto también puede deberse a otro factor: el hosting virtual. Al compartir un servidor para más de un nombre de host con HTTP, el servidor web puede detectar a partir de la solicitud HTTP/1.1 el nombre de host de destino que busca el cliente. Lamentablemente, esto resulta complicado en el caso del protocolo HTTPS, ya que el servidor debe reconocer el certificado que mostrará antes de ver la solicitud HTTP. Para abordar este problema, las versiones más nuevas de SSL, específicamente TLSv.1.0 y posteriores, admiten la indicación de nombre de servidor (SNI), que permite al cliente SSL especificar el nombre de host previsto para el servidor de modo que se pueda mostrar el certificado correspondiente.

Por suerte, HttpsURLConnection admite SNI a partir de Android 2.3. Si necesitas compatibilidad con Android 2.2 (y versiones anteriores), una solución es configurar un host virtual alternativo en un puerto único de manera que no resulte ambiguo qué certificado de servidor se debe mostrar.

La alternativa más drástica implica reemplazar el elemento HostnameVerifier por uno que no use el nombre de host de tu host virtual, sino el que muestre el servidor de forma predeterminada.

Precaución: Reemplazar HostnameVerifier puede ser muy peligroso si no tienes bajo control el otro host virtual, ya que un ataque de intermediarios podría dirigir tráfico a otro servidor sin que lo sepas.

Si estás seguro de que deseas anular la verificación del nombre del host, a continuación, te mostramos un ejemplo en el cual se reemplaza el verificador de un solo elemento URLConnection por uno que verifica que la app al menos prevea ese nombre de host:

Kotlin

    // Create an HostnameVerifier that hardwires the expected hostname.
    // Note that is different than the URL's hostname:
    // example.com versus example.org
    val hostnameVerifier = HostnameVerifier { _, session ->
        HttpsURLConnection.getDefaultHostnameVerifier().run {
            verify("example.com", session)
        }
    }

    // Tell the URLConnection to use our HostnameVerifier
    val url = URL("https://example.org/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.hostnameVerifier = hostnameVerifier
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

Java

    // Create an HostnameVerifier that hardwires the expected hostname.
    // Note that is different than the URL's hostname:
    // example.com versus example.org
    HostnameVerifier hostnameVerifier = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            HostnameVerifier hv =
                HttpsURLConnection.getDefaultHostnameVerifier();
            return hv.verify("example.com", session);
        }
    };

    // Tell the URLConnection to use our HostnameVerifier
    URL url = new URL("https://example.org/");
    HttpsURLConnection urlConnection =
        (HttpsURLConnection)url.openConnection();
    urlConnection.setHostnameVerifier(hostnameVerifier);
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

Sin embargo, debes recordar que, si reemplazas la verificación del nombre del host, en especial por motivos relacionados con el hosting virtual, el riesgo seguirá siendo muy alto si no tienes bajo control el otro host virtual; por ello, debes buscar una opción de hosting alternativa a fin de evitar este problema.

Advertencias sobre el uso directo de SSLSocket

Hasta ahora, los ejemplos se centraron en el uso de HttpsURLConnection por parte de HTTPS. Algunas veces, las apps necesitan usar SSL independientemente del protocolo HTTP. Por ejemplo, una app de correo electrónico puede usar variantes SSL de SMTP, POP3 o IMAP. En esos casos, la app podría usar SSLSocket directamente, casi de la misma manera en que HttpsURLConnection lo hace internamente.

Las técnicas que describimos hasta ahora para abordar los problemas de verificación de certificados también se aplican a SSLSocket. De hecho, al usar un TrustManager personalizado, lo que se pasa a HttpsURLConnection es un SSLSocketFactory. Por lo tanto, si necesitas usar un TrustManager personalizado con un SSLSocket, sigue los mismos pasos y usa ese SSLSocketFactory para crear tu SSLSocket.

Precaución: SSLSocket no realiza la verificación del nombre de host. Tu app realiza su propia verificación de nombre de host, preferiblemente llamando a getDefaultHostnameVerifier() con el nombre de host esperado. Recuerda también que HostnameVerifier.verify() no genera una excepción para el error; en cambio, muestra un resultado booleano que debes comprobar explícitamente.

A continuación, te mostramos un ejemplo en el que podrás ver la manera de hacer esto. Muestra que, cuando te conectes al puerto 443 de gmail.com sin compatibilidad con SNI, recibirás un certificado de mail.google.com. Esto es previsible en este caso, por lo que debes realizar una comprobación para asegurarte de que el certificado sea para mail.google.com:

Kotlin

    // Open SSLSocket directly to gmail.com
    val socket: SSLSocket = SSLSocketFactory.getDefault().run {
        createSocket("gmail.com", 443) as SSLSocket
    }
    val session = socket.session

    // Verify that the certicate hostname is for mail.google.com
    // This is due to lack of SNI support in the current SSLSocket.
    HttpsURLConnection.getDefaultHostnameVerifier().run {
        if (!verify("mail.google.com", session)) {
            throw SSLHandshakeException("Expected mail.google.com, found ${session.peerPrincipal} ")
        }
    }

    // At this point SSLSocket performed certificate verification and
    // we have performed hostname verification, so it is safe to proceed.

    // ... use socket ...
    socket.close()
    

Java

    // Open SSLSocket directly to gmail.com
    SocketFactory sf = SSLSocketFactory.getDefault();
    SSLSocket socket = (SSLSocket) sf.createSocket("gmail.com", 443);
    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
    SSLSession s = socket.getSession();

    // Verify that the certicate hostname is for mail.google.com
    // This is due to lack of SNI support in the current SSLSocket.
    if (!hv.verify("mail.google.com", s)) {
        throw new SSLHandshakeException("Expected mail.google.com, "
                                        "found " + s.getPeerPrincipal());
    }

    // At this point SSLSocket performed certificate verification and
    // we have performed hostname verification, so it is safe to proceed.

    // ... use socket ...
    socket.close();
    

Inclusión en lista negra

SSL depende en gran medida de las CA para emitir certificados solo a propietarios correctamente verificados de servidores y dominios. En situaciones poco frecuentes, las CA se someten a engaños o, en el caso de Comodo o DigiNotar, a infracciones, y se generan certificados de emisión de un nombre de host para alguien que no es el propietario del servidor o el dominio.

Para reducir este riesgo, Android tiene la capacidad de incluir en una lista negra algunos certificados o, incluso, todas las CA. Si bien esta lista históricamente se compilaba en el sistema operativo, a partir de Android 4.2 se puede actualizar de forma remota para abordar compromisos futuros.

Fijación

Mediante una técnica conocida como fijación, una app puede contar con una protección aún mayor contra certificados emitidos de forma fraudulenta. Básicamente, se aplica el mismo ejemplo proporcionado en el caso anterior sobre CA desconocidas para limitar las CA de confianza de la app a un conjunto pequeño que usarán los servidores de esta. Esto evita el compromiso de una de las más de 100 CA presentes en el sistema a fin de que no ocurra una violación del canal seguro de la app.

Certificados de cliente

Este artículo se centró en el usuario de SSL para proteger las comunicaciones con los servidores. SSL también admite la noción de certificados de cliente que permiten al servidor validar la identidad de un cliente. Si bien están fuera del alcance de este artículo, las técnicas empleadas son similares a la especificación de un TrustManager personalizado. Consulta la discusión sobre cómo crear un KeyManager personalizado en la documentación para HttpsURLConnection.

Nogotofail: una herramienta para probar la seguridad del tráfico de red

Nogotofail es una herramienta que te permite confirmar fácilmente que tus apps estén protegidas frente a vulnerabilidades y configuraciones incorrectas conocidas de TLS y SSL. Se trata de una herramienta automatizada, poderosa y escalable para comprobar la presencia de problemas de seguridad de la red en cualquier dispositivo cuyo tráfico de red pueda verse afectado.

Nogotofail se aplica a los siguientes tres casos prácticos principales:

  • Detección de errores y vulnerabilidades
  • Verificación de soluciones y búsqueda de regresiones
  • Comprensión de las aplicaciones y los dispositivos que generan tráfico

Nogotofail es compatible con Android, iOS, Linux, Windows, el Sistema operativo Chrome y OSX; de hecho, admite cualquier dispositivo que uses para conectarte a Internet. Hay un cliente fácil de usar para configurar los ajustes y obtener notificaciones en Android y Linux, como también el motor de ataque que se puede implementar como router, servidor de VPN o proxy.

Puedes acceder a la herramienta en el Proyecto de código abierto de Nogotofail.