4、TLS 1.3握手简化流程:1-RTT握手与0-RTT恢复

从 TLS 1.2 到 1.3,最大的感受就是「快」。我早年调一个 HTTPS 连接,光握手就要两个 RTT,客户端和服务端来回折腾三四趟。到了 1.3,一次往返就能搞定。如果客户端之前连过,甚至零往返恢复——速度提升非常明显。

但代价呢?1.3 砍掉了很多老协议里的「温柔」。静态 RSA 密钥交换直接没了,所有密钥协商强制使用 ECDHE。说白了,就是逼着双方必须前向安全。你在 1.2 里可能还有偷懒不配 DHE 的情况,到了 1.3 根本逃不掉。

从 1.2 到 1.3:我们甩掉了什么

我自己做过一个项目,升级 TLS 版本时发现客户端的协议栈只支持 1.2,而且用的是静态 RSA 密钥交换。当时我还犹豫要不要保留这个选项,后来发现 1.3 直接不支持,倒逼着我们把整个密钥协商改了。嗯,改完后才知道,其实也没那么复杂。

来看一下 1.3 砍掉的内容:

特性TLS 1.2TLS 1.3
密钥交换RSA、DH、ECDH(静态)仅 ECDHE / DHE
前向安全可选强制
握手 RTT2 RTT(完整)1 RTT(完整)
会话恢复会话 ID / 会话票证PSK + 0-RTT
压缩支持移除
Rehandshake支持不支持

你想想看,很多安全问题都是因为可选的措施没人用。比如静态 RSA,私钥泄露后所有历史会话都能解密——我见过不止一家公司的老系统就这样裸奔了五六年。1.3 的做法很干脆:不给你留后路,全上 ECDHE。

1-RTT 握手:一次握手就够了

TLS 1.3 的完整握手流程,从客户端发起 ClientHello 到服务端返回 ServerHello + Certificate + Finished,再回到客户端的 Finished,总共就一次往返。怎么做到的?

  • 客户端先发:ClientHello 里直接带上了支持的 ECDHE 参数(key_share),以及猜的算法套件。
  • 服务端直接回:在 ServerHello 里给出选中的参数,发送自己的 key_share,同时发送证书和 Finished 消息——这时候密钥已经算出来了。
  • 客户端收完就能发数据:验证完 Finished 后,马上发送应用数据(和 Finished 一起)。

我做过一个测试:用同一个服务端,1.2 的握手耗时大约是 20-30 ms(不包含证书链验证),而 1.3 只需要 10-15 ms。如果算上延迟高的网络,差距更明显。尤其是移动端,UDP 转 TCP 的场景,少一个 RTT 体验差别很大。

关键点:TLS 1.3 将大多数握手消息合并发送。服务端发送 ServerHello 后,紧接着发送 Certificate 和 Finished。客户端收到后立即计算密钥并发送 Finished + 数据。整个握手逻辑是「并行」而非串行。

# 伪代码示意:TLS 1.3 1-RTT 握手
客户端 -> 服务端: ClientHello (key_share, supported_versions, ...)
服务端 -> 客户端: ServerHello + EncryptedExtensions + Certificate + CertificateVerify + [Finished]
客户端 -> 服务端: [Finished] + 应用数据 (已加密)

0-RTT 恢复:快是快了,但小心重放

如果你之前连接过同一个服务端,TLS 1.3 支持 0-RTT——客户端可以在发第一条消息时就直接带着加密的应用数据。这依赖 PSK(预共享密钥),通常是上次握手协商出来的。

原理很简单:客户端保存一个 session ticket(其实就是 PSK 的载体)。下次重建连接时,ClientHello 里带上这个 PSK 标识,同时发送一条 0-RTT 数据(已经用 PSK 加密了)。服务端如果接受,直接解密并回复,不额外做密钥协商。

避坑指南:我曾经在一个支付系统里启用了 0-RTT,结果上线后出现重复扣款。原因就是客户端重试时把同一个 0-RTT 请求发了两次,而服务端没有做幂等处理。0-RTT 天然存在重放攻击风险,因为它不包含抗重放的随机数。解决办法:对 0-RTT 数据做幂等校验,或者限制 0-RTT 只用于读操作。

对比一下两种恢复方式:

特性Session ID(1.2)0-RTT(1.3)
延迟1 RTT(需要一次握手恢复)0 RTT
重放防护内置抗重放机制需应用层实现
密钥刷新每次恢复都协商新密钥可复用旧 PSK 或派生新 PSK
服务器状态需存储 session 状态(或加密的 ticket)无状态(ticket 含有 PSK

强制前向安全:这就是真正的安全感

前向安全(PFS)的意思是:即使攻击者现在拿到了你的长期私钥,也不能解密之前记录下来的加密数据。在 1.2 里,如果你用了静态 RSA,私钥一丢,之前所有通信记录都能解密。1.3 只允许 ECDHE——每次握手都生成临时 DH 密钥对,用完就丢。

讲个真实案例。有个客户的业务系统用了 1.2 + RSA 密钥交换,他们自建 PKI。三年后换证书时,运维不小心把旧私钥暴露到了 GitHub(没错,就是这么离谱)。攻击者拿着私钥,把过去三年的 HTTPS 流量全部解密——包括用户密码、银行卡号。我后来帮他们迁到 1.3,问题才根治。

强制 PFS 带来的副作用:服务器需要额外的 ECDHE 计算,但这个开销在现代 CPU 上几乎可以忽略。我建议你在服务端至少配置 X25519 曲线,它比 P-256 快不少。

实践建议:如果你的服务端同时支持 TLS 1.2 和 1.3,建议在 1.2 侧也强制使用 ECDHE(通过 cipher suite 排除 RSA key exchange)。这样即便无法完全切到 1.3,也能享受前向安全。

图:TLS 1.3 1-RTT 握手与 0-RTT 恢复流程 1-RTT 完整握手 客户端 → 服务端:ClientHello (key_share) 0-RTT 恢复 客户端 → 服务端:ClientHello (PSK标识) + 数据 1-RTT 也可用 服务端回复(1-RTT) ServerHello + key_share EncryptedExtensions + Certificate CertificateVerify + [Finished] 服务端返回一切(1次往返) 客户端验证并发送 [Finished] + 应用数据(已加密) 服务端处理 0-RTT 数据 直接解密 PSK 封装的请求 回复 [Finished] + 响应数据 0-RTT 响应 ⚠ 注意 0-RTT 请求存在重放风险,需应用层做幂等处理。 1-RTT 握手强制 ECDHE,保证前向安全。

小结

TLS 1.3 把握手从「陪洽」变成了「直球」。1-RTT 足够你用,0-RTT 则适合低延迟场景(比如 HTTP/3 的 QUIC 握手)。但代价是需要你自己处理重放防护。强制前向安全更是杜绝了那种「私钥泄露即全面沦陷」的噩梦。

我个人的习惯是:新服务全部上 1.3,旧服务逐步迁移。如果遇到 0-RTT 需求,一定花时间检查业务是否幂等——别像我当年那样,测试环境跑得欢,上线就被重放攻击打脸。