~/posts/mobile/android-conscrypt-roots-mount-namespaces.md

Android 证书信任:模块、目录与进程视图

结合 Pixel 3a 记录与固定版本源码,区分 Conscrypt 和 framework 的证书目录选择逻辑,再沿活动模块、挂载命名空间、子挂载与禁用标记定位应用信任差异。

date[31:24]
read[23:16]
9 分钟
cat[15:8]
移动端
目录
  1. 0x00记录两条更新链
  2. 0x01先识别目录选择类
  3. 0x02目录存在不等于启用
  4. 0x03在目标进程中检查挂载
  5. 0x04普通绑定可能漏掉子挂载
  6. 0x05禁用标记与证书缓存
  7. 0x06形成可复核的验证闭环
  8. 0x07参考资料

给 Android 测试机装入代理 CA 后,浏览器访问正常、应用却握手失败,首先要查的是:目标进程究竟从哪里读取信任锚?系统大版本、Conscrypt 模块、证书源实现和挂载命名空间,共同决定了这个答案。

下面以 2025 年 6 月前的 Pixel 3a 记录为线索,对照固定版本源码,拆开“模块带有证书”“进程看得见证书”和“应用接受证书”三个不同条件。设备记录不是所有同版本手机的兼容性承诺。

记录两条更新链

Mainline 允许部分系统组件独立于完整 OTA 更新。只记“Android 13”,会漏掉 Google Play 系统更新(GPSU)和活动 Conscrypt 版本。设置页中的三个字段就可能来自不同时间点:

设置页中的版本信息3 行
字段 记录值 用途
Android 版本 13 平台大版本
Android 安全更新 2023-09-01 系统补丁级别
Google Play 系统更新 2025-04-01 Mainline 更新日期

这组设置页信息与下文 Pixel 3a 的跨版本记录分开保存,不将两者拼成同一设备基线。设备侧还应采集以下只读信息;读取权限随构建而异:

Device baselinesh
adb shell getprop ro.build.fingerprint
adb shell getprop ro.build.version.sdk
adb shell ls -ld /apex/com.android.conscrypt*
adb shell cat /apex/apex-info-list.xml

模块记录要看 isActive,而不只是目录里最新的文件名。下面是活动项的字段节选:

apex-info-list.xml · recorded fieldsxml
<apex-info
    moduleName="com.android.conscrypt"
    modulePath="/data/apex/decompressed/com.android.conscrypt@331411000.decompressed.apex"
    preinstalledModulePath="/system/apex/com.google.android.conscrypt.apex"
    versionCode="331411000"
    isFactory="true"
    isActive="true" />

isFactory="true" 提醒我们:位于 /data/apex/decompressed 不等于已经安装了后续更新。压缩的预装 APEX 也会解压到该位置;需要结合活动标志、预装路径和版本判断。这个行为由 APEX 启动与解压机制解释。

先识别目录选择类

传统路径通常是 /system/etc/security/cacerts;模块路径是 /apex/com.android.conscrypt/cacerts。但“哪个存在就读哪个”并不完整,甚至两个相似的选择函数也属于不同层。

固定源码中的两条选择路径2 行
类与版本 显式 SDK 门槛 APEX 目录被选择的其他条件
Conscrypt TrustedCertificateStore,提交 a1971d3 成功取得 SDK,且 ≥ 34 Java 属性不是字符串 true;目录存在且非空
Android framework SystemCertificateSource,android-14.0.0_r1 该函数内没有 Java 属性不是字符串 true;目录存在且非空

这里比较的是不同类和源码树,不是声称两个函数是同一个模块的前后版本。framework 类随平台交付;安装一个新 Conscrypt APEX,也不代表它同时替换了 framework 的实现。

对可正常列举的目录,Conscrypt 的关键条件可整理为以下等价示意。真实源码通过 getSdkVersion() 获取运行时 SDK:

Conscrypt selection · logical equivalentjava
boolean useApex(Integer sdk, String property, boolean exists, int entries) {
    return sdk != null
        && sdk >= 34
        && !"true".equals(property)
        && exists
        && entries > 0;
}

