¿Cómo implementas certificate pinning y cómo gestionas la rotación de pines sin dejar la app inutilizable?
El pinning evita que un certificado técnicamente válido pero no confiable engañe a tu app, pero si la rotación sale mal, dejas inutilizable cada instalación sin actualizar.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo implementas certificate pinning y cómo gestionas la rotación de pines sin dejar la app inutilizable?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué la validación TLS estándar no basta
El TLS estándar ya frena la escucha pasiva y la mayoría de la manipulación activa, porque el sistema operativo valida la cadena de certificados del servidor contra un almacén de confianza de unos pocos cientos de CAs raíz. La brecha que cierra el pinning es otra distinta: te protege de un certificado que es técnicamente válido pero que no debería ser de confianza para tu API — un proxy MITM corporativo con su propia CA raíz instalada en los dispositivos gestionados, una CA comprometida o coaccionada que emite un certificado fraudulento para tu dominio, o un usuario que instaló un proxy de "depuración" (Charles, mitmproxy) cuya CA ahora intercepta en silencio cada app del dispositivo, no solo la que quería depurar. Pinning dice: "no quiero solo un certificado de confianza para este host, quiero esta clave en concreto."
Hay dos variantes, y la diferencia lo es todo a la hora de rotar. El certificate pinning fija el certificado exacto (o su hash completo) — se rompe en el instante en que ese certificado se renueva, incluso con la misma clave. El public key pinning fija en cambio el hash SHA-256 del Subject Public Key Info (SPKI) — mientras el certificado renovado reutilice el mismo par de claves, el pin sigue coincidiendo. Toda implementación seria (CertificatePinner de OkHttp, TrustKit, la propia guía de Apple) fija hashes SPKI justo por esto: los certificados caducan en un calendario que no controlas del todo, sobre todo con vidas de 90 días al estilo Let's Encrypt cada vez más comunes, pero los pares de claves sí pueden reutilizarse entre renovaciones.
#Implementarlo en ambas plataformas
En Android, el CertificatePinner de OkHttp hace la comparación de SPKI por ti — solo le das los hashes:
val certificatePinner = CertificatePinner.Builder().add("api.myapp.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // clave actual.add("api.myapp.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // clave de respaldo, generada pero aún no desplegada.build()val client = OkHttpClient.Builder().certificatePinner(certificatePinner).build()
Puedes obtener el hash SPKI de cualquier endpoint en producción directamente desde la línea de comandos:
openssl s_client -connect api.myapp.com:443 -servername api.myapp.com < /dev/null 2>/dev/null \| openssl x509 -pubkey -noout \| openssl pkey -pubin -outform der \| openssl dgst -sha256 -binary \| openssl enc -base64
iOS no tiene una API de pinning propia tan lista para usar como la de OkHttp, así que la mayoría de equipos usa TrustKit o implementa directamente el callback de reto del URLSessionDelegate:
final class PinningDelegate: NSObject, URLSessionDelegate {private let pinnedHashes: Set<String> = ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // clave actual"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // clave de respaldo]func urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard let trust = challenge.protectionSpace.serverTrust,let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],let leaf = chain.first,let publicKey = SecCertificateCopyKey(leaf),let keyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {completionHandler(.cancelAuthenticationChallenge, nil)return}let hash = Data(SHA256.hash(data: keyData)).base64EncodedString()if pinnedHashes.contains(hash) {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}}}
#Rotar pines sin generar una crisis
La solución no es "no hagas pinning", es tratar la rotación como parte del diseño desde el primer día:
- Fija la clave pública, no el certificado hoja, así una renovación rutinaria que reutiliza el par de claves no rompe nada.
- Publica un pin de respaldo antes de necesitarlo. Genera un segundo par de claves, fija su hash SPKI junto al activo, y guarda la clave privada fuera de línea hasta el día en que realmente rotes. La app que ya está en manos de los usuarios entonces ya confía en la próxima clave antes de que el servidor la presente siquiera.
- Dale a tu equipo un interruptor remoto de emergencia. Una configuración obtenida de forma remota (por una conexión que no falle duro ante un pin que no coincide, o desde un endpoint separado sin pinning usado solo para esto) que pueda pasar el pinning a modo "solo reportar" te permite recuperarte de una rotación fallida sin un lanzamiento de emergencia.
- Define una caducidad para el conjunto de pines. Publica los pines con un mecanismo de "deja de exigir esto después de tal fecha" para que una versión de la app que nadie actualiza nunca degrade a la confianza TLS estándar en vez de fallar para siempre — un cambio deliberado de margen de seguridad por disponibilidad en tu cola larga de instalaciones sin actualizar.
- Trata la rotación como un contrato entre equipos. El equipo de backend/infraestructura que rota un certificado TLS necesita saber que los pines móviles dependen de ello, con suficiente antelación para publicar el pin de respaldo, pasarlo por la revisión de la tienda y dejar que la adopción suba antes de retirar la clave vieja.