Перейти к основному содержанию
Блог

Мобильные

SSL pinning: закрепление сертификата в мобильном приложении

Если приложение принимает любой сертификат, которому доверяет система, трафик можно перехватить. Как настроить pinning на Android и во Flutter и подготовиться к смене сертификата.

· 7 мин. чтения

Фото: Lacz02, CC BY-SA 4.0 · Wikimedia Commons

В экспертизе нашего собственного приложения одной из находок высокого уровня для 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 — самому отключить своих пользователей. Чтобы этого не случилось:

  1. Закрепляйтесь на корневом CA, а не на leaf, если вы не контролируете ключ leaf-сертификата строго сами.
  2. Добавьте резервный пин — другой CA или заранее подготовленный резервный ключ.
  3. Планируйте смену хостинга или CA заранее. Сначала добавьте пин нового CA в новую версию приложения, дождитесь, пока пользователи обновятся, и только потом меняйте сертификат на сервере.
  4. Следите за датой expiration и обновляйте её с каждым крупным релизом.
  5. Учитывайте все домены — API, файловый сервер, аналитику. Если pinning есть только на основном домене, остальные остаются открытыми.

Проверьте сами

Установите на тестовое устройство CA прокси-инструмента (например, Burp Suite или mitmproxy) и запустите приложение. Правильно настроенное приложение не должно подключиться к серверу, а в прокси не должно быть видно расшифрованного трафика. Затем проверьте, что в обычной сети всё работает.

Помните, что pinning можно отключить через Frida. Поэтому он эффективен в связке с RASP и обнаружением root.

Вывод

SSL pinning — это несколько строк конфигурации, но при неправильной настройке он может остановить приложение. Правильный подход: закрепление на корневых CA, резервный пин, отдельная настройка Android и Dart и заранее спланированная смена сертификата.

Если хотите узнать, можно ли перехватить трафик вашего приложения, оставьте заявку на сканирование. Мы проверим, покажем уязвимости и поможем подготовиться к экспертизе.