Que es Certificate Pinning
Certificate pinning es una técnica de seguridad que valida el certificado SSL/TLS del servidor contra hashes SHA-256 conocidos (pins). Protege contra ataques Man-in-the-Middle (MITM) donde un atacante con una CA comprometida o acceso a un proxy corporativo intercepta trafico HTTPS.
Sin pinning, cualquier certificado firmado por una CA confiable del sistema operativo es aceptado. Con pinning, solo se aceptan certificados cuyos pins coincidan con los configurados en la app.
Por que Nvito lo implementa
nvito-client (app movil) maneja datos sensibles: tokens JWT, información de invitados, RSVPs, códigos QR. Un ataque MITM permitiria a un atacante interceptar estos datos. Certificate pinning agrega una capa adicional de protección sobre HTTPS.
Arquitectura de protección (5 capas)
El sistema tiene 5 capas de protección para garantizar que la app nunca deje de funcionar por renovacion de certificados:
| Capa | Componente | Que hace | Automático? |
|---|---|---|---|
| 1 | Dual-pin | Acepta leaf certificate actual + intermediate CA como backup | Si |
| 2 | Circuit breaker | Si el pinning falla 3 veces consecutivas, usa fetch nativo + reporta telemetria | Si |
| 3 | Cron NestJS | Verifica certificado cada lunes 9AM, envia email + WhatsApp si expira en menos de 30 días | Si |
| 4 | UptimeRobot | Verifica endpoint /health/certificate-status cada 6h, alerta independiente | Si |
| 5 | OTA Update | Distribuye pins nuevos sin pasar por App Store/Play Store | Semi-auto (1 comando) |
Archivos involucrados
nvito-client
src/api/certificate-pinning.ts— Modulo principal: pins, secureFetch, circuit breakersrc/api/client.ts— Cliente HTTP que usasecureFetch()app.json— Configuración deexpo-updatespara OTA
nvito-api
src/modules/health/certificate-check.service.ts— Verifica certificado via TLSsrc/modules/health/health.controller.ts— Endpoints/health/certificate-statusy/health/pinning-reportsrc/modules/scheduler/certificate-scheduler.service.ts— Cron semanal
Variables de entorno (nvito-api)
CERTIFICATE_CHECK_HOST— Hostname a verificar (ej:api.nvito.mx). Si está vacio, la verificacion se omite.CERTIFICATE_CHECK_PORT— Puerto (default:443)CERTIFICATE_ALERT_EMAIL— Email del admin para alertas
Obtencion de pins — Paso a paso
Prerequisitos
- Acceso a terminal con
opensslinstalado - El dominio de producción (
api.nvito.mx) debe tener certificado SSL activo
Obtener pin del leaf certificate (certificado principal)
openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
openssl enc -base64
El resultado es una cadena base64 como: Lt6FclMCwqOVd+xKh1mdNVYqD+5SfreN0tLvufVeWa4=
Obtener pin del intermediate CA (certificado de respaldo)
openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
awk '/-----BEGIN CERTIFICATE-----/{n++} n==2' | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
openssl enc -base64
Donde colocar los pins
En nvito-client/src/api/certificate-pinning.ts:
export const CERTIFICATE_PINS: Record<string, string[]> = {
'api.nvito.mx': [
'sha256/Lt6FclMCwqOVd+xKh1mdNVYqD+5SfreN0tLvufVeWa4=', // Leaf
'sha256/kO/IpuhlNGVQKz4DsJnvYCNVx1bCtPCp8mPexO/Eqmw=', // Intermediate CA
],
};
Siempre incluir mínimo 2 pins: leaf + intermediate CA. El intermediate CA no cambia al renovar el leaf certificate, actuando como backup.
Rotación de certificados — Procedimiento estandar
Cuando ejecutar este procedimiento
- El cron semanal envia alerta de que el certificado expira en menos de 30 dias
- UptimeRobot alerta que
certificate-statusretornawarning: true - Renovacion planificada del certificado SSL
Pasos
Paso 1 — Obtener el nuevo pin
Ejecutar los comandos openssl de la sección anterior contra el nuevo certificado. Si el certificado aun no se ha renovado en el servidor, obtener el pin del certificado pendiente del proveedor SSL.
Paso 2 — Actualizar certificate-pinning.ts
Agregar el nuevo pin como segundo elemento del array (mantener el pin actual como primero):
'api.nvito.mx': [
'sha256/PIN_ACTUAL', // Certificado actual (aun valido)
'sha256/PIN_NUEVO', // Nuevo certificado (pre-pin)
'sha256/PIN_INTERMEDIATE_CA', // Backup del CA
],
Paso 3 — Commit y push
git add src/api/certificate-pinning.ts
git commit -m "chore: pre-pin nuevo certificado SSL para rotación"
git push
Paso 4 — Distribuir via OTA
eas update --branch production --message "Rotación de pins SSL"
Este comando pública el nuevo bundle JS. Los usuarios reciben el update automáticamente al abrir la app. No necesitan actualizar desde el store.
Paso 5 — Esperar adopción
Esperar mínimo 48 horas para que el 95% de los usuarios reciban el OTA update.
Paso 6 — Renovar certificado en el servidor
Renovar el certificado SSL en Coolify/Railway. La app acepta ambos pins (viejo + nuevo).
Paso 7 — Verificar
# Verificar que el servidor tiene el nuevo certificado
openssl s_client -connect api.nvito.mx:443 < /dev/null 2>/dev/null | \
openssl x509 -noout -dates -fingerprint
# Verificar endpoint de status
curl https://api.nvito.mx/v1/health/certificate-status | jq .
Paso 8 — Limpiar pin antiguo (siguiente release)
En el siguiente ciclo, remover el pin antiguo y mantener solo el actual + intermediate CA.
Monitoreo automático
Cron NestJS (CertificateSchedulerService)
- Frecuencia: Cada lunes a las 9:00 AM (America/Mexico_City)
- Que verifica: Conecta via TLS al hostname configurado, extrae info del certificado
- Cuando alerta: Si el certificado expira en menos de 30 días o si hay error de conexión
- Como alerta: Log WARN con instrucciones de rotación. (Email/WhatsApp pendiente de integración)
Endpoint GET /health/certificate-status
- URL:
https://api.nvito.mx/v1/health/certificate-status - Publico: Si, sin autenticación
- Respuesta: JSON con
status,daysUntilExpiry,warning,fingerprint256 - Uso: Configurar UptimeRobot para verificar cada 6 horas
Endpoint POST /health/pinning-report
- URL:
https://api.nvito.mx/v1/health/pinning-report - Publico: Si, rate limited (10 req/min por IP)
- Uso: nvito-client envia reportes automáticos cuando el pinning falla
UptimeRobot (configuración manual)
- Crear monitor HTTP(S) keyword en UptimeRobot (plan gratuito)
- URL:
https://api.nvito.mx/v1/health/certificate-status - Keyword:
"warning":true(alertar si aparece) - Frecuencia: cada 6 horas
- Notificación: email del admin
Circuit breaker
Como funciona
- Cada fallo de pinning incrementa un contador
- Al llegar a 3 fallos consecutivos, el circuit breaker se abre
- Con circuit breaker abierto:
secureFetchusa fetch nativo (sin pinning) - Cada fallo envia telemetria a
/health/pinning-report - El circuit breaker se resetea al reiniciar la app
Implicaciones
- La app nunca deja de funcionar por fallos de pinning
- El equipo recibe alertas via telemetria para actuar rápido
- Mientras el circuit breaker está abierto, la app opera sin protección de pinning (temporalmente)
Escenarios catastroficos y pasos de emergencia
Escenario A: Certificado se renovo sin rotar pins
Diagnostico: Usuarios reportan errores de conexión. Telemetria muestra fallos de pinning.
Tiempo estimado de resolución: 2-4 horas
Pasos:
- El circuit breaker se activa automáticamente — la app sigue funcionando
- Obtener el nuevo pin del certificado actual:
openssl s_client -connect api.nvito.mx:443 ... - Actualizar
certificate-pinning.tscon el pin nuevo - Publicar OTA:
eas update --branch production - En 24h el 95% de usuarios tienen el fix
Escenario B: Intermediate CA cambio
Diagnostico: Fallos de pinning masivos. Ni el leaf ni el intermediate pin coinciden.
Tiempo estimado de resolución: 2-4 horas
Pasos:
- Circuit breaker protege a los usuarios
- Obtener AMBOS pins nuevos (leaf + intermediate): ejecutar los dos comandos
openssl - Actualizar
certificate-pinning.tscon los 2 pins nuevos - Publicar OTA:
eas update --branch production
Escenario C: App rechaza TODAS las conexiónes
Diagnostico: La app no puede comunicarse con el API. El circuit breaker NO se activo (posiblemente por un bug).
Tiempo estimado de resolución: 4-8 horas
Pasos:
- Publicar OTA de emergencia desactivando pinning temporalmente:
- En
certificate-pinning.ts: cambiar pins a['PLACEHOLDER']para forzar fetch nativo eas update --branch production
- En
- Investigar por que el circuit breaker no se activo
- Corregir el bug, restaurar pins correctos, publicar otro OTA
Escenario D: No se puede publicar OTA
Diagnostico: El servidor de EAS Update no responde o hay problema con la cuenta de Expo.
Tiempo estimado de resolución: 24-48 horas
Pasos:
- El circuit breaker protege a los usuarios existentes
- Publicar release de emergencia en App Store y Play Store con pins corregidos
- Activar forced update si está disponible
- Resolver el problema con EAS para futuras actualizaciones
Verificacion manual
Verificar que los pins son correctos
# Obtener pin actual del servidor
CURRENT_PIN=$(openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
openssl enc -base64)
echo "Pin actual del servidor: sha256/$CURRENT_PIN"
echo "Comparar con los pins en certificate-pinning.ts"
Verificar endpoint de status
curl -s https://api.nvito.mx/v1/health/certificate-status | jq .
Respuestá esperada:
{
"status": "ok",
"hostname": "api.nvito.mx",
"daysUntilExpiry": 75,
"warning": false,
"fingerprint256": "AA:BB:CC:..."
}
Probar pinning en dispositivo
- Build de desarrollo:
npx expo run:iosonpx expo run:android - Verificar que las llamadas al API funcionan
- En producción: build con
eas build --platform all --profile production
Checklist para despliegue a Producción
Acción requerida al desplegar a producción
Los ambientes DEV y TEST ya tienen certificate pinning configurado y funcional. Cuando el ambiente de producción este desplegado, se deben completar los siguientes pasos para replicar la protección. Hasta que no se completen, producción operara sin pinning (fetch nativo via placeholders).
-
Obtener pins del certificado de producción:
# Leaf certificate openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \ openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | \ openssl dgst -sha256 -binary | openssl enc -base64 # Intermediate CA openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \ awk '/-----BEGIN CERTIFICATE-----/{n++} n==2' | \ openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | \ openssl dgst -sha256 -binary | openssl enc -base64 -
Reemplazar placeholders en
nvito-client/src/api/certificate-pinning.ts:- Buscar
PLACEHOLDER_LEAF_CERTIFICATE_PIN_BASE64y reemplazar con el pin del leaf - Buscar
PLACEHOLDER_INTERMEDIATE_CA_PIN_BASE64y reemplazar con el pin del intermediate CA
- Buscar
-
Configurar variables de entorno en Railway (nvito-api):
CERTIFICATE_CHECK_HOST=api.nvito.mxCERTIFICATE_CHECK_PORT=443CERTIFICATE_ALERT_EMAIL= email del administrador
-
Instalar
react-native-ssl-pinningy descomentar la implementación real ensecureFetch(requiere EAS Build nativo) -
Publicar OTA:
eas update --branch production --message "Enable SSL pinning for production" -
Configurar UptimeRobot (gratuito):
- Monitor HTTP(S) keyword en
https://api.nvito.mx/v1/health/certificate-status - Keyword de alerta:
"warning":true - Frecuencia: cada 6 horas
- Notificación: email del admin
- Monitor HTTP(S) keyword en
-
Verificar que el endpoint responde correctamente:
curl -s https://api.nvito.mx/v1/health/certificate-status | jq .
FAQ
Se puede desactivar el pinning temporalmente?
Si. Cambiar los pins a strings con PLACEHOLDER en certificate-pinning.ts y publicar OTA. El código detecta placeholders y usa fetch nativo.
Que pasa si no configuro CERTIFICATE_CHECK_HOST?
El cron semanal se ejecuta pero retorna not_configured sin hacer nada. El endpoint certificate-status también retorna not_configured. No hay impacto negativo.
Cada cuanto se renuevan los certificados?
Depende del proveedor. Let's Encrypt renueva cada 90 días. Cloudflare varia. El cron alerta 30 días antes.
El pinning afecta el desarrollo local?
No. En desarrollo (__DEV__ = true), secureFetch usa fetch nativo automáticamente. No se validan pins.
Que pasa con el upload a S3 (URLs pre-firmadas)?
El upload de archivos usa fetch nativo directamente porque las URLs pre-firmadas son de dominios dinámicos (CDN). El pinning solo aplica a api.nvito.mx.
Como actualizo los pins cuando no tengo producción?
Los pins tienen valor PLACEHOLDER por defecto. Cuando despliegues a producción, ejecuta los comandos openssl, reemplaza los placeholders, y pública un OTA.