密语 · CIPHER | 目录 CH.21 / 26
绝密
Phase 5 · 工程实战

证书校验与 Pinning

当你不想信任"全世界的 CA",只想信任自己的那一把——证书固定,以及它引发的猫鼠游戏。

阅读 ~14 分钟 前置 第 18、19 章 Demo Pinning 校验现场

第 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,连代码都不用写:

res/xml/network_security_config.xml
<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>
DEMO Pinning 校验现场
勾选组合看看:①无 pinning + MITM = 静默沦陷;②有 pinning + MITM = 连接被拒。pinning 把"信任任意 CA"收窄成"只信我这把公钥"。

三、猫鼠游戏:Pinning 也会被绕过

但请务必看清 pinning 的能力边界。它防的是网络上的中间人——它假设攻击者在信道里。可如果攻击者控制的是设备本身(逆向工程师、安全研究员、或跑在 root 机上的分析工具),故事就不同了。

你可能在本教程站的《Frida Android 攻防实战》里已经见识过:pinning 校验的逻辑跑在 App 自己进程里,而 Frida 这类动态插桩工具可以在运行时直接把校验函数替换成"永远返回通过"。常见绕过手法:

摆正 pinning 的定位

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 检测、完整性校验,都只是提高攻击成本,而非提供保证。真正不可绕过的安全边界,永远在你控制的服务端。客户端密码学的作用是保护诚实用户免受外部攻击者,而不是防御拿着自己设备的用户本人

说到"客户端会被绕过、密钥会泄露"——这些抽象的告诫,在真实世界里到底长什么样?下一章,我们直接翻开事故档案,看那些价值连城的翻车现场:硬编码的密钥、坏掉的随机数、一个等号引发的血案。

本章要点

  • 默认 TLS 信任所有预装 CA,信任面过宽;证书固定把它收窄为"只信我内置的公钥指纹"。
  • 固定公钥(SPKI 哈希)而非整证书,并务必配置备用 pin + 过期时间,否则可能把用户锁死。
  • Pinning 防网络层 MITM(恶意 Wi-Fi、被攻破 CA),但对 root + Frida 的设备端逆向无效。
  • 客户端一切检查都可被绕过,只提高成本;不可绕过的安全边界永远在服务端。