iOS 与 Android PKI 架构差异:系统证书存储、信任锚、密钥链与 KeyStore

做跨平台 PKI SDK 兼容改造,第一关就是摸清两边的 PKI 基础设施到底长啥样。说白了,iOS 和 Android 虽然都叫移动端,但底层对证书、密钥、信任链的管理完全是两套思路。我自己在好几个项目中都踩过这个坑——明明在 iOS 上跑得好好的,移植到 Android 就直接崩,不是证书找不到,就是私钥读不出来。

这一章我们就从三个最核心的维度拆开看:系统证书存储、信任锚、密钥链与 KeyStore。把这几个点理清楚,后面做 SDK 兼容改造才不会跑偏。

1. 系统证书存储:一棵树 vs 一个目录

iOS 的证书存储是个黑盒。系统把所有根证书、中间 CA 证书、自签证书全部集中管理,你没法直接拿到证书文件路径,只能通过 SecTrustStore 或 SecCertificate 这类 API 去间接查询。我最初接手一个 VPN 项目时,想直接在 iOS 上导出一个自签 CA 证书,结果发现根本不存在「文件系统里的某个 .cer 文件」——它只在 Security.framework 的私有数据库里,连路径都不暴露。

Android 就开放很多。系统证书存在 /system/etc/security/cacerts/ 目录下,每个证书是一个 .0 后缀的哈希名文件。用户添加的证书在 /data/misc/user/0/cacerts-added/。你可以直接读文件、列目录,甚至用 adb pull 拉出来。但开放也意味着风险——我记得有个 App 因为误删了系统证书目录里的某个文件,导致整个 HTTPS 连接全断,最后只能刷机恢复。

我的经验:如果 SDK 需要在 Android 上扫描所有受信任证书做白名单校验,不要依赖 TrustManagerFactory 默认行为,它只返回系统证书。建议用 KeyStore 加载 AndroidCAStore 类型,才能拿到系统 + 用户证书的完整集合。

2. 信任锚:谁说了算?

信任锚(trust anchor),就是 PKI 链条里那个「无条件信任」的起点。俩平台的差异非常大。

维度 iOS Android
默认根证书来源 Apple 内置根证书(约 200+ 个,随系统更新) AOSP 内置 + 各厂商自加(约 150-250 个,碎片化严重)
用户自定义信任锚 通过 MDM 或描述文件安装,可设信任开关 通过 Settings > 安全 > 安装证书,无信任开关(Android 14 开始支持)
SSL 校验行为 默认只信任系统链,除非 App 自己调 SecTrustSetAnchorCertificates 默认信任系统 + 用户证书,但不同厂商可能魔改
证书吊销检查 OCSP + CRL,强制检查(可被代理拦截但默认开启) 各厂商实现不一致,部分机型不做在线检查

你看,iOS 是个「裁判员」,它帮你过滤掉绝大多数有问题的证书。Android 更像是个「裁判员 + 观众」,把用户证书也放进来,而且各手机厂商可能自己改一套 CA 列表。我曾在某款国产手机上遇到诡异的 SSL 握手失败,查到最后发现是厂商把某根根证书标记成了不信任,但系统没有报错——只是默默拒绝。

避坑指南:我曾经在 Android 上做 HostnameVerifier 自定义实现时,忘了把 TrustManager 初始化为 null(默认信任锚),结果华为手机直接抛了个 CertPathValidatorException,说根证书不受信任。后来发现华为在 Android 9 之后修改了默认 CA 列表,去掉了一些非主流 CA。所以做兼容时,一定要显式指定信任锚集,不要依赖默认行为。

3. 密钥链与 KeyStore:安全容器大不同

这是最让开发者头疼的部分。iOS 用 Keychain,Android 用 KeyStore,两者概念上都是「安全存储私钥和证书」的容器,但 API 哲学完全相反。

3.1 iOS Keychain

  • 基于 SQLite 数据库,整个系统只有一个 Keychain(by default)
  • 通过 kSecClass(如 kSecClassCertificate、kSecClassKey)区分存储类型
  • 访问权限靠 SecAccessControl 和 kSecAttrAccessible 控制,支持 Touch ID / Face ID 绑定
  • 应用共享:通过 Keychain Access Group 实现同一 TeamID 下的 App 共享

