Мобильные
SSL pinning: закрепление сертификата в мобильном приложении
Если приложение принимает любой сертификат, которому доверяет система, трафик можно перехватить. Как настроить pinning на Android и во Flutter и подготовиться к смене сертификата.
· 7 мин. чтения

В экспертизе нашего собственного приложения одной из находок высокого уровня для Android была «небезопасная проверка сертификатов». Приложение использовало HTTPS, сертификат был настоящим. Проблема была в другом: приложение принимало любой удостоверяющий центр (CA), которому доверяет устройство. В этой статье разберём, почему этого недостаточно, как работает SSL pinning и как внедрить его так, чтобы не сломать приложение при смене сертификата.
Проблема: HTTPS есть, но доверие слишком широкое
Обычно при подключении приложения к серверу система проверяет цепочку сертификатов: выдан ли сертификат одним из доверенных CA на устройстве? В этом списке десятки CA. Кроме того, пользователь или другой человек с доступом к устройству может добавить свой CA.
Именно это и проверяет эксперт: устанавливает на устройство CA своего прокси-инструмента и пытается расшифровать трафик приложения. Если приложение ничего не замечает и продолжает работать, логины, токены и финансовые данные видны в открытом виде. Это относится к группе NETWORK в MASVS и к категории M5 (Insecure Communication) в OWASP Mobile Top 10 2024.
Что такое SSL pinning
Pinning — это указание приложению: «доверяй только этому конкретному ключу или сертификату». В приложении хранится хеш ожидаемого открытого ключа. Если при подключении ни один сертификат в цепочке сервера не совпадает с этим хешем, соединение разрывается — даже если система этому сертификату доверяет.
Обычно хешируется не сам сертификат, а его открытый ключ (SPKI). Сертификат может обновиться, а ключ остаться прежним, что делает пин более устойчивым.
Что закреплять: leaf, промежуточный или корневой CA
Это самое важное решение: неверный выбор может за один день вывести приложение из строя.
- Leaf (сертификат сервера) — самый строгий вариант. Но сертификат сервера регулярно обновляется. Например, сертификаты Let's Encrypt меняются каждые 90 дней. Если новый сертификат выйдет с новым ключом, все старые версии приложения перестанут подключаться к серверу.
- Промежуточный (intermediate) CA — средний вариант, но CA тоже периодически меняет промежуточные сертификаты.
- Корневой (root) CA — самый стабильный вариант. Корневые сертификаты не меняются годами.
В нашем приложении мы закрепились на конкретных выбранных корневых CA. Это полностью отсекает сторонние и прокси-CA, но не мешает обновлять сертификат сервера в обычном порядке.
Правило: всегда держите минимум два пина — основной и резервный. Резервный лучше взять от другого CA, тогда при проблеме с одним CA приложение продолжит работать.
Android: network_security_config
Начиная с Android 7.0 pinning можно настроить без кода, через XML. Файл res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.uz</domain>
<pin-set expiration="2027-12-31">
<!-- SHA-256 хеш открытого ключа основного корневого CA -->
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- резервный: другой корневой CA -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
И в AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false">
Android сравнивает пин с любым сертификатом в цепочке, поэтому хеш корневого CA тоже работает. Атрибут expiration задаёт срок действия пинов: после этой даты pinning отключается и используется обычная проверка. Это защищает от полной неработоспособности приложения на устройствах, где оно давно не обновлялось, но не забывайте вовремя продлевать дату.
Хеш открытого ключа корневого CA можно получить так:
openssl x509 -in root_ca.pem -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
Flutter: сторону Dart нужно настраивать отдельно
Это частая ошибка во Flutter-приложениях. HTTP-клиент Dart не использует системный сетевой стек Android, поэтому network_security_config на запросы из Dart не влияет. Настраивать нужно и то, и другое.
В Dart мы отключили системный список доверенных CA и загрузили только выбранные корневые:
final context = SecurityContext(withTrustedRoots: false);
context.setTrustedCertificatesBytes(rootCaPemBytes);
final client = HttpClient(context: context);
В этом случае, какой бы CA ни был установлен на устройство, клиент Dart доверяет только корневым сертификатам, поставляемым вместе с приложением. Если нужно несколько корней, их можно объединить в один PEM-файл.
Подготовка к смене сертификата
Главный риск pinning — самому отключить своих пользователей. Чтобы этого не случилось:
- Закрепляйтесь на корневом CA, а не на leaf, если вы не контролируете ключ leaf-сертификата строго сами.
- Добавьте резервный пин — другой CA или заранее подготовленный резервный ключ.
- Планируйте смену хостинга или CA заранее. Сначала добавьте пин нового CA в новую версию приложения, дождитесь, пока пользователи обновятся, и только потом меняйте сертификат на сервере.
- Следите за датой
expirationи обновляйте её с каждым крупным релизом. - Учитывайте все домены — API, файловый сервер, аналитику. Если pinning есть только на основном домене, остальные остаются открытыми.
Проверьте сами
Установите на тестовое устройство CA прокси-инструмента (например, Burp Suite или mitmproxy) и запустите приложение. Правильно настроенное приложение не должно подключиться к серверу, а в прокси не должно быть видно расшифрованного трафика. Затем проверьте, что в обычной сети всё работает.
Помните, что pinning можно отключить через Frida. Поэтому он эффективен в связке с RASP и обнаружением root.
Вывод
SSL pinning — это несколько строк конфигурации, но при неправильной настройке он может остановить приложение. Правильный подход: закрепление на корневых CA, резервный пин, отдельная настройка Android и Dart и заранее спланированная смена сертификата.
Если хотите узнать, можно ли перехватить трафик вашего приложения, оставьте заявку на сканирование. Мы проверим, покажем уязвимости и поможем подготовиться к экспертизе.


