Skip to content

Most visited

Recently visited

navigation

Seguridad con HTTPS y SSL

La capa de sockets seguros (SSL), ahora conocida 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 destacamos los inconvenientes comunes que pueden presentarse al usar protocolos de red seguros, y también abordamos algunas inquietudes más amplias sobre el uso de la Infraestructura de claves públicas (PKI).

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 acuerdo entre un servidor y un cliente SSL, el servidor confirma que tiene la clave privada firmando su certificado con criptografía de clave pública.

No obstante, cualquiera puede generar claves privadas y certificados propios, de modo que un acuerdo 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 grupo, no se confiará en el servidor.

Este enfoque simple tiene varias desventajas. Los servidores deben poder actualizar a claves más seguras con el tiempo (“rotación de claves”), que reemplazan la clave pública en el certificado por una nueva. Lamentablemente, ahora la app cliente se debe actualizar debido a lo que básicamente representa un cambio de configuración del servidor. Esto resulta particularmente 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). La plataforma host generalmente 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 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 usando 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 esto, 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, ya que es el puerto 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:

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 una 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 framework de Android se ocupa de esto a través de estas API. A esto debes apuntar, si fuera posible. Dicho esto, a continuación abordamos otras consideraciones.

Problemas comunes de la verificación de certificados del servidor

Supón que, en lugar de recibirse 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)

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

  1. La CA que emite el certificado del servidor no es conocida.
  2. El certificado del servidor no está firmado por una CA; en lugar de esto, 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 de certificación desconocida

En este caso, la 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 bien 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 emite una entidad como un gobierno, una empresa o una institución educativa 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 InputStream y la usa para crear un KeyStore, que luego se emplea para crear e inicializar un TrustManager. Un TrustManager es lo que usa el sistema para validar certificados del servidor y, al crear uno desde un 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 SSLSocketFactory que puedes usar para anular el SSLSocketFactory predeterminado de la 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:

// 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.

Advertencia: En muchos sitios web se describe una solución alternativa insuficiente que consiste en instalar un TrustManager que no hace nada. Si recurres a esta, podrías omitir la encriptación de tu comunicación y cualquier intruso podría atacar a tus usuarios en un hotspot Wi-Fi público usando trucos de DNS para enviarles tráfico a través de un proxy propio que simule ser 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. Simplemente, hazlo.

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 2048 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 como funciona esto en la práctica, a continuación te mostramos una cadena de certificados mail.google.com tal como se ve en el comando openssl s_client:

$ openssl s_client -connect mail.google.com:443
---
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/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.

No obstante, es común configurar un servidor para que no incluya las CA intermedias necesarias. A continuación, te mostramos un ejemplo de un servidor que puede provocar un error en los navegadores Android y excepciones en las apps 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ían una CA completamente desconocida o un certificado del servidor sin firmar. Esto se debe a que la mayoría de los cachés de los servidores 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 intencionalmente 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, algunas veces estos servidores podrían proporcionar un servicio web al que intentas llamar desde tu app de Android, lo cual no proporciona tanta flexibilidad.

Existen dos enfoques para solucionar este problema:

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 claves. 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 el cliente busca. Desafortunadamente, 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 servidor, de modo que se pueda mostrar el certificado correspondiente.

Afortunadamente, HttpsURLConnection admite SNI a partir de Android 2.3. Una solución temporal, si necesitas compatibilidad con Android 2.2 (y versiones anteriores), consiste en configurar un host virtual alternativo en un puerto único para evitar ambigüedades respecto del certificado de servidor que se mostrará.

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

Advertencia: Reemplazar HostnameVerifier puede resultar muy riesgoso 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 una sola URLConnection por uno que verifica que la app al menos prevea ese nombre de host:

// 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, y 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.

Advertencia: SSLSocket no realiza la verificación del nombre de host. La app debe realizar su propia verificación del nombre de host, preferentemente llamando a getDefaultHostnameVerifier() con el nombre de host previsto. Recuerda también que HostnameVerifier.verify() no genera una excepción para el error; en lugar de esto, 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. Cuando te conectes al puerto 443 de gmail.com sin compatibilidad con SNI, recibirás un certificado para 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:

// 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 verificaiton 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 de prueba de 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 pudiera verse afectado.

Nogotofail se aplica para tres casos de uso principales:

Nogotofail es compatible con Android, iOS, Linux, Windows, Chrome OS 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.

This site uses cookies to store your preferences for site-specific language and display options.

Get the latest Android developer news and tips that will help you find success on Google Play.

* Required Fields

Hooray!

Follow Google Developers on WeChat

Browse this site in ?

You requested a page in , but your language preference for this site is .

Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a short survey?
Help us improve the Android developer experience.
(Sep 2017 survey)