El certificate pinning fortalece la seguridad SSL/TLS validando que el certificado del servidor coincida con un certificado conocido y confiable. Esto previene ataques man-in-the-middle (MITM) incluso cuando el trust store del dispositivo está comprometido.

Para una visión completa de los fundamentos de seguridad móvil, consulta nuestra guía de Testing de Seguridad Móvil.

Android Certificate Pinning

OkHttp CertificatePinner

import okhttp3.CertificatePinner

val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // Backup pin
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

Generando Pins de Certificados

# Extraer certificado del servidor
openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null \
    | openssl x509 -outform DER > cert.der

# Generar hash SHA-256
openssl x509 -in cert.der -inform DER -pubkey -noout \
    | openssl pkey -pubin -outform DER \
    | openssl dgst -sha256 -binary \
    | base64

Testing Certificate Pinning

class CertificatePinningTest {

    @Test
    fun testValidCertificate() = runTest {
        val client = createClientWithPinning()

        val response = client.newCall(
            Request.Builder()
                .url("https://api.example.com/users")
                .build()
        ).execute()

        assertTrue(response.isSuccessful)
    }

    @Test
    fun testInvalidCertificate() = runTest {
        val client = createClientWithPinning()

        assertThrows<SSLPeerUnverifiedException> {
            client.newCall(
                Request.Builder()
                    .url("https://evil.example.com/users")
                    .build()
            ).execute()
        }
    }

    @Test
    fun testMITMAttempt() = runTest {
        val proxyClient = OkHttpClient.Builder()
            .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", 8888)))
            .certificatePinner(certificatePinner)
            .build()

        assertThrows<SSLHandshakeException> {
            proxyClient.newCall(
                Request.Builder()
                    .url("https://api.example.com/users")
                    .build()
            ).execute()
        }
    }
}

Public Key Pinning

class PublicKeyPinning : CertificatePinner {
    private val pinnedPublicKeys = setOf(
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    )

    override fun check(hostname: String, peerCertificates: List<Certificate>) {
        val publicKeyHashes = peerCertificates.map { cert ->
            sha256(cert.publicKey.encoded).base64()
        }

        if (publicKeyHashes.none { it in pinnedPublicKeys }) {
            throw SSLPeerUnverifiedException("Certificate pinning failure!")
        }
    }
}

Estrategia de Rotación de Pins

Pins de Respaldo

val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com",
        "sha256/PRIMARY_PIN_HASH")
    .add("api.example.com",
        "sha256/BACKUP_PIN_HASH_1")
    .add("api.example.com",
        "sha256/BACKUP_PIN_HASH_2")
    .build()

Configuración Remota de Pins

class DynamicPinningManager(
    private val configService: ConfigService,
    private val secureStorage: SecureStorage
) {
    suspend fun updatePins() {
        val newPins = configService.getCertificatePins()

        if (validatePins(newPins)) {
            secureStorage.savePins(newPins)
        }
    }
}

@Test
fun testPinRotation() = runTest {
    val manager = DynamicPinningManager(mockConfigService, storage)

    mockConfigService.respondWith(mapOf(
        "api.example.com" to setOf("OLD_PIN", "NEW_PIN")
    ))

    manager.updatePins()

    val client = manager.createClient()
    assertTrue(client.certificatePinner.pins.size == 2)
}

Testing de Detección MITM

La detección MITM es crítica para la seguridad móvil. Aprende técnicas avanzadas de testing de seguridad en nuestra guía de Fundamentos de Penetration Testing.

Detección de Bypass

class SecurityChecker {
    fun detectRootAndBypassTools(): SecurityStatus {
        val issues = mutableListOf<String>()

        if (isDeviceRooted()) {
            issues.add("Dispositivo rooteado")
        }

        if (isFridaRunning()) {
            issues.add("Frida detectado")
        }

        return if (issues.isEmpty()) {
            SecurityStatus.Secure
        } else {
            SecurityStatus.Compromised(issues)
        }
    }
}

Mejores Prácticas

  1. Siempre usa pins de respaldo - Incluye 2-3 pins para rotación
  2. Pin a claves públicas, no certificados - Más flexible para rotación
  3. Monitorea expiración de pins - Configura alertas antes de que expiren
  4. Prueba pinning en CI/CD - Detecta mala configuración temprano
  5. Implementa actualizaciones remotas - Para rotación de emergencia
  6. Combina con otras medidas de seguridad - Detección root, validación SSL

El certificate pinning funciona mejor cuando se combina con testing de OAuth 2.0 y JWT para seguridad completa de autenticación. Para ingenieros QA que buscan expandir conocimientos de seguridad, consulta Testing de Seguridad para QA.

Conclusión

El testing de certificate pinning asegura:

  • Prevención de ataques MITM
  • Validación apropiada SSL/TLS
  • Rotación graceful de pins
  • Detección de intentos de bypass

El certificate pinning correctamente implementado y probado mejora significativamente la seguridad de apps móviles.