Java 的 System.getProperty("system.certs.enabled") 与 Android 属性服务是不同机制。在 shell 中设置同名 setprop 或 resetprop 值,不足以证明这里读到的 Java 属性已经变化。源码做的是精确字符串比较,"True" 与 "true" 也不是同一输入。

检查设备时应确定实际执行的是哪一个类、来自哪个构建,再读取它的条件。对应实现见 Conscrypt 固定提交与 Android 14 framework 证书源。

目录存在不等于启用

Pixel 3a 的记录把模块布局与运行时选择分开了:

设备记录:系统与模块分开比较4 行
环境 Conscrypt 版本 观察
Android 11,RP1A.200720.009 300900703 APEX 内没有 cacerts;目标更新未成功取得
Android 12,SP1A.210812.015,更新前 310727000 APEX 内没有 cacerts
同一 Android 12,GPSU 显示 2025-04-01 351412000 APEX 内出现 cacerts
Android 11,RQ3A.211001.001,手动安装该模块后 351412000 目录存在;记录中的选择逻辑仍指向传统路径

这些观察支持“新布局可以出现在较旧平台上”,却没有证明 Android 11、12 会普遍启用模块根证书。Android 13 还有一项依赖 APEX 目录的记录,但旧模块版本未留存,也未完成同版本复现,因此不据此扩大结论。

版本 351412000 的包元数据摘录如下。包名是 com.google.android.conscrypt,与 APEX 模块名 com.android.conscrypt 不同:

Package metadata · relevant fieldsxml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.conscrypt"
    android:versionCode="351412000">
    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="35" />
    <application android:hasCode="false" />
</manifest>

minSdkVersion="30" 描述包兼容性下限,不会取消内部根目录选择函数的 SDK 34 门槛;hasCode="false" 也不表示整个 APEX 不含 JAR 或原生库。这里展示的是安装包元数据,而不是 APEX 载荷的完整清单。

跨设备复制或回滚模块还受签名、架构和系统接受条件约束。staged 安装应在重启后重新核对活动版本;一次把文件按 .apk 处理所触发的 INSTALL_FAILED_DUPLICATE_PACKAGE,也不是所有非 staged 安装行为的证明。

在目标进程中检查挂载

确认实现会选择 APEX 路径后,才进入文件可见性这一层。下面的图是排查顺序,不是一次完整 TLS 调用栈:

flowchart TD
  accTitle: 从实际实现走到应用信任结果
  accDescr: 先确定证书源,再检查目标命名空间内的文件,最后检查用户禁用状态与应用策略。
  A["实际执行的证书源类"] --> B["选定系统或 APEX 路径"]
  B --> C["目标进程挂载命名空间"]
  C --> D["可见文件与子挂载"]
  D --> E["用户证书、禁用标记与缓存"]
  E --> F["应用信任配置与额外校验"]

根管理模块可以先合并证书并覆盖传统目录,再把该目录绑定到 APEX 路径。然而当前 shell 的挂载视图,不是所有应用的视图。挂载传播属性、zygote 的状态,以及应用进程创建时机都应分别记录。

以下命令是在设备端 shell 中使用的只读检查模板。PID 应替换为已核验的目标进程号;权限和 nsenter 支持情况需要先确认:

Target namespace · inspection templatesh
readlink /proc/PID/ns/mnt
cat /proc/PID/mountinfo
nsenter --mount=/proc/PID/ns/mnt -- \
  ls -l /apex/com.android.conscrypt/cacerts

只修改 zygote 后,应分别检查新建应用和已存在的进程;只重启应用,也应记录它是否仍由预期的 zygote 派生。不要将“路径在 shell 中存在”直接写成“应用已使用这些证书”。

普通绑定可能漏掉子挂载

Android 15 的一项记录中,系统证书文件本身就是独立子挂载。相关路径包括 bf64f35b.0、5acf816d.0 和 d41b5e2a.0,均位于 /system/etc/security/cacerts 下。这里保留文件名用于对应挂载记录,不据此推断证书主体。

