快思学习空间

首页/ 学习笔记/ TLS 证书固定

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 说它可信」换成「我事先就知道它是谁」。 在局域网这种物理可达但身份不可信的环境里,这个转换很值 —— 前提是你认真处理了首次传递和密钥轮换这两件事。

← 上一篇:文件秒传背后的判重策略 返回学习笔记 →