¿Cómo funciona realmente el certificate pinning, y qué se rompe cuando lo haces mal?
El pinning cambia un tipo de riesgo — una CA fraudulenta suplantando tu API — por otro: rota el certificado del servidor sin un plan y puedes dejar inutilizada cada copia instalada de tu app.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona realmente el certificate pinning, y qué se rompe cuando lo haces mal?
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.#El problema de confianza que el pinning en realidad resuelve
TLS estándar ya verifica que tu app está hablando con algún servidor que posee un certificado firmado por alguna autoridad certificadora en la que confía el sistema operativo — pero "alguna CA en la que confía el sistema" es un conjunto mucho más grande de lo que la mayoría asume. Cientos de CAs raíz e intermedias vienen confiadas por defecto en Android y iOS, cualquier perfil de MDM empresarial puede agregar una raíz corporativa en la que el dispositivo confiará silenciosamente, y la historia tiene más de un incidente de una CA emitiendo por error un certificado para un dominio que no tenía por qué firmar. El certificate pinning reduce "cualquier CA en la que confía el sistema" a "específicamente este certificado, o esta clave, y nada más" — así que incluso una CA comprometida o demasiado permisiva en el almacén de confianza no puede usarse para hacer man-in-the-middle sobre el tráfico de tu app, ya sea que ese MITM lo haga un criminal en wifi público o un proxy corporativo inspeccionando silenciosamente tráfico que nunca debió ver.
#Dos cosas que realmente puedes fijar (pin), y por qué una gana
Puedes fijar el certificado hoja completo, o puedes fijar el hash SHA-256 de solo su Subject Public Key Info (SPKI) — la clave pública, sin los metadatos del certificado alrededor. Fijar el certificado hoja es frágil: cada vez que el certificado se renueva (anualmente, o mucho más seguido con certificados de vida corta), el pin queda obsoleto y la app se rompe a menos que publique una actualización sincronizada con cada rotación de certificado del servidor, lo cual es una dependencia operativa que nadie quiere. Fijar el hash SPKI es la práctica que de verdad sobrevive a la operación real, porque mientras el operador del servidor renueve el certificado reutilizando el mismo par de claves, el pin sigue coincidiendo a través de la renovación — el hash solo cambia cuando la clave misma cambia, un evento mucho más raro y deliberado.
#Dónde lo conectas realmente
En Android, el camino declarativo es un archivo XML de Network Security Config referenciado desde el manifest — sin código, y sobrevive a un cambio de librería HTTP porque lo impone la propia pila TLS de la plataforma, no algo dentro de OkHttp:
<network-security-config><domain-config><domain includeSubdomains="true">api.acme.com</domain><pin-set expiration="2027-06-01"><pin digest="SHA-256">k3XnEC1nEZXsanA1FBcx9x9wgD/DZKAxDS0S07uMDeo=</pin><pin digest="SHA-256">MHJYVThihUrJcxW6wcqyOISTXIsInsdactHnV8gxwWQ=</pin></pin-set></domain-config></network-security-config>
El equivalente en código, si ya estás configurando OkHttp directamente:
val certificatePinner = CertificatePinner.Builder().add("api.acme.com", "sha256/k3XnEC1nEZXsanA1FBcx9x9wgD/DZKAxDS0S07uMDeo=").add("api.acme.com", "sha256/MHJYVThihUrJcxW6wcqyOISTXIsInsdactHnV8gxwWQ=").build()val client = OkHttpClient.Builder().certificatePinner(certificatePinner).build()
En iOS, sin una librería de terceros, se hace dentro de URLSessionDelegate, evaluando el certificado del servidor contra tus hashes embebidos durante el desafío TLS:
func urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard let trust = challenge.protectionSpace.serverTrust,let certificate = SecTrustGetCertificateAtIndex(trust, 0) else {completionHandler(.cancelAuthenticationChallenge, nil)return}let serverPublicKeyHash = spkiSHA256(of: certificate)let pinnedHashes: Set<String> = ["k3XnEC1nEZXsanA1FBcx9x9wgD/DZKAxDS0S07uMDeo=","MHJYVThihUrJcxW6wcqyOISTXIsInsdactHnV8gxwWQ=",]if pinnedHashes.contains(serverPublicKeyHash) {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}}
#El modo de falla que de verdad tumba producción
Ambos ejemplos anteriores publican dos pines, y eso no es casualidad. Si fijas exactamente una clave y el operador del servidor alguna vez rota a una clave nueva — una mejora de seguridad planeada, un cambio de autoridad certificadora, una migración de infraestructura — cada copia instalada de tu app pierde de golpe la capacidad de alcanzar tu API, sin forma de recuperarse salvo publicando una actualización de la app, que además tiene que pasar la revisión de la tienda mientras toda tu base de usuarios ve errores de red en cada pantalla. La mitigación estándar es fijar tanto la clave actual como una clave de respaldo generada de antemano pero aún no desplegada en el servidor, para que una rotación futura hacia esa clave de respaldo sea aceptada silenciosamente por cada app que ya está en producción — el segundo pin es un seguro que compras antes de necesitarlo, porque no hay forma de agregarlo después a una versión ya instalada en cien mil dispositivos.
#Acertar el límite en el día a día del desarrollo
El pinning tiene que ser, en la práctica, un asunto exclusivo del build de release, porque es directamente incompatible con los proxies de depuración tipo MITM (Charles, Proxyman) de los que depende la mayoría de los equipos mobile para inspeccionar su propio tráfico durante el desarrollo — el patrón estándar es una bandera de configuración de build que desactiva la validación de pines en builds de debug mientras la impone incondicionalmente en release, para que nadie publique accidentalmente un build donde el pinning silenciosamente no hace nada. También vale la pena fijar en la capa correcta de tu propia infraestructura: si las peticiones terminan en el borde de un CDN, distintos puntos de presencia pueden presentar certificados diferentes para el mismo hostname, así que fija lo que el borde del CDN realmente presenta en cada región, no solo el certificado de origen que ves desde tu propia máquina — de lo contrario, "funciona en staging, se rompe para una porción de usuarios en producción" se convierte en un reporte de bug muy confuso.