wandres.dev
SEGURIDAD · Keystore y biometría

Seguridad de red: TLS, configuración declarativa y pinning

La red es la única frontera de tu aplicación donde el atacante no necesita el dispositivo en la mano. Esta lección recorre la defensa completa de esa frontera: qué garantiza `TLS` y qué sigue sin garantizar, cómo la plataforma retiró el texto en claro y dejó de confiar en las autoridades añadidas por el usuario, cómo se expresa una política de red en `network_security_config` en lugar de dispersarla por el código, cómo se fija una clave pública correctamente con copias de respaldo y caducidad, y por qué la fijación de certificados es la única medida de seguridad de esta guía capaz de dejar tu aplicación inservible sin que nadie te ataque.

⏱ 25 min

De todas las superficies de ataque de una aplicación móvil, la red es la que menos se parece a las demás, y la diferencia es de naturaleza y no de grado. Comprometer el almacenamiento local o la integridad del proceso exige acceso al dispositivo concreto de una persona concreta, lo que acota el daño a esa persona y encarece enormemente el ataque a escala. Interceptar tráfico no exige nada de eso: basta con controlar un punto intermedio del camino, un punto de acceso inalámbrico, un resolutor de nombres o un tramo de red operadora, y a partir de ahí el atacante recoge a todos los usuarios que pasen por ahí sin haber tocado ninguno de sus teléfonos. Android ha respondido a esta asimetría de la forma más eficaz posible, que no ha sido dar mejores herramientas al desarrollador sino retirarle progresivamente la capacidad de equivocarse: el texto en claro dejó de estar permitido por defecto, las autoridades que el usuario instala dejaron de ser creíbles, y la política de red se sacó del código y se llevó a un fichero declarativo que se revisa de un vistazo. Lo que queda por decidir es poco, pero una de esas decisiones puede inutilizar tu producto entero.

🎯 Al terminar esta lección sabrás
  • Precisar qué propiedades garantiza TLS, contra qué adversario, y cuáles siguen siendo responsabilidad de tu aplicación.
  • Expresar la política de red completa en network_security_config, incluyendo anclas de confianza y excepciones de depuración.
  • Implementar fijación de clave pública con copias de respaldo, comprendiendo qué se fija exactamente y por qué.
  • Evaluar el coste de mantenimiento de la fijación y decidir con criterio cuándo no aplicarla.

Lo que ya viene resuelto

Una conexión TLS correctamente establecida proporciona tres propiedades y conviene enunciarlas por separado porque se confunden constantemente. Cifra el tráfico, de modo que un observador intermedio no lee el contenido. Garantiza su integridad, de modo que un intermediario no puede modificar bytes sin que la conexión se rompa. Y autentica al servidor mediante una cadena de certificados que termina en una autoridad presente en el almacén de confianza del sistema. No autentica al cliente salvo que lo configures explícitamente, no oculta con qué servidor hablas, y no dice absolutamente nada sobre si ese servidor merece confianza más allá de que alguien validó el control del nombre de dominio.

La plataforma ha ido cerrando las grietas históricas de ese modelo con cambios que rompen compilaciones antiguas, que es exactamente el objetivo. Desde que una aplicación declara como objetivo la API 24, las autoridades de certificación que el usuario instala manualmente dejan de formar parte del conjunto de confianza por defecto, lo que anula de un golpe la técnica más habitual de interceptación mediante un certificado instalado por ingeniería social. Desde la API 28, el tráfico sin cifrar está prohibido salvo que se autorice de forma expresa y acotada.

La contrapartida es que las herramientas de análisis de tráfico dejan de funcionar en compilaciones de depuración, y ahí es donde aparece el atajo más destructivo de todo este nivel: implementar un gestor de confianza que acepte cualquier certificado, o un verificador de nombre de host que siempre devuelva cierto. Ese código elimina íntegramente la autenticación del servidor, convierte cada conexión en interceptable por cualquiera, y tiene la costumbre documentada de sobrevivir hasta producción porque nunca falla de forma visible.

⚠️
El anti-patrón que más credenciales ha filtrado

Un TrustManager vacío o un HostnameVerifier permisivo no debilitan TLS: lo desactivan por completo dejando intacta la apariencia de que sigue activo. La conexión sigue cifrada, el candado sigue apareciendo en las herramientas, y sin embargo cualquier intermediario con un certificado autofirmado lee y modifica todo el tráfico. Si necesitas inspeccionar peticiones, la vía correcta es una configuración de depuración que solo se aplica a compilaciones depurables, nunca una excepción en el código de producción.

