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

Для комплексного обзора основ мобильной безопасности смотрите наше руководство по Тестированию Мобильной Безопасности. Тестирование certificate pinning тесно связано с общими практиками тестирования безопасности API, где SSL/TLS валидация играет ключевую роль. Также рекомендуем изучить документирование тестов безопасности для правильного оформления результатов проверок pinning.

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 значительно улучшает безопасность мобильных приложений.

Смотрите также