我做过一个企业级 PKI 客户端,需要在多个自研 App 之间共享同一个用户证书。iOS 上只需要在 Entitlements 里加上相同的 keychain-access-groups,几个 App 就能读写同一个条目。Android 呢?嗯,没那么简单。

3.2 Android KeyStore

  • 从 Android 4.3 开始引入,底层基于硬件(TEE/SE)或软件实现
  • 每个 App 有自己独立的 KeyStore 实例,不与其他 App 共享
  • 使用 KeyStore.Entry 和 KeyStore.SecretKeyEntry 等概念,通过 alias 操作
  • 支持强硬件绑定(如 Google Pixel 的 Titan M 芯片)
  • Android 9 之后支持 KeyStore.getCertificateChain() 返回完整链

关键差异来了:iOS 的 Keychain 默认是进程级共享的(同一开发团队),而 Android 的 KeyStore 默认是应用级隔离的。你想在 Android 上让两个 App 共享一个私钥?只能通过 KeyChain 系统服务(注意大小写),或者用 ContentProvider 搞序列化传输——但私钥一出 KeyStore 就失去硬件保护,安全大打折扣。

我的建议:如果你在做跨平台 SDK,设计密钥存储模块时最好抽象成两层:上层是「逻辑 Store」,统一增删改查接口;下层是「平台实现」,iOS 用 Keychain,Android 用 KeyStore + 可选 KeyChain。千万别试图用数据库或者 SharedPreferences 存私钥——那样跟把密码写在便利贴上没区别。

4. 一张图看懂整体差异

下面这张 SVG 图把三个维度的关系串起来了。你可以看到,iOS 偏向封闭、统一、由系统主导;Android 偏向开放、碎片、由开发者灵活配置。做兼容改造时,我的原则是:往 iOS 的「紧」方向靠,再给 Android 留够扩展口。

iOS 与 Android PKI 架构差异总览 iOS 平台 证书存储:Security.framework 私有数据库 不可直接访问文件路径,仅通过 Sec API 操作 系统根证书随版本更新,用户证书受 MDM 管控 信任锚:Apple 内置根列表 + 描述文件 SSL 默认只信任系统链;开发者可追加锚 密钥链(Keychain) 单个系统级安全数据库 支持 TeamID 群组共享,可选生物识别绑定 Android 平台 证书存储:文件系统目录 /system/etc/security/cacerts/ + 用户目录 哈希命名的 .0 文件,可直接读写 信任锚:AOSP 内置 + 厂商定制 + 用户证书 默认信任用户证书;碎片化严重 KeyStore(应用隔离) 每个 App 独立实例,不与其他 App 共享 硬件绑定(TEE/SE),需显式使用 KeyChain 系统服务共享 对比 对比 对比 核心原则:iOS 封闭可控 → SDK 适配需模拟其统一行为 Android 开放碎片 → SDK 需抽象平台层并提供厂商兼容覆盖

5. 实战中的一个「坑」

讲个真实案例。我在做 SDK 内部的 SSL 握手自定义验证时,iOS 上全程用 SecTrustCreateWithCertificates + 自己构建的信任锚,一切正常。移植到 Android,我用了 TrustManagerFactory 默认实例,结果在小米手机上抛出 CertificateException: Chain validation failed。排查后发现:小米在 /system/etc/security/cacerts/ 中包含了自签名测试证书(可能来自 CTS 测试),而我的 TrustManager 直接信任了这些证书,但服务端证书链里恰好引用了一个被小米标记为「不推荐」的中间 CA。

解决方案很简单:像 iOS 一样,显式构造自己的信任锚集。我写了个 PKIXRevocationChecker 加上白名单过滤,才把问题解决。所以你看,Android 的开放性既是优点也是隐患,SDK 必须做防御性编程。

总结一下:做 PKI SDK 兼容改造,就是「补平台差异的课」。iOS 帮你做了大部分安全决策,你只需要在它划定的框架里做事;Android 把选择权交给你,但你必须自己处理碎片和边界情况。下一节我们会深入代码层面,看如何用 C 层抽象实现平台无关的证书链验证,到时候你会发现,今天讲的这些架构差异全都会体现在代码里。