3. TLS 1.3 握手协议:总体流程,1-RTT 握手
TLS 1.3 的握手,说白了就是把之前的繁琐流程砍掉了一大半。我记得刚开始看 1.3 的 RFC 时,第一反应是:这也太清爽了。以前的 TLS 1.2 要来回折腾两轮,现在正常情况下,一个来回(1-RTT) 就搞定了。
为什么会这样?核心思路就是:能提前算的,绝不等到后面再算。客户端一开始就把自己的密钥共享参数(Key Share)丢过去,服务端也不用藏着掖着,直接算好主密钥返回。这样双方都省了一次延迟。
3.1 从 1.2 到 1.3:砍掉的步骤
我个人习惯把 TLS 1.2 的握手看成是“三步一回头”:先打招呼、再交换证书、最后换密钥。每一步都需要对方确认。而 1.3 的逻辑更像“一步到位”。
我们来对比一下两个版本的关键差异:
| 对比维度 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手次数 | 2-RTT(4 次消息) | 1-RTT(2 次消息,核心握手) |
| Change Cipher Spec | 需要,单独发送 | 废弃,不再需要这个信号 |
| 密钥协商 | 分两次完成(ServerHello 后协商,Finished 前确认) | 一次完成(ClientHello 携带 Key Share) |
| 证书传输 | 必须单独发,随时可能被打断 | 加密后传输,更安全 |
| 会话恢复 | 使用 Session ID 或 Session Ticket | PSK + 0-RTT,几乎无缝 |
| 算法协商 | 独立协商再确认 | ClientHello 里一次性指定,服务端选择 |
3.2 1-RTT 握手流程全景
我们先看一个标准的 1-RTT 握手,它包含两个主要阶段:Hello 阶段和完成阶段。
- 阶段一:ClientHello → ServerHello。客户端发送支持的密码套件列表、Key Share(DH 公钥)、random nonce 等。服务端回复 ServerHello,选定密码套件,带上自己的 Key Share 和证书链。
- 阶段二:客户端验证服务端证书,并立即发送 Finished 消息。服务端也发送 Finished 消息。此时双方都计算出了相同的握手密钥,加密通道建立。
你想想看,整个过程只交换了两次网络消息(不算下层 TCP 的 SYN/SYN-ACK)。在慢速网络上,这一下能省掉 100-200 毫秒的延迟。
3.3 握手过程中的关键状态
我建议在实际调试时,重点关注以下几个状态机节点。我曾在生产环境的 Wireshark 抓包中吃了亏,就因为忽略了其中一个。
- ClientHello 中的 supported_versions 扩展。这个字段明确告诉服务端“我支持 1.3”。如果服务端只支持 1.2,它会拒绝并降级。但注意,降级有风险,容易被降级攻击。
- Key Share 扩展。客户端可以携多个(比如 x25519 和 secp256r1),服务端选一个并回复。如果服务端返回 HelloRetryRequest,说明它都不支持,需要客户端重新发一个符合要求的 Key Share。这会导致多一次往返,变成 2-RTT。
- EncryptedExtensions。这是服务端发的第一条加密消息。用来传递 ALPN、SNI 等扩展。我之前犯过糊涂,以为这里是明文,结果抓包一看是加密的,才意识到 1.3 在 ServerHello 之后基本全加密了。
- CertificateVerify。用于验证服务端对证书的私钥拥有权。这一步必须做,否则证书可以被任何人冒用。我在一次渗透测试中,见过有人伪造证书但是无法通过 CertificateVerify,直接被拒绝。
- Finished 消息。包含整个握手过程的 HMAC 校验值。双方在收到对方 Finished 前,不能认为握手完成。嗯,这里要注意,Finished 必须在加密通道内发送,否则中间人可以篡改。
3.4 密钥派生的核心逻辑
我一直认为 TLS 1.3 的密钥派生比 1.2 要优雅。它只用了一条链:从 PSK(或 DH 共享密钥)一路推到应用密钥。
你可以这么理解:
- 初始阶段:客户端和服务端都有了自己的 DH 私钥,然后通过 ClientHello 和 ServerHello 交换公钥,计算出 共享秘密。
- 这两条消息交换完后,双方都拥有
early_secret(如果用了 PSK)和handshake_secret(从 DH 推导)。 - 在服务端发送 Certificate 和 Finished 前,它基于
handshake_secret派生出一组 握手加密密钥。这时,后续的消息都是加密传输的。 - 最后,基于
handshake_secret再派生一次,得到master_secret,再派生出应用层加密密钥。此时握手结束。
整个过程只用了一次 DH 运算,后续全部通过 HKDF 来拓展密钥材料。我个人习惯把这套流程画成一个链条:PSK (可选) → early_secret → DH → handshake_secret → master_secret → 应用密钥。简单明了。
3.5 避坑指南
在实际开发或排查问题中,有几点我个人反复遇到过:
- HelloRetryRequest 会打乱性能预期。如果你的客户端和服务端支持的曲线不一致,就会触发一次额外的往返。我曾经用了一个只支持 P-256 的客户端去连接只支持 x25519 的服务端,结果握手变成 2-RTT。后来我要求两边都配置主流曲线,避免这种尴尬。
- 不要忽略随机数(random)的作用。虽然 1.3 不依赖 random 做防重放(因为有 PSK 和 DH),但 random 在密钥派生中仍被用作 salt。我见过有人为了省事把 random 固定为全 0,结果导致每次握手密钥都一样,完全丧失了前向安全性。
- 证书链需要确保完整性。因为 1.3 中证书是加密传输的,如果你漏发了中间证书,客户端无法验证,会导致握手失败。我建议在服务端配置里直接包含完整链(叶子 + 中间件),不要依赖客户端去抓取。
openssl s_client -tls1_3 -msg 来查看每一笔消息的原始内容。特别是当遇到握手中断时,根据输出日志里的 “Early Finished” 或 “ServerHello” 的位置,就能快速定位问题。
嗯,1-RTT 握手就是这些。它通过减少一次往返、压缩消息流程、以及加密阶段前移,做到了既快又安全。你把它理解成“单程票直达”,一点没错。