每个 HTTPS 请求都始于一段大多数工程师从未留意过的对话。在你的浏览器发送 GET / 之前,它会和服务器商定协议版本、交换密钥、检查证书,并证明这一切都没有被篡改。出问题时,你会看到 handshake_failure 或 bad_certificate 这类晦涩的错误;而一旦变慢,每个页面都要为此买单。
TLS 1.3 让握手比 TLS 1.2 更短、更私密。本文借助一张时序图逐条讲解握手消息,然后介绍你在生产环境中会遇到的各种变体:HelloRetryRequest、会话恢复与 0-RTT、双向 TLS、Encrypted Client Hello 以及后量子密钥交换。
握手需要完成什么
在任何应用数据流动之前,TLS 握手要完成四项工作:
- 协商参数:协议版本、密码套件和密钥交换组。
- 建立共享密钥:双方在从不发送秘密的情况下推导出相同的秘密。
- 认证服务器,并可选地认证客户端,依据是证书。
- 确认完整性:双方核对彼此看到的握手消息是否相同,这样攻击者就无法降级或篡改协商过程。
当前的规范是 2026 年 7 月发布的 RFC 9846。它取代了 RFC 8446,但保留了相同的协议版本,因此按旧 RFC 实现的 TLS 1.3 客户端,仍然可以和按新 RFC 实现的服务器通信。
完整握手,逐条消息
TLS 1.3 只需要一次往返。客户端会猜测服务器将接受哪种密钥交换,并在第一条消息里就发送自己这一半的密钥交换。
| 消息 | 方向 | 是否加密 | 携带的内容 |
|---|---|---|---|
| ClientHello | 客户端 → 服务器 | 否 | 随机值、密码套件、supported_versions、key_share、signature_algorithms、server_name(SNI)、ALPN |
| ServerHello | 服务器 → 客户端 | 否 | 选定的密码套件、服务器的 key_share |
| EncryptedExtensions | 服务器 → 客户端 | 是 | 其余已协商的扩展,例如 ALPN 协议 |
| Certificate | 服务器 → 客户端 | 是 | 服务器的证书链 |
| CertificateVerify | 服务器 → 客户端 | 是 | 用证书私钥对目前为止的握手所做的签名 |
| Finished | 服务器 → 客户端 | 是 | 对整个握手记录的 MAC |
| Finished | 客户端 → 服务器 | 是 | 客户端对握手记录的 MAC |
关键在于 ServerHello 之后的那条线。客户端一拿到服务器的密钥共享,双方就运行密钥调度(对共享秘密和握手记录做 HKDF),推导出握手流量密钥。从这一刻起,每条握手消息都会被加密。在 TLS 1.2 中,网络上的观察者可以读到服务器的证书;在 TLS 1.3 中则不行。
CertificateVerify 证明服务器持有它刚刚发送的证书的私钥。Finished 证明双方看到了相同的消息。如果攻击者从 ClientHello 中删掉一个密码套件,双方的握手记录就会不一致,Finished 校验就会失败。
服务器在发出自己的 Finished 之后就可以立即发送应用数据,客户端也会在自己的 Finished 之后立即发送第一个请求。从 ClientHello 起只需一次往返,加密连接就已就绪。
与 TLS 1.2 相比的变化
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| 新连接所需往返次数 | 2 | 1 |
| 密钥交换 | RSA 密钥传输或 (EC)DHE | 仅 (EC)DHE,或带 (EC)DHE 的 PSK |
| 前向保密 | 可选 | 始终具备,仅 PSK 恢复除外 |
| 密码套件 | 数十种,包括 CBC 和 RC4 | 五种,全部为 AEAD |
| 线路上的服务器证书 | 明文 | 加密 |
| 压缩与重协商 | 支持 | 已移除 |
密码套件列表缩减为 TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256 以及两个 CCM 套件。TLS 1.3 的密码套件现在只指定记录加密和哈希;密钥交换和签名算法则单独协商。RSA 证书依然可用。消失的是 RSA 密钥传输:在这种方式下,客户端用服务器的公钥加密秘密,一旦密钥被盗,所有被录下的会话都能被解密。
当一次往返变成两次:HelloRetryRequest
一次往返的握手依赖于一个猜测。客户端为它预计服务器会接受的组发送密钥共享,通常是 X25519。如果服务器一个都不支持,它会回复一条 HelloRetryRequest,指明自己接受的组,客户端再用新的 ClientHello 重新开始。这会多出整整一次往返。
当客户端和服务器在组的选择上不一致时就会出现这种情况,例如服务器只配置了 P-256,而客户端只发送 X25519 的密钥共享。解决办法在于配置:确保客户端优先提供的组,正是服务器偏好的组。
会话恢复与 0-RTT
握手完成后,服务器可以发送 NewSessionTicket 消息。有了票据,客户端之后就能用预共享密钥(PSK)恢复会话,而不必进行完整的证书交换。会话恢复通常仍会执行一次新的 (EC)DHE 交换,因此恢复后的会话依然具备前向保密。
有了票据,客户端还可以发送称为 0-RTT 的早期数据:第一个请求和 ClientHello 在同一轮(flight)中发出。这节省了一次往返,但 RFC 9846 明确指出,0-RTT 数据在不同连接之间没有任何防重放保证。截获第一轮消息的攻击者可以再发送一次。
实践中的规则:
- 只把幂等请求作为早期数据发送:没有副作用的
GET可以,支付或下单则绝对不行。 - 在服务器端,只在应用能够拒绝或容忍重放的地方启用 0-RTT。许多服务器会标记早期数据请求(例如使用 RFC 8470 定义的
Early-Data: 1头),以便应用返回425 Too Early。 - 除非你已经把重放问题想清楚,否则不要把 0-RTT 用于需要认证的操作。
双向 TLS
在双向 TLS(mTLS)中,服务器也会向客户端索要证书。服务器在 EncryptedExtensions 之后加入一条 CertificateRequest,客户端则在自己的 Finished 之前,用自己的 Certificate 和 CertificateVerify 作答。往返次数不变。
mTLS 是服务网格中服务之间的默认做法:每个工作负载都获得一张短期证书,由 sidecar 或代理处理握手。服务网格架构图展示了这些证书从何而来,零信任架构图则解释了为什么每个请求的身份比请求来自哪个网络更重要。
隐私与后量子密钥:2026 年的新变化
最近的两份 RFC 改变了握手在线路上的样子。
Encrypted Client Hello。 ClientHello 是在还没有任何密钥时发送的,因此 SNI,也就是客户端想访问的主机名,是以明文传输的。2026 年 3 月发布的 RFC 9849 定义了 Encrypted Client Hello(ECH):客户端用服务器在 DNS 中发布的密钥,加密一个包含真实主机名的内部 ClientHello。观察者只能看到一个公开的外部名称,通常属于某家大型服务商。
混合后量子密钥交换。 今天被录下的流量,将来可能被量子计算机解密,所以密钥交换率先迁移。2026 年 8 月发布的 RFC 10024 定义了 X25519MLKEM768 这样的混合方案,它把 X25519 和 ML-KEM-768 结合在一起。两份密钥共享都会进入密钥调度,因此只要其中任一算法仍然可靠,连接就是安全的。Chrome 和 Edge 131 及更高版本、Firefox 132 及更高版本默认提供这一方案。
代价是体积。X25519MLKEM768 的客户端密钥共享为 1,216 字节,服务器密钥共享为 1,120 字节,而单纯的 X25519 各只有 32 字节。ClientHello 现在可能跨越两个数据包,这暴露了一些以为这种情况永远不会发生的中间设备。
往返的代价
决定连接多快就绪的是往返次数,而不是带宽。假设客户端和服务器之间的往返时间为 100 ms,不计 DNS:
| 方案 | 收到响应前的往返次数 | 时间 |
|---|---|---|
| TCP + TLS 1.2 + 请求 | 4 | 400 ms |
| TCP + TLS 1.3 + 请求 | 3 | 300 ms |
| TCP + TLS 1.3,使用 0-RTT | 2 | 200 ms |
| QUIC(HTTP/3),新连接 | 2 | 200 ms |
| QUIC,使用 0-RTT | 1 | 100 ms |
QUIC 在自己的数据包中承载同样的 TLS 1.3 握手(RFC 9001),并把它与传输层握手合并,多出来的那次往返就是在这里省掉的。在离用户近的地方终止 TLS,比如 CDN 或边缘负载均衡器,可以缩短往返时间本身;TLS 终止通常发生在哪里,见负载均衡器图。
握手图
图表内容
- Client (browser)
- Server
- mutual TLS only
- ClientHello: key_share, supported_versions, SNI, ALPN
- ServerHello: key_share, chosen cipher suite
- {EncryptedExtensions}
- {CertificateRequest} (mTLS only)
- {Certificate}: server chain
- {CertificateVerify}: signature over the transcript
- {Finished}: MAC over the transcript
- {Certificate}: client chain
- {CertificateVerify}
- {Finished}
- [Application data], both directions
- NewSessionTicket (for resumption)
这张图的读法如下:
- 两条生命线。 左边是客户端,右边是服务器,时间自上而下流动。
- 普通箭头以明文发送:ClientHello 和 ServerHello。标有“handshake keys”的那条线以下的一切都经过加密。
- 服务器的这一轮消息一次性带上 EncryptedExtensions、Certificate、CertificateVerify 和 Finished,这正是握手能在一次往返内完成的原因。
- 可选片段里是客户端的 Certificate 和 CertificateVerify,只在双向 TLS 中发送。服务器这一轮中虚线表示的 CertificateRequest 就是用来索要它们的。
- Markdown 块列出了决定握手成败的扩展:版本、组、签名算法、SNI 和 ALPN。
绘制能用来排障的协议图
- 标出加密边界。 关于 TLS 最常见的问题是观察者能看到什么。把这条线画出来。
- 只标出重要的扩展,而不是全部。
key_share和supported_versions能解释大多数失败。 - 把可选路径和备选路径放进片段。 用
opt片段表示客户端证书,用alt片段表示 HelloRetryRequest,主流程就能保持清晰。 - 一个问题一张图。 完整握手、带 0-RTT 的会话恢复和 mTLS 交换,如果各自都需要细节,就是三张图。
时序图示例更深入地介绍了生命线、片段和命名约定。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加两个时序节点包中的 生命线 节点,分别命名为 Client 和 Server。
- 在两条生命线之间自上而下用连线画出消息:ClientHello、ServerHello,然后是服务器的四条加密消息和客户端的 Finished。为每条连线标注消息名称及其关键字段。
- 添加一个时序节点包中的 组合片段,使用
opt运算符,框住客户端的 Certificate 和 CertificateVerify,并把服务器的 CertificateRequest 画成虚线连线。 - 添加一个基础节点包中的 Markdown 块,写上这些扩展和往返次数,再添加一个 便签 标出加密从哪里开始。
这个示例共用 4 个图表节点,在 Free 套餐的限制之内。
常见问题
TLS 1.3 握手需要几次往返? 新连接只需一次,而 TLS 1.2 需要两次。HelloRetryRequest 会增加第二次往返;0-RTT 会话恢复则让客户端在没有任何握手往返的情况下就发出第一个请求。
TLS 1.3 中服务器证书是加密的吗? 是的。ServerHello 之后的一切,包括 Certificate 消息,都用握手密钥加密。ClientHello 没有加密,而 Encrypted Client Hello 正是用来填补这个空白的。
启用 0-RTT 安全吗? 只对可以安全重放的请求是安全的。早期数据可能被截获并重新发送,所以要把它限制在幂等请求上,并让应用用 425 Too Early 拒绝其他一切请求。
TLS 1.3 还支持 RSA 证书吗? 支持。RSA 签名仍然用于认证服务器。TLS 1.3 移除的是 RSA 密钥传输:在那种方式下,会话秘密用服务器的 RSA 密钥加密,一旦密钥泄露,过去的流量就会暴露。
TLS 和 mTLS 有什么区别? 在普通 TLS 中,只有服务器出示证书。在双向 TLS 中,服务器也会向客户端索要证书,这样双方在同一次握手中都完成认证。