8.6 TLS 握手、证书与 mTLS

一句话

TLS 握手只干三件事——验证身份、协商密钥、开始加密;TLS 1.3 把两次往返压成一次,会话复用再压成 0-RTT,而线上大多数 “握手慢/连不上” 不是加密太慢,而是证书链不全、OCSP 阻塞、或没开复用。

1.2 与 1.3:差的是往返,不是算法

维度 TLS 1.2 TLS 1.3
完整握手往返 2-RTT 1-RTT
密钥交换 RSA 或 ECDHE(RSA 无前向保密) 仅 ECDHE(前向保密成为默认)
证书是否明文 明文(可被被动观察) 加密
会话复用 Session ID / Session Ticket PSK(预共享密钥),可 0-RTT
可协商的套件 多且杂(含弱套件) 精简到 AEAD 套件

一次完整握手的耗时约等于 2 × RTT(TLS 1.2)或 1 × RTT(TLS 1.3);跨洲 RTT 150ms 时,这直接是 300ms 与 150ms 的差别——首屏时间的固定成本。

证书链:最常见的 “部分用户连不上”

叶证书(你的域名)
   └── 中间 CA(服务端必须一起下发)
          └── 根 CA(客户端信任库内置)
错误 客户端表现 为什么难查
中间证书没下发 桌面浏览器正常(会补全),Android/Java/curl 失败 你的环境正常,用户环境失败
证书过期 全量失败,报 certificate has expired 监控没覆盖到期时间
域名不在 SAN 里 全量失败(CN 已不再被信任) 用了通配符却漏了二级子域
自签 / 私有 CA 未装 CA 的客户端失败 内网互调容易 “大家都装了” 而忘记新服务
客户端时钟偏差 报 “证书尚未生效” 容器/设备时间不准
# 看服务端到底下发了哪几张证书(链全不全)
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E "^ [0-9] s:|i:"
# 只验链(不建连接)
curl -vI https://example.com 2>&1 | grep -E "SSL certificate|expire|subject"

判据:s_client 输出里应能看到 叶 + 至少一张中间 CA;只有叶证书就是链不全。

SNI 与虚拟主机:一个 IP 多个域名

服务端在 TLS 握手时靠 SNI(客户端明文告知的域名)选择证书。三个推论:

  • 老客户端不支持 SNI → 只能拿到默认证书(如今罕见,但内网设备仍有)。
  • 直接 curl https://<IP> 会因 SNI 缺失/不匹配而证书校验失败——用 IP 测 HTTPS 失败不代表服务坏了。
  • 多域名共用 IP 时,证书要覆盖所有 SNI 名字,否则 “A 域名正常、B 域名报错”。

mTLS:把 “你是谁” 也放进握手

维度 单向 TLS 双向(mTLS)
谁验证谁 客户端验证服务端 双方互验证书
客户端身份 靠 token/cookie(应用层) 证书(传输层),应用层无需再传凭证
证书管理 服务端一套 每个工作负载一套,轮换是主要成本
典型场景 公网 HTTPS 服务网格、零信任内网、B2B 对接

mTLS 的工程成本集中在三件事:签发(谁当 CA、有效期多长)、分发(每 Pod 一张证书,如何注入)、轮换(短有效期 + 自动续签,否则到期即全站中断)。所以实践中它通常由服务网格或 SPIFFE 类身份框架统一承担(见 30.5 Service Mesh 与 Sidecar、38.8 零信任)。

会话复用与 0-RTT:省一次往返,换一个风险

机制 省下的 风险
Session ID / Ticket(TLS 1.2) 免去完整握手 票据密钥泄露 = 可解密历史流量
PSK(TLS 1.3) 1-RTT 变 0-RTT —
0-RTT 数据 首包即可带应用数据 可重放:只能用于幂等请求(GET),不能用于下单/转账

0-RTT 的正确用法是 “CDN 上的静态 GET 用它,写请求回退到 1-RTT”。

反模式

反模式 后果
只配叶证书,忘记中间证书 部分客户端(Android/Java)全量失败
证书到期靠人记 到期当天全站不可用;应做自动续期 + 到期告警(提前 30/7/1 天)
用 IP 做健康检查且走 HTTPS SNI 不匹配 → 探针永远失败
对所有请求开 0-RTT 写请求被重放,产生重复订单
内网明牌 “不需要 TLS” 内网横向移动可直接嗅探;零信任要求内网也加密
mTLS 证书有效期 1 年且手工换 轮换窗口巨大,一次遗漏就是全站断连
关掉证书校验 “先跑通” 中间人攻击面完全敞开,且很难再改回来

三条铁律

🔑 链要完整:叶 + 中间 CA 一起下发;只发叶证书 = 部分用户不可用。

🔑 证书要自动化:签发、续期、告警都交给流水线,人只处理异常。

🔑 0-RTT 只给幂等请求;写操作必须等完整握手。

自检

Q1 浏览器访问正常,安卓 App 报 “证书链不完整”。为什么浏览器没事?

▸ 答案:因为桌面浏览器多数会自动补全缺失的中间证书(AIA fetching),而 Android/Java/Go 的默认校验只认服务端下发的链。修法是把中间证书拼进服务端证书文件(顺序:叶 → 中间),并用 openssl s_client -showcerts 验证下发了两张。

Q2 curl https://1.2.3.4 报证书错误,说明服务坏了?

▸ 答案:不能说明。用 IP 访问时没有 SNI,服务端只能返回默认证书,而默认证书的 SAN 里不会有这个 IP(除非专门签了 IP 证书)。正确验证方式是 curl --resolve example.com:443:1.2.3.4 https://example.com,保留 SNI 与 Host。

Q3 为什么 TLS 1.3 的 0-RTT 不能用于下单?

▸ 答案:因为 0-RTT 数据可以被重放:攻击者录下第一个 0-RTT 包再发一次,服务端会当成新请求处理(此时还没完成防重放协商)。下单/支付/改状态这类非幂等操作会被执行两次。工程规则是 “GET 类可 0-RTT,写类必须 1-RTT”,并在服务端做幂等兜底(见 25.4 幂等设计与去重)。

Q4 内网服务之间要不要 mTLS?代价是什么?

▸ 答案:要,但要看代价能否被平台吸收。收益是把 “应用层 token 泄露即被冒充” 变成 “没有私钥就无法建连”,并为零信任提供传输层身份。代价是三件事:每个工作负载一套证书、短有效期带来的轮换压力、以及握手带来的 CPU 与延迟(会话复用可摊薄)。团队没有证书自动化能力时,先上网格(由 sidecar 代管)比自己写轮换脚本更稳。

Q5 握手慢,你会先看哪三个指标?

▸ 答案:① RTT 次数(1.2 的 2-RTT vs 1.3 的 1-RTT,未开复用则每次都完整握手);② 证书链大小(超过约 4KB 会跨多个 TCP 段,慢速链路上显著变慢);③ OCSP/CRL 是否阻塞(未开 stapling 时客户端要额外请求 CA,可能几百毫秒甚至超时)。三者任一都可能让 “加密” 背锅。

关联