Реализация Certificate Pinning в мобильном приложении
Приложение перехватывают через Charles Proxy или Frida — читают запросы, модифицируют ответы, изучают API. Это происходит потому, что TLS-соединение валидирует сертификат относительно системных CA, а пользователь (или пентестер) просто добавил свой CA в доверенные. Certificate pinning закрывает этот вектор: клиент принимает только конкретный сертификат или ключ, независимо от системного хранилища CA.
Pinning сертификата vs pinning публичного ключа
Certificate pinning — клиент хранит полный сертификат (или его SHA-256 хэш) и сравнивает с тем, что прислал сервер. Просто реализуется, но сертификат меняется при каждом renewal (обычно каждые 1–2 года). Если забыли обновить пин до ротации — приложение перестаёт работать у всех пользователей одновременно.
Public key pinning (HPKP-стиль) — пинится хэш SubjectPublicKeyInfo. При ротации сертификата приватный ключ остаётся тем же, пин актуален. Это правильный выбор для продакшена. Дополнительно всегда держите backup pin — хэш резервного ключа (может быть у вашего CA), чтобы при компрометации основного ключа не остаться без возможности обновить клиентов.
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 или словарь, поддерживает backup pins, reporting violations на 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 не поддерживает кастомную валидацию на уровне публичного ключа без обёрток.
Проблемы с обновлением пинов и Debug-сборки
Главная операционная боль — ротация ключей. За 30+ дней до замены сертификата нужно выпустить обновление с новым backup pin в списке, дать пользователям обновиться, затем ротировать ключ. Это требует координации между командой инфраструктуры и мобильной.
В debug/QA-сборках pinning обычно отключают, чтобы Charles/Proxyman работал для тестировщиков. Делается через BuildConfig.DEBUG флаг или отдельный flavor без pinning-конфига. Убедитесь, что release-сборка в CI собирается именно с pinning — иначе теряете защиту незаметно.
Обход Frida и root
Certificate pinning не защищает от атак на рутованном устройстве — Frida может хукнуть SSLContext или методы сравнения хэшей. Это отдельная задача (root detection + runtime integrity check). Pinning защищает от proxy на нерутованных устройствах, что закрывает большинство пользовательских сценариев перехвата.
Сроки
Настройка pinning на одном хосте с правильной схемой backup-пинов — 2–3 дня с учётом тестирования, настройки CI для release-сборки и документирования процедуры ротации ключей.







