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.2 | TLS 1.3 |
|---|---|---|
| 密钥交换 | RSA、DH、ECDH(静态) | 仅 ECDHE / DHE |
| 前向安全 | 可选 | 强制 |
| 握手 RTT | 2 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 则适合低延迟场景(比如 HTTP/3 的 QUIC 握手)。但代价是需要你自己处理重放防护。强制前向安全更是杜绝了那种「私钥泄露即全面沦陷」的噩梦。
我个人的习惯是:新服务全部上 1.3,旧服务逐步迁移。如果遇到 0-RTT 需求,一定花时间检查业务是否幂等——别像我当年那样,测试环境跑得欢,上线就被重放攻击打脸。