Android не доверяет сертификату прокси

Приложения под Android 7+ игнорируют пользовательские сертификаты. Как разрешить их отладочной сборке через network_security_config и чего это не починит.

Что это значит

Сертификат установлен, в настройках Android он виден, Chrome на телефоне работает, а приложение всё равно падает с SSLHandshakeException: Trust anchor for certification path not found. Trust anchor называют корневой сертификат, с которого начинается проверка пути (RFC 5280, пункт 6.1.1), и подходящего у приложения здесь нет: собранное под Android 7.0 (API 24) и выше, оно доверяет только системным центрам сертификации и игнорирует всё, что добавил пользователь. Так и задумано: это закрывает чтение трафика приложения для любого, у кого есть доступ к телефону.

Как исправить

Приложение должно само разрешить пользовательские сертификаты, причём только в отладочной сборке.

Создайте app/src/main/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
            <certificates src="system" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

Сошлитесь на него из манифеста:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... >

Весь фокус держится на <debug-overrides>. Блок работает только тогда, когда приложение собрано с android:debuggable="true". Gradle ставит этот флаг в debug-сборках и никогда не ставит в релизных, поэтому выкладывать такой файл безопасно.

Не заменяйте его обычным <base-config> с src="user". Он тоже сработает, но сработает и в релизе, а это ровно та дыра, которую Android закрыл.

Если нужен один хост, а не все:

<domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <trust-anchors>
        <certificates src="user" />
    </trust-anchors>
</domain-config>

Этот блок нельзя положить внутрь <debug-overrides>: там допустим только <trust-anchors>. Значит, <domain-config> действует и в релизной сборке. Чтобы его там не было, положите отдельную копию файла в app/src/debug/res/xml/, и тогда Gradle возьмёт её только для debug-варианта.

Ещё два момента, на которых спотыкаются:

  • Порог задаёт targetSdkVersion приложения, а не версия Android на телефоне.
  • Конфиг компилируется внутрь APK. После правки приложение надо пересобрать и переустановить, потому что перезапуск ничего не меняет.

Чего это не починит

Пиннинг сертификата. Приложение может проверять отпечаток само: CertificatePinner в OkHttp, <pin-set> в том же конфиге, собственный TrustManager. Тогда ничего из написанного выше не поможет, потому что приложение отвергнет сертификат, которому система уже доверяет. Пиннинг снимается в коде (убрать CertificatePinner в debug-варианте) или не снимается вообще.

Flutter. У Dart свой TLS-стек. Он игнорирует network_security_config и не смотрит на пользовательские сертификаты. Нужен SecurityContext с вашим корневым сертификатом или badCertificateCallback в отладочной сборке.

Чужое приложение. Исходников нет, значит конфиг добавить некуда. Остаются два пути: установить сертификат как системный (нужен root и модуль Magisk или KernelSU вроде cert-fixer или AlwaysTrustUserCerts) либо перепаковать APK через apk-mitm.

Android 14+. Системное хранилище сертификатов переехало из /system/etc/security/cacerts в APEX-модуль com.android.conscrypt, по пути /apex/com.android.conscrypt/cacerts, и доступно теперь только для чтения. Старые способы «подложить файл в /system» там не работают: нужен модуль, который умеет про путь в APEX. Пользовательские сертификаты никуда не переезжали.

Как увидеть это у себя

Когда приложение начнёт доверять сертификату, его запросы будут видны целиком: хост, путь, заголовки, тела запроса и ответа. До этого Solpuga показывает только соединение, которое открылось и сразу оборвалось. Это и значит, что network_security_config ещё не подхватился. Прокси на телефоне настраивается отдельно. Полная инструкция по настройке: Мобильные устройства.

Посмотреть в Solpuga

Рядом