3. 原生PKI SDK对比:iOS Security框架 vs Android java.security / Conscrypt

做跨平台PKI改造,第一关就是吃透两边的原生SDK。iOS和Android虽然都支持X.509证书链验证、RSA/ECC密钥对生成、TLS握手这些基础PKI操作,但底层的架构理念和API设计思路完全不同。我当年第一次从iOS转做Android时,就被certificate chain的验证方式坑过——同一个证书链,iOS能通过,Android抛异常。说白了,两边对信任锚和路径构建的默认行为有差异。你想想看,如果不把这类差异摸透,兼容改造就是空中楼阁。

3.1 体系架构:谁管得宽,谁管得细

iOS Security框架是个相对封闭但一致性强的“黑箱”。它把底层加密、证书存储、策略判定都打包在了一起。你通过Security framework的API去验证一张证书链时,系统会自己决定信任锚、自己拼接路径,甚至自己缓存OCSP结果。我个人习惯把iOS PKI部分看作“管家式服务”——你给个证书,剩下的它办。

Android java.security / Conscrypt走的则是“乐高积木”风格。java.security定义了抽象的引擎类(如CertificateFactory、TrustManager),而Conscrypt作为Provider提供实际实现。路径构建、信任锚选择、撤销检查这些步骤,多数时候需要开发者自己组合。Android 7.0之前默认用Bouncy Castle,之后才切到Conscrypt,而且不同厂商的ROM可能对Provider做了裁剪。我曾在一台旧三星平板上发现Conscrypt的某些Provider方法返回空——这就是碎片化带来的头疼。

核心差异一句话:
iOS是“监工包办一切”,接口少但内部细节不透明;
Android是“给你工具自己干”,灵活但你需要懂更多底层逻辑。

来看张对比图,感受下两边的模块划分:

iOS与Android PKI SDK架构对比 iOS Security Framework SecCertificate / SecKey / SecIdentity 证书对象 / 密钥引用 / 身份绑定 SecTrust + SecPolicy 路径构建 / 信任评估 / 策略 SecTrustEvaluateAsyncWithError 异步链验证 / 自动OCSP / CRL 钥匙串 Keychain 封闭存储 Android java.security / Conscrypt CertificateFactory / KeyPairGenerator 抽象引擎类 + 约定SPI接口 TrustManager / KeyStore / CertPathBuilder 需手动组装 策略 / Provider / 信任锚 Conscrypt TrustManager / CertPathValidator Provider实现 / 路径验证 / 撤销扩展 Android KeyStore / 硬件隔离(TEE) API风格差异 iOS 封闭一体
Android 开放拼装

3.2 证书链验证:默认行为 VS 手动控制

证书链验证是你调用PKI SDK最频繁的场景。iOS的SecTrustEvaluateWithError是个“一次调用出结果”的接口。系统默认从SecTrust里拿回证书列表,自动补全中间证书(如果系统证书库里有的话),再去系统信任锚列表里找根。注意:iOS不会去你App Bundle里自动搜中间CA。我曾经遇到客户把自己的中间CA放在主bundle里,结果因为没调用SecTrustSetAnchorCertificates设置额外的信任锚,验证一直失败。

Android这边用TrustManager.checkServerTrusted或CertPathValidator。核心问题是:默认的信任锚集合由系统根证书库(受信CA证书)决定,且中间证书的补全需要你自己在处理证书链时控制。很多App开发者在HTTPS连接时直接用HttpsURLConnection,依赖系统默认TrustManager,看似简单但无法精细控制。我建议在自定义TrustManager实现里至少做两步:一是显式设置信任锚集,二是开启CertPathBuilder的强制链搜索。

避坑指南:
我曾经做过一个跨平台证书链验证库,发现同一个自签根CA下发的叶子证书,在iOS上直接调SecTrustEvaluate返回errSecSuccess,但在Android上用CertPathValidator.validate抛了CertPathValidatorException。原因:iOS默认把自签根当作信任锚(如果用户手动信任了),而Android需要你在KeyStore里显式导入根证书。如果不导入,TrustManager不认识它,链就断在根上。所以兼容改造时一定要把信任锚设置逻辑单独抽离,不要依赖两边的默认行为。

对比维度 iOS Security.framework Android java.security / Conscrypt
证书链调用接口 SecTrustEvaluateWithError / SecTrustEvaluateAsync checkServerTrusted(X509Certificate[], String) / CertPathValidator.validate
默认信任锚源 系统钥匙串受信证书 + 用户手动信任 /system/etc/security/cacerts 目录(系统CA列表)
中间证书补全 自动搜索系统证书库,不搜App Bundle 不自动补全,需要你自己维护AIA扩展或传完整链
自定义信任锚 SecTrustSetAnchorCertificates(锚列表) 自定义TrustManager -> 构建TrustAnchor集合 -> 传给CertPathBuilder
撤销检查 内嵌OCSP/CRL(可配置策略) 需要显式启用PKIXRevocationChecker或自定义
异常信息 返回NSError对象,原因码+描述 抛出CertPathValidatorException / SSLException,错误链比较长

