Реалізація закріплення сертифіката в мобільних додатках
Додатки перехоплюються через Charles Proxy або Frida — запити читаються, відповіді змінюються, API вивчаються. Це відбувається, оскільки TLS-з'єднання перевіряє сертифікат відносно системних ЦС, а користувач (або пентестер) просто додав свою ЦС до довірених. Закріплення сертифіката закриває цей вектор: клієнт приймає лише конкретний сертифікат або ключ, незалежно від системного сховища ЦС.
Закріплення сертифіката vs закріплення публічного ключа
Закріплення сертифіката — клієнт зберігає повний сертифікат (або його SHA-256 хеш) та порівнює з тим, що надіслав сервер. Простий у реалізації, але сертифікат змінюється при кожному оновленні (зазвичай кожні 1–2 роки). Якщо ви забули оновити pin до ротації — додаток перестає працювати у всіх користувачів одночасно.
Закріплення публічного ключа (HPKP-стиль) — закріплення хешу SubjectPublicKeyInfo. При ротації сертифіката приватний ключ залишається тим же, pin залишається дійсним. Це правильний вибір для продакшену. Додатково завжди тримайте резервний pin — хеш резервного ключа (може бути у вашої ЦС), щоб при компрометації основного ключа не залишитися без можливості оновити клієнтів.
iOS: NSURLSession + TrustKit
Нативний спосіб через URLSessionDelegate:
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Витягуємо публічний ключ із сертифіката сервера
guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0),
let serverKey = SecCertificateCopyKey(serverCert) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let serverKeyData = SecKeyCopyExternalRepresentation(serverKey, nil)! as Data
let serverKeyHash = sha256(data: serverKeyData)
let pinnedHashes = ["base64encodedSHA256ofSubjectPublicKeyInfo=="]
if pinnedHashes.contains(serverKeyHash) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Для проектів з кількома хостами зручна TrustKit — налаштовується через Info.plist або словник, підтримує резервні pins, звітування про порушення на endpoint.
Android: OkHttp CertificatePinner
У більшості Android-проектів мережа йде через OkHttp:
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAA...primaryKeyHash==")
.add("api.example.com", "sha256/BBBBBBBBB...backupKeyHash==")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
OkHttp самостійно обчислює SHA-256 SubjectPublicKeyInfo та порівнює. При невідповідності — SSLPeerUnverifiedException. Хеші можна отримати через openssl:
openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
Для Flutter — пакет http_certificate_pinning або нативна реалізація через platform channels, оскільки dart:io HttpClient не підтримує користувацьку валідацію на рівні публічного ключа без обгорток.
Проблеми з оновленням pins та Debug-сборки
Головний операційний біль — ротація ключів. За 30+ днів до заміни сертифіката потрібно випустити оновлення з новим резервним pin у списку, дати користувачам час оновитися, потім ротувати ключ. Це вимагає координації між інфраструктурою та мобільною командою.
У debug/QA-сборках pinning зазвичай вимикається, щоб Charles/Proxyman працював для тестувальників. Робиться через флаг BuildConfig.DEBUG або окремий flavor без pinning-конфіга. Переконайтеся, що release-сборка в CI збирається з pinning — інакше втратите захист непомітно.
Обхід Frida та root
Закріплення сертифіката не захищає від атак на рутованих пристроях — Frida може зацепити SSLContext або методи порівняння хешів. Це окреме завдання (root detection + runtime integrity check). Pinning захищає від proxy на не-рутованих пристроях, закриваючи більшість користувацьких сценаріїв перехвату.
Терміни
Налаштування pinning на одному хості з правильною схемою резервних pins — 2–3 дні з урахуванням тестування, налаштування CI для release-сборки та документування процедури ротації ключів.







