一次握手,一个 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 个阶段来看。不啰嗦,直接上图。

TLS 1.3 单次 RTT 握手流程 Client Server Time ClientHello (key_share + 密码套件) ServerHello + EncryptedExtensions (证书 + 握手完成) Finished (验证证书 + 发送握手完成) Application Data 使用握手密钥保护 完成:1 个 RTT 内完成密钥协商、证书验证、握手确认

看到没?整个消息交换紧密贴合在一个来回里。不需要服务端先发证书、客户端再验证、再发密钥的拖沓过程。

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 后,工作流非常紧凑:

  1. 选套件:从客户端列表里挑一个密码套件。
  2. 匹配密钥:从 key_share 里找到自己能用的曲线,用服务端的私钥和客户端的公钥算出共享秘密。
  3. 衍生密钥:用 HKDF 把共享秘密展开成主密钥、会话密钥。
  4. 发送响应: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 前置,把两次协商压缩成一次。这是协议架构上的进步,不是简单的版本迭代。

好了,这部分就聊到这里。握手的细节基本讲清楚了,你可以去试着抓个包看看实际效果。我也经常这么干。