TLS 证书指纹固定:局域网里的中间人防御
「反正在内网」是很多设计里最危险的一句话。 这篇笔记整理局域网环境下的实际威胁,以及证书固定如何把信任锚点从 CA 换成设备自己。
为什么「内网」不等于「可信」
局域网常被默认为安全区,理由通常是「外面的人进不来」。 但这个假设忽略了一点:攻击者未必要从外面进来,他可能本来就在里面 —— 比如连着你家 Wi-Fi 的智能插座、访客的手机、或者隔壁能收到信号的人。
局域网内几种实际可行的攻击:
| 攻击 | 原理 |
|---|---|
| ARP 欺骗 | 局域网内主机靠 ARP 找对方,而 ARP 没有认证。攻击者广播伪造的 ARP 应答,声称「网关的 IP 对应我的 MAC」,就能把流量引到自己这里中转 |
| 伪造热点 | 架一个同名(甚至同密码)的 AP,客户端可能自动连上去,此时所有流量都经过攻击者 |
| DNS 劫持 | 局域网 DNS 请求通常也没加密,可以伪造应答把域名指向任意 IP |
| 恶意接入设备 | Wi-Fi 密码泄露,或 IoT 设备被攻陷后成为内网跳板 |
这些攻击的共同点是:它们不破解加密,而是让你和错误的对方建立加密连接。
默认的 TLS 信任模型
正常情况下 TLS 能挡住这类攻击 —— 前提是证书校验正常工作。它的信任链是这样的:
操作系统 / 浏览器内置若干「根证书颁发机构」
↓ 根 CA 签发中间 CA
↓ 中间 CA 签发站点证书
站点出示证书 → 客户端沿链校验到某个受信任的根
这个模型有效,但它的信任面非常宽:
- 系统里内置了几百个根 CA,任何一个都有能力签发任意域名的证书
- 企业网络常装自己的根证书做流量审计,这在设备看来是完全合法的
- 用户可能因为各种原因手动安装根证书
对公网服务来说,这个权衡是合理的 —— 你没法提前把全世界的网站证书都告诉客户端。 但在一个「客户端和服务器本来就是一对」的场景里,这个信任面就太宽了。
证书固定(pinning)的思路
既然客户端事先就知道自己要连的是哪台设备, 那就可以跳过 CA 体系,直接信任那一张特定的证书:
常规: 证书 → 校验签名链 → 某个受信任的根 CA ✓
固定: 证书 → 指纹比对本地存的指纹 → 完全一致 ✓
这样即使攻击者拿到了某张合法 CA 签发的证书,因为指纹对不上,依然无法冒充。 实践中这条路径通常配合自签证书使用:不依赖任何 CA, 证书就是设备自己生成的。
首次信任问题
自签 + 固定的方案有一个必须回答的问题:
客户端第一次接触这台设备时,怎么知道拿到的指纹是真的?
如果第一次就是被中间人接的,那客户端会把攻击者的指纹存下来, 之后一直信任攻击者 —— 这叫 TOFU(Trust On First Use,首次使用即信任), 和 SSH 第一次连接让你确认指纹是同一个问题。
解决办法是带外传递:让指纹通过一条与数据通道无关的路径到达客户端。
- 配对时由设备在屏幕上显示二维码或数字码,用户扫码 / 手动输入
- 或经由一条已经建立信任的通道下发(比如登录后的服务端接口)
关键词是「已经建立信任」—— 指纹的传递路径本身必须是可信的, 否则问题只是被推后了一层。
固定什么:证书指纹 vs SPKI
| 方式 | 固定对象 | 特点 |
|---|---|---|
| 证书指纹 | 整张证书 DER 编码的 SHA-256 | 直观;但证书一续期(哪怕公钥没变)指纹就变了,客户端必须同步更新 |
| SPKI 指纹 | 证书里公钥信息(Subject Public Key Info)的哈希 | 续期时只要复用同一对密钥,指纹就不变;实践中的主流选择 |
SPKI 更实用的原因很简单:证书续期是很频繁的运维动作, 而密钥对通常可以长期复用。
实现要点
各平台的介入点:
-
iOS / macOS:实现
URLSessionDelegate的urlSession(_:didReceive:completionHandler:), 在.serverTrust分支里取到SecTrustRef, 自行校验后决定是否接受 -
Android:
network_security_config.xml里声明<pin-set>;或使用 OkHttp 的CertificatePinner - 通用:先做常规的证书链 / 有效期校验,再比对指纹。 只比指纹而跳过常规校验,等于放弃了其他所有保护
一定要准备备用 pin
固定最危险的地方在于把自己锁死: 一旦服务端换了证书,而客户端里只有旧指纹,所有连接会立刻全部失败 —— 而且是那种「用户卸载重装才能恢复」的失败。
标准做法是在客户端里同时内置备用公钥的指纹:
pins = [
"当前使用中的公钥指纹",
"离线保管、暂未启用的备用公钥指纹"
]
轮换流程:先用备用密钥上线服务 → 客户端因为内置了备用 pin 依然能连上 → 再通过版本更新把新的备用密钥推下去。全程没有断连窗口。
代价与取舍
证书固定不是免费的:
- 运维复杂度上升。密钥轮换变成需要提前规划的事,不能临时换
- 失去了灵活性。常规 TLS 下换证书对客户端完全透明,固定之后不是了
- 调试变麻烦。抓包工具(Charles、mitmproxy)默认会失效,需要专门加白名单
所以它适合的场景很明确:客户端与服务器是固定的对应关系,且流量价值高。 典型的反例是浏览器访问任意网站 —— 你不可能固定全世界。
一句话总结
证书固定真正做的事,是把信任的锚点从「某个 CA 说它可信」换成「我事先就知道它是谁」。 在局域网这种物理可达但身份不可信的环境里,这个转换很值 —— 前提是你认真处理了首次传递和密钥轮换这两件事。