当你不想信任"全世界的 CA",只想信任自己的那一把——证书固定,以及它引发的猫鼠游戏。
第 18 章末尾我们看到 PKI 的软肋:你的设备信任几十个预装根 CA,而攻击者只要攻破、胁迫或欺骗其中任何一个,就能签发出你的域名的"合法"证书,神不知鬼不觉地做中间人。对银行、支付、企业这类高价值 App,这个信任面太宽了。证书固定,就是把它收窄到一根针。
回忆第 19 章:App 连服务器时,系统会验证服务器证书是否由某个受信 CA签发。问题在"某个"——你的设备信任列表里有 DigiCert、Let's Encrypt、GlobalSign,还有一堆你没听过的、来自各国的 CA。它们中的任意一个都能为 你的域名 签发一张会被系统接受的证书。
再加上用户可能被诱导安装了恶意的用户级 CA(比如"装这个证书就能用免费 VPN"),或者企业 MDM 推了个 CA——中间人的门槛进一步降低。默认 TLS 校验挡住了随手的攻击,却挡不住"搞定一个 CA"的定向攻击。
证书固定(certificate pinning)的思路直截了当:App 里预先内置你自己服务器证书(或其公钥)的指纹,连接时不光要求"某个 CA 签过",还要求"证书/公钥的指纹正好等于我内置的那个"。这样,即使攻击者搞到了另一张 CA 签发的合法证书,指纹对不上,连接照样拒绝。
最佳实践是固定公钥(SPKI 哈希)而非整张证书。因为证书会定期轮换、续期(尤其 Let's Encrypt 90 天一换),但你可以在续期时复用同一个密钥对,这样公钥指纹不变,pin 就不会因为换证书而失效。固定的是 SHA-256(公钥) 的 Base64 值。
Android 上最省心的做法是声明式的 Network Security Config,连代码都不用写:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.mybank.com</domain>
<pin-set expiration="2027-01-01">
<!-- 当前公钥的 SPKI-SHA256 -->
<pin digest="SHA-256">k3XnEeQ...主钥...=</pin>
<!-- 务必再放一个备用钥的 pin,防止主钥出事把用户锁死 -->
<pin digest="SHA-256">bkup7Zx...备钥...=</pin>
</pin-set>
</domain-config>
</network-security-config>
但请务必看清 pinning 的能力边界。它防的是网络上的中间人——它假设攻击者在信道里。可如果攻击者控制的是设备本身(逆向工程师、安全研究员、或跑在 root 机上的分析工具),故事就不同了。
你可能在本教程站的《Frida Android 攻防实战》里已经见识过:pinning 校验的逻辑跑在 App 自己进程里,而 Frida 这类动态插桩工具可以在运行时直接把校验函数替换成"永远返回通过"。常见绕过手法:
okhttp3.CertificatePinner.check() 让它什么都不做;TrustManager.checkServerTrusted() 直接放行;Pinning 不是"防逆向"或"防抓包分析"的手段——对一个能 root、能跑 Frida 的分析者,它只是一道会被绕过的减速带。它真正的价值在于防御针对普通用户的、网络层的定向 MITM(恶意 Wi-Fi、被攻破的 CA、企业代理)。
别为了 pinning 而牺牲可用性:务必配置备用 pin 和过期时间。真实事故中不乏"主证书密钥出问题、App 又没留备用 pin、结果全球用户集体连不上、只能紧急发版"的惨案。pinning 把一部分风险从"被监听"转移到了"把自己锁死",这个权衡要想清楚。
这一章其实是全书"接缝"主题和"没有绝对安全"主题的一次集中体现。把 pinning 放进一个分层的心智模型里:
| 威胁 | 防御 | 能挡住吗 |
|---|---|---|
| 公共 Wi-Fi 被动窃听 | TLS(第 19 章) | 能 |
| 被攻破的 CA 做 MITM | 证书固定 | 能 |
| 用户装了恶意 CA | 证书固定 | 能 |
| Root 设备 + Frida 逆向 | pinning | 挡不住(需 root/篡改检测等其它手段) |
| 服务端逻辑被绕过 | 客户端任何手段 | 挡不住(关键校验必须在服务端) |
最后一行是本 Phase 最重要的工程真理:任何跑在客户端的检查,原则上都可被绕过。pinning、root 检测、完整性校验,都只是提高攻击成本,而非提供保证。真正不可绕过的安全边界,永远在你控制的服务端。客户端密码学的作用是保护诚实用户免受外部攻击者,而不是防御拿着自己设备的用户本人。
说到"客户端会被绕过、密钥会泄露"——这些抽象的告诫,在真实世界里到底长什么样?下一章,我们直接翻开事故档案,看那些价值连城的翻车现场:硬编码的密钥、坏掉的随机数、一个等号引发的血案。