证书界面的可见结果2 行
状态 设置界面观察 仍需检查
正常视图 系统页显示多个证书条目 目标应用实际读取的路径
异常视图 系统页未显示证书条目 文件内容、子挂载、读取权限与缓存

空白界面仅证明条目未被显示,不等于设备上已安装证书总数为零。若源目录中的内容依赖子挂载,普通 --bind 不会递归复制这些挂载;--rbind 才处理整棵挂载子树,但其中的 unbindable 子树会被剪除。详见 Linux shared subtree 语义。

相应的操作形式如下,仅适用于已检查源目录与传播关系的测试设备;命令工具的位置和选项以设备实际提供的实现为准:

Recursive bind · device-specific templatesh
nsenter --mount=/proc/PID/ns/mnt -- \
  mount --rbind /system/etc/security/cacerts \
  /apex/com.android.conscrypt/cacerts

这是挂载子树问题的针对性修正,不是通用抓包开关。执行前保存目标命名空间的挂载基线,执行后逐项比较子挂载和实际文件;回滚也应恢复已记录的挂载层级,而不是反复叠加绑定。SELinux 标签、证书编码或应用独立信任库的问题仍需另查。

禁用标记与证书缓存

系统 CA 文件留在只读位置,并不妨碍用户禁用它。在固定 Conscrypt 实现中,deleteCertificateEntry() 对系统条目写入删除记录目录,保存的是证书内容;getTrustAnchor() 对候选系统项检查删除记录。对用户新增条目的处理则是另一条分支。

framework 的 SystemCertificateSource 使用当前 Android 用户配置目录中的 cacerts-removed,并检查与候选 CA 文件同名的标记。UserCertificateSource 读取对应的 cacerts-added。用户 0 的常见路径如下,其他用户应按实际用户 ID 确认:

Per-user certificate state · user 0text
/data/misc/user/0/cacerts-added
/data/misc/user/0/cacerts-removed

不要把这些路径机械套到任意独立 Conscrypt 实例:固定提交的默认用户根目录是 $ANDROID_DATA/misc/keychain,并提供 setDefaultUserDirectory()。最终路径取决于调用方配置。

缓存也属于证据链。Android 14 的 DirectoryCertificateSource 缓存枚举结果,handleTrustStorageUpdate() 清空该集合,但不会因此重新选择构造时的目录。排查时区分清空证书集合与重建证书源对象,避免把一次路径或属性修改当成所有旧对象立即更新。

形成可复核的验证闭环

离线检查使用 javac 13-ea:提取固定提交的 shouldUseApex(),仅替换 SDK 查询入口;framework 函数只将路径重定向到本地夹具。测试覆盖 5 种 SDK 输入、4 种属性值与 3 种目录状态;可列举的非空目录中放置标记文件,不模拟有效证书。

Offline selector checkslog
PASS Conscrypt source method: 60 cases
PASS framework source method with redirected fixture paths: 12 cases

这是选择分支验证,不是重新运行 Pixel 设备、挂载修改或 TLS 握手。设备上的最终检查应继续落实到应用:

从版本到连接结果4 步
  1. 1

    冻结基线

    保存构建号、SDK、GPSU 日期、活动 APEX、实际类来源与进程启动时间。

  2. 2

    确认文件视图

    在目标命名空间内比较证书指纹、目录与子挂载,并记录权限和用户 ID。

  3. 3

    核对信任状态

    区分系统根、用户新增项、禁用记录和已缓存对象;重启前后分别记录。

  4. 4

    验证相同请求

    固定主机名、证书链与应用输入,对比调整前后结果,再恢复基线验证原行为。

现代应用是否信任用户 CA,要看目标 API 和 Network Security Configuration;自定义 TrustManager、原生网络栈和证书固定又可能增加额外条件。能修改自有应用时,调试构建中明确配置所需信任锚,比依赖整机挂载变化更可控。目录可见性是必要排查项,而不是最终信任结果。

参考资料

NORMAL~/posts/mobile/android-conscrypt-roots-mount-namespaces.md§--
0%zh-CN