La política declarativa

La configuración de seguridad de red es un fichero en res/xml referenciado desde el manifiesto que sustituye a toda la configuración dispersa que antes vivía en el cliente HTTP. Su valor principal no es lo que permite hacer sino que reúne en un solo lugar auditable las decisiones que antes estaban repartidas entre varias clases: qué dominios admiten texto en claro, en qué autoridades se confía, qué excepciones existen y para qué compilaciones.

La estructura es jerárquica. Una configuración base define el comportamiento por defecto, y cada configuración de dominio la sobrescribe para un conjunto de nombres, con herencia opcional a los subdominios. Las excepciones de depuración se aplican exclusivamente cuando la aplicación es depurable, lo que resuelve limpiamente la tensión anterior sin abrir agujeros en la compilación de publicación.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>

    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>

    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.ejemplo.com</domain>
        <pin-set expiration="2027-03-01">
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>

    <!-- Solo se aplica si la compilacion es depurable. -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>

</network-security-config>

Una excepción legítima y frecuente es el dominio de desarrollo local, que puede necesitar texto en claro. Acotarla a un nombre concreto en una configuración de dominio es aceptable; activar el permiso global en el manifiesto para conseguir lo mismo es abrir todo el tráfico de la aplicación para arreglar un caso, y es la forma más común de que una aplicación moderna acabe enviando datos sin cifrar sin que nadie lo advierta.

Fijar la clave pública

La fijación reduce el conjunto de certificados aceptables para un dominio desde todas las autoridades del almacén del sistema, que son más de cien y cualquiera de ellas puede emitir un certificado válido para tu dominio, hasta las que tú enumeres. Elimina así una clase entera de ataques: una autoridad comprometida, coaccionada o simplemente negligente deja de poder suplantar tu servidor.

El detalle técnico que más se malinterpreta es qué se fija exactamente. No se fija el certificado sino el resumen de la clave pública que contiene, lo que permite renovar el certificado conservando el par de claves sin tocar la aplicación. Y no conviene fijar la hoja: fijar el certificado intermedio de tu autoridad ofrece casi la misma protección con muchísimo menos mantenimiento, porque los intermedios viven años mientras las hojas pueden durar semanas.

Toda fijación necesita al menos una copia de respaldo, y esa copia debe corresponder a un par de claves ya generado y custodiado fuera de línea, no a un certificado en uso. Su función es permitir una migración de emergencia si la clave principal se compromete: sin ella, un incidente de clave obliga a publicar una versión nueva y a esperar a que se instale, mientras la aplicación instalada no puede conectar con nada.

🛡️

Fijar el intermedio

Protege frente a cualquier autoridad ajena a la tuya y sobrevive a la renovación rutinaria de la hoja. Es el equilibrio razonable para la mayoría de los productos.

⚠️

Fijar la hoja

Máxima protección y máxima fragilidad. Con certificados de noventa días obliga a coordinar una publicación con cada renovación, algo que ningún calendario resiste mucho tiempo.

Existe además una decisión de emplazamiento que suele tomarse por inercia. La configuración declarativa se aplica a todo el tráfico de la plataforma y se audita en un solo fichero, pero viaja dentro del artefacto y solo cambia publicando. La fijación en el cliente HTTP es más flexible, permite variar por entorno o por variante de compilación y facilita las pruebas, pero se dispersa por el código y es fácil que una segunda instancia del cliente nazca sin ella. La combinación defendible es declarar la política en el fichero y usar el cliente únicamente para los casos que el fichero no puede expresar.

