3. TLS 1.3 握手协议:总体流程,1-RTT 握手

TLS 1.3 的握手,说白了就是把之前的繁琐流程砍掉了一大半。我记得刚开始看 1.3 的 RFC 时,第一反应是:这也太清爽了。以前的 TLS 1.2 要来回折腾两轮,现在正常情况下,一个来回(1-RTT) 就搞定了。

为什么会这样?核心思路就是:能提前算的,绝不等到后面再算。客户端一开始就把自己的密钥共享参数(Key Share)丢过去,服务端也不用藏着掖着,直接算好主密钥返回。这样双方都省了一次延迟。

1-RTT 的核心目标: 在握手完成时,双方立即拥有加密能力,不需要额外等待第二次握手。

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 里一次性指定,服务端选择
个人体会: 我最初调试 1.2 时,最头疼的就是 Change Cipher Spec 这个状态,经常因为中间件乱序处理导致握手失败。1.3 直接干掉它,真是省心不少。

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 毫秒的延迟。

注意: 这里的“1-RTT”指的是从客户端发起 Hello 到双方 Finished 完成,不包括 TCP 连接的建连时间。如果加上 TCP 三次握手,实际延迟是 1.5 个 RTT(TCP 建了 1 个 RTT,然后做 TLS 握手 1 个 RTT。但你也可以用 TCP Fast Open 来优化)。
TLS 1.3 1-RTT 握手流程图 客户端 服务端 ClientHello Key Share (DH 公钥) ServerHello Key Share + EncryptedExtensions Certificate + CertificateVerify Finished (双方发送) 此时计算握手密钥完成 总网络延迟:1-RTT(TCP 建链后)

3.3 握手过程中的关键状态

我建议在实际调试时,重点关注以下几个状态机节点。我曾在生产环境的 Wireshark 抓包中吃了亏,就因为忽略了其中一个。

  1. ClientHello 中的 supported_versions 扩展。这个字段明确告诉服务端“我支持 1.3”。如果服务端只支持 1.2,它会拒绝并降级。但注意,降级有风险,容易被降级攻击。
  2. Key Share 扩展。客户端可以携多个(比如 x25519 和 secp256r1),服务端选一个并回复。如果服务端返回 HelloRetryRequest,说明它都不支持,需要客户端重新发一个符合要求的 Key Share。这会导致多一次往返,变成 2-RTT。
  3. EncryptedExtensions。这是服务端发的第一条加密消息。用来传递 ALPN、SNI 等扩展。我之前犯过糊涂,以为这里是明文,结果抓包一看是加密的,才意识到 1.3 在 ServerHello 之后基本全加密了。
  4. CertificateVerify。用于验证服务端对证书的私钥拥有权。这一步必须做,否则证书可以被任何人冒用。我在一次渗透测试中,见过有人伪造证书但是无法通过 CertificateVerify,直接被拒绝。
  5. Finished 消息。包含整个握手过程的 HMAC 校验值。双方在收到对方 Finished 前,不能认为握手完成。嗯,这里要注意,Finished 必须在加密通道内发送,否则中间人可以篡改。
我曾经踩过的坑: 有一次做负载均衡器,它把 TLS 1.3 的 ServerHello 和 EncryptedExtensions 分开转发到了两个后端,结果客户端直接卡死。因为 1.3 要求这两条消息必须由同一个服务端发出。后来我在代码里加了个检查,确保握手期间所有消息来自同一个源 IP。

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 → 应用密钥。简单明了。

关键设计: 1.3 中的 Finished 消息是用握手密钥加密的。因此,当客户端收到服务端的 Finished,它可以确认服务端确实持有私钥,且没有被降级过。这个设计比 1.2 的 CertificateVerify 更紧凑,也更安全。

3.5 避坑指南

在实际开发或排查问题中,有几点我个人反复遇到过:

  • HelloRetryRequest 会打乱性能预期。如果你的客户端和服务端支持的曲线不一致,就会触发一次额外的往返。我曾经用了一个只支持 P-256 的客户端去连接只支持 x25519 的服务端,结果握手变成 2-RTT。后来我要求两边都配置主流曲线,避免这种尴尬。
  • 不要忽略随机数(random)的作用。虽然 1.3 不依赖 random 做防重放(因为有 PSK 和 DH),但 random 在密钥派生中仍被用作 salt。我见过有人为了省事把 random 固定为全 0,结果导致每次握手密钥都一样,完全丧失了前向安全性。
  • 证书链需要确保完整性。因为 1.3 中证书是加密传输的,如果你漏发了中间证书,客户端无法验证,会导致握手失败。我建议在服务端配置里直接包含完整链(叶子 + 中间件),不要依赖客户端去抓取。
一个小技巧: 调试 1.3 握手时,我常用 openssl s_client -tls1_3 -msg 来查看每一笔消息的原始内容。特别是当遇到握手中断时,根据输出日志里的 “Early Finished” 或 “ServerHello” 的位置,就能快速定位问题。

嗯,1-RTT 握手就是这些。它通过减少一次往返、压缩消息流程、以及加密阶段前移,做到了既快又安全。你把它理解成“单程票直达”,一点没错。