La propia documentación de Android de Google dice que el certificate pinning «no se recomienda para aplicaciones Android».
Apple dice que «en la mayoría de los casos el pinning no es necesario y debería evitarse».
Y aquí tienes a un ingeniero de Chromium respondiendo a una pregunta sobre cómo configurar bien el pinning. Empieza así: «Aunque el pinning en general nunca debería fomentarse, porque perjudica activamente la seguridad y la estabilidad de internet en su conjunto, si haces pinning contra una CA privada, el Network Security Config de Android es el enfoque preferible». Está explicando literalmente cómo hacerlo y aun así no puede evitarlo.
Mientras tanto, el pinning está presente en aproximadamente la mitad de las apps móviles que revisamos, incluidas las que llegan a nuestra mesa para una auditoría. La buena noticia: si estás en Flutter, probablemente no funciona. La mala noticia: la misma frase.
Lo esencial
- El pinning es una defensa estrecha frente a una amenaza que se ha reducido mucho, y su radio de impacto es toda tu base de usuarios. Certificate Transparency, los registros CAA y el cambio que Android 7 hizo en el almacén de confianza le quitaron casi todo su trabajo original.
- Es realmente necesario en banca, salud y servicios públicos: apps que deben verificarse contra MAS-L2, el perfil de defensa en profundidad de OWASP para software que maneja datos sensibles. Para el resto, Google, Apple, Cloudflare y la mitad de OWASP dicen que no.
- La vida de los certificados se está desplomando: 200 días desde marzo de 2026, 100 en 2027, 47 en 2029. Cualquier esquema que fije el certificado de hoja y necesite una release para rotar acaba costando ocho releases forzadas al año.
- Fija la clave (SPKI), nunca el certificado, e incluye siempre un pin de respaldo para una clave offline que controles tú.
- Nunca salgas directamente a hard-fail, y escribe tu propia telemetría de fallos, porque ninguna de las dos plataformas la trae.
- En Flutter, el
<pin-set>denetwork_security_config.xmlno hace nada con el tráfico de Dart. Dart ejecuta su propia pila TLS. El bug está abierto desde enero de 2022. - La prueba de 30 minutos que hay al final del artículo te dice cuál de estos casos describe tu app. La versión ingenua de esa prueba te miente.
De qué protege realmente el pinning
Una base rápida, para hablar el mismo idioma. Normalmente tu app confía en cualquier certificado firmado por cualquier autoridad de certificación del almacén del sistema, y ese almacén guarda alrededor de ciento cincuenta certificados raíz. El pinning reduce esa lista a algo concreto: esta clave y ninguna otra.
Primero, el nombre. Todo el mundo dice «SSL pinning», nosotros incluidos, en el título de este artículo. SSL está muerto desde hace más de una década: todo es TLS. Pero eso es lo que la gente busca, así que se queda. Ten en cuenta una cosa: si alguien habla de «SSL» en 2026 y lo dice en sentido literal, eso revela algo sobre lo actualizadas que están sus fuentes.
Ahora el modelo de amenazas honesto. Hay dos cosas de las que el pinning protege de verdad:
Una autoridad de certificación comprometida o desleal. Ese es su trabajo real. El caso de manual es DigiNotar, en 2011: los atacantes comprometieron una CA neerlandesa, emitieron certificados fraudulentos para dominios de Google e interceptaron a usuarios iraníes. Fue el pinning lo que lo detectó: Chrome llevaba un pin precargado para google.com.
La interceptación corporativa. Proxies DLP, antivirus que inspeccionan TLS, un MDM que instala una raíz a nivel de sistema. El pinning los rompe todos. Ese es el objetivo, pero también significa que romperás el proxy legítimo de tu cliente corporativo junto con el del atacante.
Y ahora la parte que los artículos de 2015 no cubren, porque nada de esto existía todavía.
La amenaza de la CA desleal se ha reducido mucho
Certificate Transparency publica cada certificado de confianza pública en registros abiertos de solo adición. El mecanismo nació en 2013, pero se volvió obligatorio en la práctica en 2018: Chrome empezó a exigirlo en la versión 68 y Apple lo verifica a nivel de sistema operativo, es decir, dentro de las apps y no solo en Safari.
Y no es teoría. Septiembre de 2015 fue la primera vez que CT detectó un certificado mal emitido en el mundo real: un certificado no autorizado para google.com que Symantec produjo durante pruebas internas. Sin mala intención y válido un solo día. Fue también la primera grieta de una historia que terminó en 2018 con los navegadores retirándole por completo la confianza a Symantec.
Uno reciente se conoció en septiembre de 2025: la CA croata Fina había emitido doce certificados no autorizados para el 1.1.1.1 de Cloudflare, repartidos entre febrero de 2024 y agosto de 2025. Nadie se dio cuenta durante año y medio; lo que acabó sacándolos a la luz fueron los registros de CT. Y ojo: Fina no está en los almacenes de confianza móviles, así que esos certificados no habrían validado nunca en un teléfono. El registro los detectó independientemente de quién confíe en esa CA. Ahí está exactamente el valor.
A eso se suman los registros CAA, registros DNS que declaran qué CA pueden emitir para tu dominio, y la validación de dominio obligatoria desde varias perspectivas de red.
Parte del trabajo ya la hizo la plataforma
A partir de Android 7 —más exactamente, de targetSdk 24— las apps no confían en los certificados instalados por el usuario por defecto. Así es desde 2016. Así que el escenario de «la víctima se instaló una raíz maliciosa» ya no es algo de lo que el pinning te salve. El sistema operativo llegó antes.
targetSdk | CA del sistema | CA instaladas por el usuario | HTTP en claro |
|---|---|---|---|
| ≤ 23 | confianza | confianza | permitido |
| 24–27 | confianza | sin confianza | permitido |
| ≥ 28 | confianza | sin confianza | bloqueado por defecto |
La tabla depende de targetSdk, no de la versión de Android del dispositivo, y por eso una app antigua en un teléfono nuevo sigue confiando en lo que el usuario se haya instalado.
De qué no protege el pinning en absoluto
El planteamiento de OWASP es directo: si un atacante controla el dispositivo, simplemente desactiva tu lógica de pinning. En un Android rooteado o un iPhone con jailbreak eso es un comando en objection, y para Flutter existen herramientas específicas, a las que llegaremos porque son interesantes.
Así que: el pinning protege a tus usuarios reales de la interceptación en una red hostil. No protege tu API del dueño del dispositivo. Si usas pinning para esconder claves o frenar la ingeniería inversa, ya has perdido. Para eso existe la atestación en el servidor —Play Integrity, App Attest—, no los trucos de cliente.
OWASP se contradice, y eso importa cuando alguien te entrega un informe
MASVS sí exige pinning, pero solo para una app que deba verificarse contra MAS-L2, el perfil de defensa en profundidad de OWASP para software que maneja datos sensibles. No es un control de línea base. Y la guía de pruebas dice claramente que si una app no implementa pinning, eso no debería reportarse como vulnerabilidad. Mientras tanto, la Pinning Cheat Sheet de OWASP dice: «La primera pregunta debería ser: ¿debería hacer pinning? La respuesta es probablemente nunca». Ese texto se añadió en marzo de 2023; antes no estaba. Así que cuando un informe de pentest marca «falta de SSL pinning» como hallazgo, eso suele ser una lista de comprobación automática y no una decisión sobre tu modelo de amenazas.
¿De verdad necesitas certificate pinning?
Lista corta. Si alguno de estos puntos es tu caso, no hagas pinning.
- No controlas los dos extremos: tu servidor y tu app
- No puedes actualizar el conjunto de pines de forma segura y rápida
- Actualizar los pines exige una release de la app
- No puedes conocer el par de claves antes de que llegue a producción
- No es una app móvil nativa
Quién lo necesita de verdad: finanzas, salud, servicios públicos; en general, cualquier ámbito donde los datos sean sensibles y el modelo de amenazas asuma una red hostil. Ese es el caso MAS-L2. Para el resto, probablemente no.
Por qué esto se volvió urgente en 2026
En abril de 2025 el CA/Browser Forum votó reducir por fases la vida de los certificados TLS. La propuesta la presentó Apple. El calendario:
| Desde | Vida máxima | Rotaciones al año |
|---|---|---|
| antes de marzo de 2026 | 398 días | ~1 |
| 15 de marzo de 2026 | 200 días | ~2 |
| 15 de marzo de 2027 | 100 días | ~4 |
| 15 de marzo de 2029 | 47 días | ~8 |
El escalón de 200 días está en vigor ahora mismo. Cuarenta y siete días son aproximadamente ocho rotaciones de certificado al año. Si fijas el certificado de hoja y no puedes actualizar los pines sin publicar, eso son ocho actualizaciones forzadas de la app al año: con revisión en la tienda y con usuarios que no actualizan.
Y el ecosistema pelea contra el pinning a propósito. Ya en 2024, Let's Encrypt empezó a elegir al azar el intermedio emisor, precisamente para romper la costumbre de fijar intermedios. En noviembre de 2025 llevó la misma política a una nueva jerarquía de seis intermedios, y el anuncio lo deja escrito: «como antes, cada emisión elegirá al azar qué intermedio usar, para desincentivar el pinning de claves intermedias». La CA más usada de internet lleva dos años rompiendo deliberadamente ese tipo de pinning.
No es mala fe, es experiencia pagada a un precio alto. El mundo de los navegadores ya pasó por esto.
HPKP —pinning mediante una cabecera HTTP, especificado en 2015 por ingenieros de Google— fracasó por completo. Chrome lo marcó como obsoleto en la versión 67 y lo eliminó en la versión 72, en enero de 2019. Los motivos que dio Google: adopción muy baja, riesgo de denegación de servicio y pinning hostil.
La víctima famosa es Smashing Magazine, en octubre de 2016: cuatro días fuera de servicio para la mayoría de sus lectores. Rotaron a un certificado con una clave nueva, publicaron una cabecera que contenía solo el pin nuevo, y todo el que tuviera el conjunto anterior en caché se quedó fuera. No había marcha atrás: el certificado antiguo ya había caducado.
En el mundo móvil, Barclays, noviembre de 2016. Según lo publicado, la app fijaba un intermedio obsoleto, la cadena cambió y los pagos se detuvieron en la víspera del Black Friday. Para arreglarlo a tiempo, Symantec emitió un certificado bajo el intermedio antiguo, y para hacerlo tuvo que incumplir el requisito de números de serie del CA/Browser Forum.
La moraleja es la misma en los dos casos: el pinning no se rompe cuando te atacan. Se rompe un martes cualquiera, cuando alguien renueva un certificado.
Cinco reglas si vas a hacerlo de todos modos
Regla 1. Fija la clave, no el certificado
Técnicamente eso es el SPKI: SubjectPublicKeyInfo, la estructura dentro del certificado que contiene la clave pública. Se fija su hash SHA-256 en base64.
Por qué esto lo cambia todo: el certificado cambia en cada renovación, la clave no. Si fijas la clave, una renovación con la misma clave no toca tu pin. Si fijas la huella del certificado completo, se rompe siempre: dos veces al año con 200 días, ocho con 47.
Para un host en producción:
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
Y el mismo hash a partir de una clave privada que ya has generado pero aún no has certificado, que es como se calcula un pin de respaldo:
openssl pkey -in backup.key -pubout -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
La buena noticia: las dos plataformas, Android e iOS, solo admiten SPKI de forma nativa. Así que si estás fijando un certificado completo, o lo escribiste a mano o elegiste una biblioteca que lo hace por ti, y casi con seguridad sin ningún motivo válido.
Regla 2. Los pines de respaldo son obligatorios
Al menos una clave completamente bajo tu control. Esa es la formulación de Google, y «completamente bajo tu control» es la parte que sostiene la frase. No «un segundo certificado de la misma CA». Un par de claves que generaste por adelantado y guardas offline; puedes pedir un certificado para él cuando lo necesites. Su pin va en la app junto al de producción y se queda ahí esperando.
La falta de ese pin de respaldo es exactamente lo que convirtió el mal día de Smashing Magazine en cuatro días de caída. En realidad todavía tenían la clave antigua: su proveedor la había conservado. Lo que habían publicado en la cabecera era solo la huella de la nueva.
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-03-01">
<!-- clave en producción -->
<pin digest="SHA-256">YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</pin>
<!-- clave de respaldo offline, que nunca ha servido tráfico -->
<pin digest="SHA-256">sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=</pin>
</pin-set>
</domain-config>
</network-security-config>
El equivalente en iOS, declarativo y aplicado por el sistema operativo:
<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSIncludesSubdomains</key><true/>
<key>NSPinnedLeafIdentities</key>
<array>
<dict><key>SPKI-SHA256-BASE64</key><string>YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</string></dict>
<dict><key>SPKI-SHA256-BASE64</key><string>sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=</string></dict>
</array>
</dict>
</dict>
</dict>
Ese ejemplo fija claves de hoja. La documentación de Apple usa NSPinnedCAIdentities: la misma estructura un nivel más arriba en la cadena, que es justo el argumento de la regla 3.
Regla 3. Elige tu nivel en la cadena y sé consciente de lo que cuesta
Aquí no hay consenso, así que van las dos posturas. Apple recomienda fijar la CA en lugar del servidor, para poder rotar los certificados del servidor sin publicar una actualización. OWASP recomienda lo contrario: fijar la hoja, pero siempre con respaldo.
| Nivel | Coste de rotación | En qué confías | Veredicto en 2026 |
|---|---|---|---|
| Hoja | Gratis si la renovación mantiene la clave; una release en caso contrario | En exactamente una clave | Viable solo con pines de respaldo |
| Intermedio | Impredecible | En todo lo que ese intermedio emita | Descartado en CA públicas |
| Raíz pública | Raro | En todo lo que esa CA emita alguna vez | Tan amplio que es casi inútil |
| Tu propia raíz privada | Lo decides tú | En tu propia PKI | El único caso claramente bueno |
Fijar un intermedio para certificados de confianza pública está descartado desde 2024: Let's Encrypt tiene ahora seis intermedios y no puedes predecir cuál le toca a tu certificado. Si estás en una CA pública, quedan la hoja con pines de respaldo o la raíz.
La configuración en la que el pinning sigue siendo claramente una buena idea es tu propia CA privada, la que controlas de punta a punta. Fijar tu propia raíz es sensato, predecible y no se rompe porque otro haya cambiado de opinión. Y es, por cierto, exactamente lo que decía el ingeniero de Chromium en la cita del principio.
Regla 4. Nunca salgas directamente a hard-fail
El orden es: primero modo de solo informe, en el que el pin se comprueba, la conexión no se bloquea y los fallos van a telemetría. Se observa durante una semana. Después hard-fail para el uno por ciento de los usuarios. Después un despliegue escalonado hasta el cien por cien.
Y aquí viene la parte molesta: ninguna de las dos plataformas trae informes de fallo integrados. Ni el Network Security Config de Android, ni App Transport Security en iOS. El único mecanismo que hizo esto alguna vez fue el report-uri de HPKP, y está muerto. Así que la telemetría la escribes tú: capturas la excepción, construyes un evento —dominio, clave recibida, claves esperadas, versión de la app— y lo envías por un canal que no esté fijado, o el informe de tu propia caída no saldrá nunca.
Otra cosa: aprende a distinguir una caída de un ataque. Si fallan todos a la vez, has roto tu propia rotación. Los fallos dispersos son un proxy corporativo, un antivirus o un intento real de interceptación.
Regla 5. Construye el interruptor de emergencia antes de necesitarlo
En Android hay uno integrado: el atributo expiration del conjunto de pines. Pasada esa fecha, el pinning simplemente se detiene: los pines no se comprueban y el tráfico circula como si nada. Un interruptor con temporizador.
Pero tiene un precio, y Google lo dice explícitamente: «establecer un tiempo de expiración en los pines puede permitir que los atacantes eludan tus certificados fijados». Así que es a la vez tu seguro contra dejar la app inservible y una forma de saltarse tu pinning simplemente esperando. Y en otra página de la misma documentación Google recomienda «un periodo de expiración suficientemente corto». Las dos afirmaciones son oficiales y las dos están vigentes.
Y aquí está nuestro detalle favorito de toda esta historia. El ejemplo canónico de la documentación de Google sigue llevando expiration="2018-01-01". Si lo copiaste —y todo el mundo lo copia—, tu pinning lleva ocho años completamente desactivado. Actualizaron esa página en junio de 2026. La fecha se quedó.
En iOS no existe ningún mecanismo equivalente. Desactivar el pinning allí significa publicar una release.
Si haces el interruptor por configuración remota o feature flags, cuidado con el problema del huevo y la gallina: el canal de entrega no puede estar protegido por los mismos pines, o nunca podrás recuperarte. La respuesta correcta es firmar el conjunto de pines con una clave cuya mitad pública venga compilada en la app. Así da igual por qué canal llegó. Y añade un número de versión que solo pueda subir, para que nadie pueda reinyectar un conjunto anterior firmado correctamente.
Flutter: donde el certificate pinning se desmorona
Todo lo anterior vale para cualquiera. Esta es la parte por la que escribimos el artículo: construimos apps en Flutter y aquí hemos metido las manos hasta el fondo.
El titular, y va contra la intuición de la mayoría: Dart no usa la pila de red de la plataforma.
HttpClient de dart:io no pasa por OkHttp ni HttpURLConnection en Android, ni por NSURLSession en iOS. Abre un socket y ejecuta el handshake TLS por su cuenta, con una copia de BoringSSL compilada dentro del motor de Flutter.
La consecuencia práctica: tu <pin-set> en network_security_config.xml no hace nada con el tráfico de Dart. En silencio. Sin error y sin aviso: la petición simplemente sale.
Esto no lo hemos descubierto nosotros. Es el issue #96722 de Flutter, abierto en enero de 2022 y todavía sin cerrar. Un miembro del equipo de Flutter lo reprodujo y dejó la conclusión por escrito: en iOS la petición se cancela correctamente, en Android sale igualmente, y en una app de control nativa de Android con la misma configuración falla como debería.
En iOS el comportamiento es distinto: se informa de que funciona, porque Dart en las plataformas de Apple delega la decisión de confianza al SecTrust del sistema. Eso no está documentado en ninguna parte, y sinceramente es el peor desenlace posible. Un pinning que funciona en una plataforma y no hace nada en silencio en la otra es más peligroso que un pinning que no funciona en ninguna, porque tú lo probaste. En un iPhone.
Y luego tres trampas. Hemos caído en las tres en producción.
Dart no te da acceso al SPKI. La clase X509Certificate expone der, pem, sha1, sujeto, emisor y fechas de validez. La clave pública no está entre ellos. Así que prácticamente todos los tutoriales fijan un SHA-256 del certificado completo, que, según la regla 1, se rompe en cada renovación. Hacerlo bien implica analizar ASN.1 a mano.
El propio README de dio muestra dos maneras de hacer esto mal. Esta es la primera, condensada:
// Ejemplo del README de dio. Tres problemas en nueve líneas.
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient(context: SecurityContext(withTrustedRoots: false));
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
validateCertificate: (cert, host, port) =>
cert != null && fingerprint == sha256.convert(cert.der).toString(),
);
withTrustedRoots: false junto a un badCertificateCallback incondicional desactiva la construcción de la cadena, la caducidad y la verificación del nombre de host. La huella que compara es un hash de cert.der, es decir, del certificado completo, y eso —según la regla 1— se rompe en cada renovación. Y validateCertificate se ejecuta más tarde de lo que parece; sigue leyendo.
La segunda variante del README es peor: fija el pin dentro de badCertificateCallback, comparando cert.pem con un PEM guardado. Y ese callback, por definición, se dispara solo cuando la validación ya ha fallado. Si un atacante presenta un certificado firmado válidamente por cualquier CA de confianza, no se ejecuta en absoluto y no se compara nada. Como mecanismo de pinning está roto por diseño.
El hook correcto se ejecuta demasiado tarde. En dio hay uno: validateCertificate, que se dispara cuando la validación es correcta. Pero corre después de que la petición haya salido y la respuesta ya haya llegado. Y no son solo el cuerpo de la petición y la cabecera Authorization los que ya viajaron: también has aceptado el estado y las cabeceras de la respuesta. Lo único que salva esa comprobación es el cuerpo de la respuesta. La petición ya se filtró.
Y ahora los paquetes
El paquete de pinning más popular de pub.dev —unos ciento sesenta likes y casi cincuenta mil descargas al mes cuando lo comprobamos en agosto de 2026— usa un puente de platform channels para hacer una petición nativa aparte a tu host, comprueba la huella en ella y, si coincide, envía la petición real por una conexión distinta sin ningún pinning. Una conexión se valida; otra transporta tus datos. Además, cada petición son ahora dos peticiones. No lo nombramos, porque esto no va de un mantenedor concreto: lee el código del paquete de pinning que uses tú y averigua qué conexión comprueba en realidad.
Qué hacer de verdad. En las plataformas de Apple hay una respuesta limpia: mover tu capa HTTP a cupertino_http. Eso es un NSURLSession real, lo que significa que el pinning es declarativo: NSPinnedDomains en Info.plist, exactamente como en la regla 2. Lo aplica el sistema operativo, va por SPKI y no requiere código de terceros.
En Android no hay equivalente. Cronet tiene su propia API de pinning, pero el paquete cronet_http no la expone. El binding JNI está ahí mismo, dentro del paquete. Lo que el paquete sí expone es enablePublicKeyPinningBypassForLocalTrustAnchors: el interruptor que elude el pinning. El pinning, no.
Queda además una pregunta abierta que nadie en internet responde con claridad, y la dejamos marcada como abierta en lugar de fingir lo contrario: si el Network Security Config de Android se aplica al tráfico que pasa por Cronet. Las fuentes se contradicen y no hay documentación oficial en ningún sentido. Pruébalo tú antes de apoyarte en cualquiera de las dos respuestas.
Dos avisos finales específicos de Flutter. Los SDK de terceros —analítica, informes de fallos, pagos— hacen llamadas de red con sus propios clientes nativos, y tu pinning no los toca. Esa misma separación es la que convierte el deferred deep linking en Flutter en un problema por plataforma en lugar de una sola implementación. Y si migras a native_dio_adapter para usar la pila de red nativa, el pinning basado en dio que copiaste de un artículo se apaga en silencio, porque SecurityContext y los dos callbacks dejan de invocarse.
Cómo comprobar tu pinning en media hora
Hazlo bien, porque la versión ingenua te miente.
La versión ingenua es: levantas un proxy, instalas su certificado en el teléfono y miras si la app se rompe. Se romperá. Y eso no significa nada: como vimos arriba, desde Android 7 las apps ya no confían en los certificados instalados por el usuario. Concluirás que te protege el pinning cuando lo que te protegía todo este tiempo era el sistema operativo.
La verificación de pinning en 30 minutos
Ejecútala una vez por plataforma. Todo lo que quede sin marcar es un hallazgo.
Preparación
- Build de release, no de debug: un bloque debug-overrides invalida toda la prueba
- Certificado del proxy instalado en el almacén de confianza del SISTEMA, no en el del usuario
- Flutter: confirmado que el tráfico llega al proxy mediante captura por VPN o iptables, porque Dart ignora los ajustes de proxy del sistema
- Recorrido un flujo autenticado real, no solo la primera pantalla
Qué buscar
- El tráfico de tu API no es legible; si lo puedes leer, tu pinning no funciona
- El tráfico de los SDK de terceros y el tuyo se comportan igual, o sabes por qué no
- El fallo que ves es un fallo de pinning, no un fallo del almacén de confianza
- El mismo resultado en Android y en iOS
Después revisa la propia configuración
- Los pines son hashes SPKI, no huellas de certificado
- Hay al menos un pin de respaldo, para una clave offline que controlas tú
- Android: la fecha de expiration del conjunto de pines está en el futuro y se eligió a conciencia
- Existe un interruptor de emergencia que no exige una release en la tienda
- La telemetría de fallos sale por un canal que no está fijado
La versión corta
El pinning es una defensa estrecha frente a una amenaza que se ha vuelto mucho menos probable, y su radio de impacto abarca a toda tu base de usuarios. Es realmente necesario en banca, salud y servicios públicos. Para el resto, Google, Apple, Cloudflare y la mitad de OWASP dicen que no, y tienen motivos: los mismos que mataron HPKP y los que están recortando la vida de los certificados a 47 días.
Si lo haces: la clave, no el certificado. Pines de respaldo con una clave offline. Observación antes del hard-fail. Tu propio interruptor de emergencia. Y claridad sobre lo que pagas por tu posición en la cadena.
Y si estás en Flutter, averigua primero si de verdad funciona. Las tres respuestas más probables: pinning desactivado por una fecha de 2018 copiada de la documentación de Google; pinning declarado en un archivo de configuración que Dart nunca lee; o pinning que valida una conexión por la que tus datos no viajan.
La versión que corre por los blogs dice «activa el pinning, protege a tus usuarios». La realidad de ingeniería es averiguar a qué estás fijado de verdad, verificar que se dispara en las dos plataformas y guardar una clave de repuesto en una caja fuerte. De eso nadie hace vídeos virales.
Fuentes
Todo lo anterior es comprobable, así que aquí tienes de dónde sale cada cosa.
- Google, «Security with HTTPS and SSL» — el pinning «no se recomienda para aplicaciones Android»; pines de respaldo y «al menos una clave completamente bajo tu control»: developer.android.com/privacy-and-security/security-ssl
- Google, «Network security configuration» — el atributo
expiration, la advertencia sobre la elusión, «solo se admiteSHA-256» y el ejemplo con2018-01-01: developer.android.com/privacy-and-security/security-config - Apple, «Identity Pinning: How to configure server certificates for your app» — «en la mayoría de los casos el pinning no es necesario y debería evitarse», y la recomendación de fijar la CA en lugar de la hoja: developer.apple.com/news
- Ryan Sleevi en
net-dev@chromium.org, marzo de 2021 — la cita sobre la CA privada completa: groups.google.com - OWASP Pinning Cheat Sheet — «la respuesta es probablemente nunca» y la lista de motivos para no fijar: cheatsheetseries.owasp.org
- OWASP MASTG, comunicación de red — «si una app no implementa pinning, no debería reportarse como vulnerabilidad; pero si la app debe verificarse contra MAS-L2, debe implementarse»: github.com/OWASP/mastg
- Propuesta SC-081v3 del CA/Browser Forum — el calendario 398 → 200 → 100 → 47, presentada por Apple y aprobada el 11 de abril de 2025: cabforum.org
- Let's Encrypt, «Generation Y» — seis intermedios elegidos al azar «para desincentivar el pinning de claves intermedias»: letsencrypt.org
- Cloudflare — los doce certificados no autorizados para 1.1.1.1 y la ausencia de Fina en los almacenes raíz móviles: blog.cloudflare.com
- Issue #96722 de Flutter — abierto desde enero de 2022: github.com/flutter/flutter
- dio,
IOHttpClientAdapter— dónde se ejecuta realmentevalidateCertificate, en el código fuente: github.com/cfug/dio - Dart
X509Certificate— la lista completa de lo que expone, sin la clave pública: api.dart.dev cronet_http,CronetEngine.build— la lista de parámetros, con la elusión incluida y el pinning ausente: pub.dev
Si quieres el contexto sobre qué avala realmente una autoridad de certificación, lo escribimos en HTTPS sin complicaciones, y la criptografía que hay debajo en cómo funciona el cifrado. Este artículo acompaña a nuestro episodio en vídeo sobre el mismo tema; el anterior fue todos se equivocaron de plazos.
Y si al ejecutar la prueba encontraste algo inesperado, sobre todo el fallo en una sola plataforma, nos interesa de verdad saberlo. Nuestra apuesta es que la mitad de las apps Flutter tienen un pinning que falla al menos en una plataforma.
Ilya Nixan es fundador y lead developer en Nerdy Production, una agencia Flutter-first que construye y mantiene apps para fintech, salud y retail. También hacemos desarrollo de apps en Flutter para equipos que preferirían no descubrir todo esto en producción.
