一次握手,一个 RTT
咱们直接聊 TSL 1.3 最核心的改进——一次 RTT 完成握手。
你可能经历过这种场景:打开一个海外网页,白屏了好几秒。原因之一就是 TLS 握手太慢了。传统 TLS 1.2 握手需要 2 个 RTT,也就是客户端和服务端来回两次才能开始传数据。在跨洋网络里,一个 RTT 轻松上百毫秒,用户等得心焦。
为什么 TLS 1.3 能做到 1-RTT?
说白了,就是把以前需要两次协商的事,压缩到一次完成。
TLS 1.2 的握手流程是这样的:
- RTT 1:Client Hello → Server Hello(协商密码套件、协议版本、交换证书)
- RTT 2:Client 验证证书 → 生成 Pre-Master Secret → Change Cipher Spec → Finished
TLS 1.3 做了关键剪枝——客户端直接在第一个消息里就把“密钥协商材料”扔过去了。服务端收到后立刻算出共享密钥,顺便把证书和握手确认塞进同一个响应里。一个来回,完事。
关键变化:TLS 1.3 的 Client Hello 里就带了 key_share 扩展。这玩意包含了客户端的公钥。服务端不需要额外交换就能生成主密钥。
1-RTT 握手消息流程
我习惯把这 1-RTT 拆成 3 个阶段来看。不啰嗦,直接上图。
看到没?整个消息交换紧密贴合在一个来回里。不需要服务端先发证书、客户端再验证、再发密钥的拖沓过程。
Client Hello 里到底放了什么?
这是 TLS 1.3 最精巧的设计。客户端不再傻等,而是把“家底”先亮出来。
核心内容:
- 支持的密码套件:TLS 1.3 的密码套件只有 5 种,不像 1.2 里有几十种。客户端列出自己愿意用的那几个。
- key_share 扩展:客户端生成一对临时密钥,把公钥塞进去。这是实现 1-RTT 的关键。
- supported_versions 扩展:表明自己支持 1.3,告诉服务端“我准备好了”。
- pre_shared_key(可选):如果之前和这个服务端握过手,客户端可以直接带上 PSK ID,甚至 0-RTT 完成。
我在项目中调整过 key_share 的曲线选择。默认用 X25519,但有些 FIPS 环境要求 P-256。这时候你需要在 Client Hello 里同时提供两组 key_share,服务端会挑合适的用。
Server Hello 的处理逻辑
服务端收到 Client Hello 后,工作流非常紧凑:
- 选套件:从客户端列表里挑一个密码套件。
- 匹配密钥:从 key_share 里找到自己能用的曲线,用服务端的私钥和客户端的公钥算出共享秘密。
- 衍生密钥:用 HKDF 把共享秘密展开成主密钥、会话密钥。
- 发送响应:Server Hello 里带上自己的 key_share、支持版本、已选的密码套件,后面紧跟着 Encrypted Extensions 和证书。
证书这时候是加密的。服务端会用计算出的握手密钥加密证书和 Finished 消息。客户端收到后,一样能算出这把密钥,解密证书并验证。
我曾经遇到一个兼容性问题:某旧版 TLS 库在 1.3 握手时只发了一个 key_share,但服务器不支持该曲线。服务端返回了 HRR(HelloRetryRequest),这样握手暴增到 2-RTT。你想想看,本来想省时间,结果比 1.2 还慢。
密钥衍生:给你看代码
我画了个简单示意,对应 TLS 1.3 的密钥衍生链:
// TLS 1.3 密钥衍生(简化版)
early_secret = HKDF-Extract(0, PSK)
handshake_secret = HKDF-Extract(early_secret, DH_shared_secret)
master_secret = HKDF-Extract(handshake_secret, 0)
// 然后派生出各种工作密钥:
client_handshake_traffic_secret
server_handshake_traffic_secret
client_application_traffic_secret
server_application_traffic_secret
关键在于 DH_shared_secret 是通过 key_share 交换产生的。这个值在客户端发送 Client Hello 时、服务端收到后,两边同时能算出来。所以服务端一发回 Server Hello,双方就有了相同的握手密钥。
这就跟传统方法很不一样。TLS 1.2 需要客户端先收到证书,验证通过后生成 Pre-Master Secret,再用服务端公钥加密发回去。一来一回,硬生生多了一次延迟。
握手完成后的状态
Finished 消息发送后,双方都进入了“握手完成”状态。这时候的安全性要点如下:
| 阶段 | 密钥保护范围 | 备注 |
|---|---|---|
| Client Hello | 明文 | 仅携带密钥协商材料,不暴露敏感信息 |
| Server Hello + EncryptedExtensions | 用握手密钥加密 | 证书、请求的 SNI 等信息都受保护 |
| Finished | 用握手密钥加密 | 包含完整的握手哈希,防篡改 |
| Application Data | 用应用数据密钥加密 | 真正的业务数据通道 |
你发现没有?TLS 1.3 的 Server Hello 之后的所有消息都加密了。这就避免了一个常见攻击——中间人篡改证书协商阶段的内容。1.2 版本里 EncryptedExtensions 只是可选项,1.3 直接把它变成强制加密的。
迁移时要注意什么
作为一个干过几回迁移的工程师,我得说三次:
- 确认双方都支持 key_share 扩展。我在升级一个旧版 nginx 时发现,它的 OpenSSL 版本低于 1.1.1,根本不认 key_share。客户端发来 1.3 的 ClientHello,它当 1.2 解析,直接报错。
- HRR 不是 bug,但会吞性能。如果你的客户端和服务器 curve 偏好不一致,就会触发 HelloRetryRequest。理想情况下应该配成一致的曲线列表。
- 0-RTT 不是 1-RTT 的替代品。它更像是锦上添花。第一把握手永远是 1-RTT,之后才能用 0-RTT 重连。
关键结论:TLS 1.3 的核心创新就是通过 key_share 前置,把两次协商压缩成一次。这是协议架构上的进步,不是简单的版本迭代。
好了,这部分就聊到这里。握手的细节基本讲清楚了,你可以去试着抓个包看看实际效果。我也经常这么干。