// Alternativa en el cliente, util cuando la politica debe convivir
// con multiples entornos o variantes de compilacion.
val fijador = CertificatePinner.Builder()
    .add("api.ejemplo.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.ejemplo.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
    .build()

val cliente = OkHttpClient.Builder()
    .certificatePinner(fijador)
    .connectTimeout(15, TimeUnit.SECONDS)
    .build()
flowchart TD
A[Peticion HTTPS] --> B[Handshake TLS]
B --> C[Validar cadena contra el almacen del sistema]
C -->|cadena invalida| X[Conexion rechazada]
C -->|cadena valida| D[Hay conjunto de pines para el dominio]
D -->|no| E[Conexion aceptada]
D -->|si| F[Comparar resumen de clave publica]
F -->|coincide algun pin| E
F -->|no coincide| G[Comprobar fecha de caducidad del conjunto]
G -->|caducado| E
G -->|vigente| X

El coste que nadie calcula por adelantado

La fijación es la única medida de esta guía que puede provocar una interrupción total del servicio sin que exista atacante alguno. El escenario es siempre el mismo y se ha repetido en empresas grandes: alguien del equipo de infraestructura renueva un certificado generando un par de claves nuevo, cosa perfectamente normal, y en ese instante todas las versiones instaladas de la aplicación dejan de conectar simultáneamente. La corrección exige publicar, esperar revisión y esperar a que los usuarios actualicen, un proceso de días durante el cual el producto no funciona.

Por eso el conjunto de pines admite una fecha de caducidad y por eso, superada esa fecha, la fijación deja de aplicarse en lugar de bloquear. Esa decisión de diseño es deliberadamente insegura y es la correcta: prefiere degradar a la validación normal antes que dejar inservibles aplicaciones que ya nadie va a actualizar. Omitir la fecha para no perder la protección es exactamente el error que convierte una medida defensiva en una bomba de relojería.

De aquí salen las condiciones bajo las que la fijación tiene sentido. Exige control sobre la infraestructura del servidor y un procedimiento escrito que impida rotar claves sin coordinación, exige copias de respaldo custodiadas, exige vigilancia sobre la fecha de caducidad y exige que el valor protegido justifique el riesgo operativo. Se cumplen en banca, en salud y en mensajería cifrada; casi nunca en una aplicación de contenidos que además habla con dominios de terceros que no controlas y cuyos certificados rotan sin avisarte.

Fijar es apostar a que tu proceso operativo es mejor que el ecosistema de autoridades

Conviene mirar la fijación de certificados por lo que realmente es en términos de riesgo, porque enunciada así la decisión deja de ser técnica y se vuelve organizativa, que es donde de verdad se decide. Sin fijación, tu aplicación delega la autenticación del servidor en un conjunto amplio de autoridades de certificación, y aceptas que cualquiera de ellas pueda emitir un certificado válido para tu dominio; es una superficie incómodamente ancha, pero está vigilada por transparencia de certificados, por navegadores que expulsan autoridades negligentes y por un ecosistema entero cuyo trabajo consiste en detectar emisiones indebidas. Con fijación, retiras esa delegación y la sustituyes por una afirmación tuya: que la clave correcta es esta y solo esta, indefinidamente, para todas las copias instaladas de tu aplicación. La pregunta que decide el asunto no es si la fijación aporta seguridad, porque evidentemente la aporta, sino cuál de los dos sistemas falla con más probabilidad en tu caso concreto: el ecosistema global de autoridades, con su vigilancia distribuida y su historial de incidentes acotados, o tu propio proceso interno de gestión de claves, que en muchas organizaciones consiste en una persona que sabe dónde está el certificado y un recordatorio de calendario que alguien desactivó. Formulada así, la respuesta honesta para la mayoría de los equipos es que el eslabón débil está dentro y no fuera, y de ahí se sigue que fijar sin haber construido antes el proceso operativo no aumenta la seguridad neta: cambia un riesgo externo pequeño y vigilado por un riesgo interno mayor y desatendido. El corolario incómodo, y el que separa el criterio de la ceremonia, es que la fijación no es una casilla que se marca en una auditoría sino un compromiso permanente que se adquiere con el equipo de infraestructura, y que quien la activa sin ese compromiso no ha comprado protección: ha comprado una interrupción futura cuya fecha todavía no conoce.

⚔️ Audita y endurece tu frontera de red
  1. Escribe o revisa tu configuración de seguridad de red y comprueba que ninguna excepción de texto en claro es global.
  2. Busca en todo el proyecto cualquier gestor de confianza o verificador de nombre de host personalizado y justifica o elimina cada uno.
  3. Calcula el resumen de la clave pública del certificado intermedio de tu dominio y compáralo con lo que tengas fijado.
  4. Genera un par de claves de respaldo, custódialo fuera de línea y añade su pin al conjunto con una fecha de caducidad realista.
  5. Simula una rotación de clave no coordinada en un entorno de prueba y cronometra cuánto tardarías en recuperar el servicio.