3.3 密钥生成与存储:钥匙串 vs Android KeyStore

密钥管理是PKI的安全基石。iOS这边通过SecKeyCreateRandomKey生成密钥对,然后直接存入钥匙串,用SecKeyCopyPublicKey取出公钥。钥匙串默认加密级别是硬件级的(Secure Enclave),私钥不允许被导出。你想想看,这种设计天然防止了内存中窃取私钥的攻击。我个人习惯在生成RSA密钥时指定kSecAttrTokenID为kSecAttrTokenIDSecureEnclave以确保硬件安全。

Android这边从API 18开始引入AndroidKeyStore Provider。密钥对通过KeyPairGenerator.getInstance("RSA", "AndroidKeyStore")生成,私钥同样无法导出,存放在TEE或StrongBox中。但Android的碎片化问题在这里非常突出:低端机可能没有TEE支持,导致密钥生成退回到软件实现。我记得有一次测试高通骁龙4xx的机型时,KeyStore生成的RSA密钥对签名速度比预期慢了一个数量级——后来发现是因为该机型TEE性能差,回退到软计算了。如果要做跨平台PKI SDK兼容,密钥生成时一定要做runtime能力检测:能否使用硬件隔离?支持哪些算法(RSA/EC)?密钥可被哪些应用访问?

注意: iOS钥匙串中的SecKey对象是引用类型,你只能通过SecKeyRef操作,拿不到原始数据。Android KeyStore的PrivateKey在API 18以下可以直接获取(不安全),API 18以上被拿走了getEncoded()实现。如果你的SDK需要在两端统一处理私钥输出权限,建议只在初始化阶段生成密钥,后续只使用签名/解密接口,不要读取原始字节。

3.4 算法与性能:Conscrypt 的JNI开销与iOS的内核级加速

iOS Security Framework的加密运算在内核层或Secure Enclave中完成,调用链短,开销小。做过性能压测的人应该知道,相同RSA-2048签名操作,iOS的SecKeyCreateSignature比Android的Signature.getInstance("SHA256withRSA", "Conscrypt")快20%-40%。这并不是说Conscrypt写得差,而是因为Conscrypt作为Java Provider,每次运算都要经过JNI层通信到native的BoringSSL。再加上Android系统的JIT编译、垃圾回收等因素,性能波动比iOS大。我在一次移动设备上测试证书链验证延迟时,iOS在iPhone 12上平均耗时28ms,Android在Pixel 6上平均49ms,最高可到70ms。

但Conscrypt有个iOS没有的灵活性——你可以通过Provider接口切换不同的底层加密库。比如在Android 13上你可以手动切换到Bouncy Castle做实验性算法(如Ed25519),而iOS只能等系统支持。所以如果你的PKI SDK需要兼容非标准曲线或实验性哈希算法,Android反而更方便。

兼容改造视角下的结论:
1. iOS PKI高内聚、低开销、API简洁,适合“开箱即用”的证书验证调用。
2. Android PKI低耦合、高灵活、需手动组装,适合需要自定义信任策略或算法扩展的场景。
3. 跨平台SDK应该把所有信任锚设置、撤销策略、密钥生成参数抽为统一配置层,底层根据平台映射到对应SDK调用。不要在业务代码里直接写SecTrustEvaluate或checkServerTrusted——耦合太紧,后期维护会想哭。

3.5 再啰嗦两句:撤销检查的暗坑

iOS的SecTrustEvaluateWithError默认开启了基础撤销检查,它会去证书的CRL DP扩展或OCSP地址取状态。但如果网络不通或OCSP响应慢,iOS会回调kSecTrustResultRecoverableTrustFailure——意味着证书可能被吊销,但因为没有验证到最新状态所以标记为可恢复失败。Android默认的TrustManager.checkServerTrusted不执行任何撤销检查!

我做过调查:超过一半的Android App的证书链验证没有显示开启撤销检查。如果你在改造SDK时希望两端行为一致,记得在Android上使用PKIXRevocationChecker(Java 8+)或手动下载CRL并调用X509CRL.isRevoked。这一步虽然增加了开发量,但能避免因为证书被吊销导致的安全漏洞。

总结一下:两边各有千秋,没有的好坏,只有适不适合你的改造目标。你需要在统一的抽象接口下,摸清每一边的脾性,然后针对性地做适配。嗯,能把这些差异抽成配置项并跑通两端,PKI SDK兼容已经成功了六成。