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 连接全断,最后只能刷机恢复。
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 握手失败,查到最后发现是厂商把某根根证书标记成了不信任,但系统没有报错——只是默默拒绝。
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 留够扩展口。
5. 实战中的一个「坑」
讲个真实案例。我在做 SDK 内部的 SSL 握手自定义验证时,iOS 上全程用 SecTrustCreateWithCertificates + 自己构建的信任锚,一切正常。移植到 Android,我用了 TrustManagerFactory 默认实例,结果在小米手机上抛出 CertificateException: Chain validation failed。排查后发现:小米在 /system/etc/security/cacerts/ 中包含了自签名测试证书(可能来自 CTS 测试),而我的 TrustManager 直接信任了这些证书,但服务端证书链里恰好引用了一个被小米标记为「不推荐」的中间 CA。
解决方案很简单:像 iOS 一样,显式构造自己的信任锚集。我写了个 PKIXRevocationChecker 加上白名单过滤,才把问题解决。所以你看,Android 的开放性既是优点也是隐患,SDK 必须做防御性编程。