Certificate pinning усиливает безопасность SSL/TLS, валидируя, что сертификат сервера совпадает с известным, доверенным сертификатом. Это предотвращает man-in-the-middle (MITM) атаки даже когда trust store устройства скомпрометирован.

Для комплексного обзора основ мобильной безопасности смотрите наше руководство по Тестированию Мобильной Безопасности.

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()

Генерация Certificate Pins

# Извлечь сертификат с сервера
openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null \
    | openssl x509 -outform DER > cert.der

# Сгенерировать SHA-256 хэш
openssl x509 -in cert.der -inform DER -pubkey -noout \
    | openssl pkey -pubin -outform DER \
    | openssl dgst -sha256 -binary \
    | base64

Тестирование 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!")
        }
    }
}

Стратегия Ротации Pin’ов

Резервные Pin’ы

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()

Удалённая Конфигурация Pin’ов

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)
}

Тестирование Обнаружения MITM

Обнаружение MITM критически важно для мобильной безопасности. Изучите продвинутые техники тестирования безопасности в нашем руководстве по Основам Penetration Testing.

Обнаружение Bypass

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

        if (isDeviceRooted()) {
            issues.add("Устройство с root")
        }

        if (isFridaRunning()) {
            issues.add("Frida обнаружена")
        }

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

Лучшие Практики

  1. Всегда используйте резервные pin’ы - Включайте 2-3 pin’а для ротации
  2. Pin к публичным ключам, не сертификатам - Более гибко для ротации
  3. Мониторьте истечение pin’ов - Настройте оповещения до истечения
  4. Тестируйте pinning в CI/CD - Обнаруживайте неправильную конфигурацию рано
  5. Реализуйте удалённые обновления - Для экстренной ротации
  6. Комбинируйте с другими мерами безопасности - Обнаружение root, валидация SSL

Certificate pinning работает лучше всего в сочетании с тестированием OAuth 2.0 и JWT для комплексной безопасности аутентификации. QA-инженерам, желающим расширить знания в области безопасности, рекомендуем Тестирование Безопасности для QA.

Заключение

Тестирование certificate pinning обеспечивает:

  • Предотвращение MITM атак
  • Правильную валидацию SSL/TLS
  • Graceful ротацию pin’ов
  • Обнаружение попыток bypass

Правильно реализованный и протестированный certificate pinning значительно улучшает безопасность мобильных приложений.