入门基础
TLS 1.3 握手详解:逐条消息拆开看
TLS 1.3 握手只有 1 个往返:客户端发出带 key share 的 ClientHello,服务器回 ServerHello 和一组加密消息(EncryptedExtensions、Certificate、CertificateVerify、Finished),客户端再回一个 Finished 就完成了。ServerHello 之后的内容全部加密。消息定义以现行规范 RFC 9846 为准,它在 2026 年 7 月取代了最初的 RFC 8446,版本号不变、向后兼容。
完整握手全景图
TLS 1.3 完整握手共三轮发送,下图包含可选的客户端认证。花括号表示用握手密钥加密,方括号表示用应用数据密钥加密,* 表示可选消息。
客户端 服务器
ClientHello
+ supported_versions, key_share, signature_algorithms,
+ server_name, ALPN, psk_key_exchange_modes,
+ pre_shared_key*, early_data* -------->
(0-RTT [应用数据*]) -------->
ServerHello
+ supported_versions
+ key_share
+ pre_shared_key*
{EncryptedExtensions}
{CertificateRequest*}
{Certificate*}
{CertificateVerify*}
{Finished}
<-------- [应用数据*]
{EndOfEarlyData*}
{Certificate*}
{CertificateVerify*}
{Finished} -------->
<-------- [NewSessionTicket*]
[应用数据] <-------> [应用数据]
图里出现三组密钥:早期数据密钥(只给 0-RTT 用)、握手密钥(ServerHello 到 Finished 之间)、应用数据密钥(Finished 之后)。它们都出自同一个密钥调度过程,后文有图。
ClientHello:客户端的开场报价
ClientHello 把服务器选参数、算密钥所需的东西一次带齐。固定字段大多是历史包袱,真正的协商都在扩展里。
| 字段或扩展 | 在 TLS 1.3 中的作用 |
|---|---|
legacy_version |
为了兼容固定写 0x0303(TLS 1.2),真实版本看 supported_versions |
random |
32 字节随机数,进入握手记录哈希 |
legacy_session_id |
兼容模式下是随机 32 字节,否则为空 |
cipher_suites |
TLS 1.3 套件如 TLS_AES_128_GCM_SHA256;仍支持 1.2 的客户端还会列出 1.2 套件 |
supported_versions |
支持的版本,如 0x0304(TLS 1.3)、0x0303(TLS 1.2) |
supported_groups |
可接受的密钥交换组,按偏好排序(X25519MLKEM768、X25519、P-256……) |
key_share |
针对其中一个或几个组的真实公钥,相当于押注服务器会选哪个 |
signature_algorithms |
客户端能验证的签名算法,如 ecdsa_secp256r1_sha256、rsa_pss_rsae_sha256、ed25519 |
server_name |
要访问的域名(SNI,RFC 6066),服务器据此选证书 |
application_layer_protocol_negotiation |
ALPN 列表,如 h2、http/1.1 |
psk_key_exchange_modes |
会话恢复时能否跳过 (EC)DHE(psk_ke)还是必须带上(psk_dhe_ke) |
pre_shared_key |
用于会话恢复的票据标识,必须是最后一个扩展 |
early_data |
表示后面跟着 0-RTT 数据 |
浏览器还会塞进 GREASE 值(RFC 8701):一些随机的保留编号,用来逼服务器正确忽略不认识的值,避免协议“锈死”。
key share 让 ClientHello 比以前大了很多。用 OpenSSL 3.6 实测,默认同时发送 X25519MLKEM768 和 X25519 两个 key share,ClientHello 共 1533 字节,其中 key_share 扩展就占 1258 字节:混合 share 是 1184 字节的 ML-KEM-768 公钥加 32 字节的 X25519 公钥。ClientHello 超出一个 TCP 报文段后,浏览器刚启用混合密钥交换时曾让一些有缺陷的中间设备出问题,详见后量子 TLS。
HelloRetryRequest:押错了 key share
如果客户端带的 key share 服务器一个都不支持,但客户端列出的其他组里有服务器支持的,服务器就回一个 HelloRetryRequest,客户端换上对应的 key share 重发 ClientHello,握手因此多 1 个往返。
HelloRetryRequest 在线路上其实是一条 ServerHello,只是它的 random 是一个固定的特殊值(字符串 "HelloRetryRequest" 的 SHA-256)。它可以带 cookie 扩展让客户端原样带回,方便服务器做无状态处理。为了控制握手记录的长度,第一条 ClientHello 在哈希里会被替换成一条合成的 message_hash 消息。
下面是对一台只接受 P-384 的测试服务器的实测(OpenSSL 3.6,-msg 输出,去掉了记录头行):
>>> TLS 1.3, Handshake [length 0601], ClientHello
<<< TLS 1.3, Handshake [length 0058], ServerHello <- 这条是 HelloRetryRequest
>>> TLS 1.3, Handshake [length 017e], ClientHello <- 换成 secp384r1 重发
<<< TLS 1.3, Handshake [length 00bb], ServerHello
<<< TLS 1.3, Handshake [length 0006], EncryptedExtensions
<<< TLS 1.3, Handshake [length 0193], Certificate
<<< TLS 1.3, Handshake [length 004f], CertificateVerify
<<< TLS 1.3, Handshake [length 0034], Finished
>>> TLS 1.3, Handshake [length 0034], Finished
服务器应当支持主流客户端会直接发 share 的组(X25519;截至 2026 年 11 月,X25519MLKEM768 也越来越普遍),避免这笔额外开销。
ServerHello:服务器拍板
ServerHello 确定密码套件并给出服务器的 key share,之后双方都能算出握手秘密。它包含:
cipher_suite:从客户端列表里选中的一个 TLS 1.3 套件;supported_versions:0x0304,即真实协议版本;key_share:服务器在所选组上的公钥。X25519MLKEM768 下它是 1120 字节,即 1088 字节的 ML-KEM 密文加 32 字节 X25519 公钥;pre_shared_key:会话恢复时接受了哪张票据。
服务器的 random 还承担降级保护:支持 TLS 1.3 的服务器如果最终协商成 TLS 1.2 或更低版本,会把最后 8 字节写成 DOWNGRD 加 01 或 00,支持 TLS 1.3 的客户端看到就中止连接。random 受服务器签名保护,攻击者无法抹掉这个标记。
EncryptedExtensions
EncryptedExtensions 是第一条加密消息,装的是所有不参与密钥计算的协商结果:服务器选定的 ALPN 协议(如 h2)、对 server_name 的空确认、服务器的 supported_groups 偏好(供下次参考)、是否接受 0-RTT 的 early_data 等。这些信息挪出 ServerHello 后,被动观察者就看不到了。
CertificateRequest(可选)
只有服务器要求客户端证书时才发 CertificateRequest,也就是双向 TLS 的场景。它包含 certificate_request_context、服务器接受的 signature_algorithms,以及可选的 certificate_authorities(可接受的签发者列表)。客户端如果声明了 post_handshake_auth,TLS 1.3 还允许握手完成后再要证书。
Certificate
Certificate 消息携带服务器证书链:先是站点证书,再是中间证书。每个证书条目都可以带自己的扩展,所以 OCSP 装订响应(status_request)和 SCT 时间戳会挂在对应证书上一起发送。根证书一般不发,因为客户端本来就得信任它;漏发中间证书则是缺少中间证书报错的经典原因。证书链还可以用 RFC 8879 压缩,比如 Chrome 会协商 Brotli 压缩。
CertificateVerify
CertificateVerify 证明服务器确实持有证书私钥。服务器对“64 个空格 + 上下文字符串 TLS 1.3, server CertificateVerify + 0 字节 + 目前为止的握手哈希”签名,上下文字符串防止签名被挪用到别的协议。使用 RSA 密钥的服务器必须用 RSA-PSS 签这条消息(rsa_pss_rsae_sha256 等),哪怕证书本身是 PKCS#1 v1.5 签的。客户端这边,RFC 9963(2026 年 4 月)新增了 rsa_pkcs1_sha256_legacy 等编号,让只能做 PKCS#1 v1.5 签名的智能卡、U 盾等硬件也能在 TLS 1.3 下完成客户端认证,前提是服务器在 CertificateRequest 里提供这些编号。
Finished
Finished 是用握手密钥派生出的 finished_key 对整个握手记录做的 HMAC。它确认双方看到的消息完全一致、算出的密钥也一致;ClientHello 被篡改或有人试图降级,这一步都会失败。服务器发完 Finished 就可以开始发应用数据(所谓 0.5-RTT 数据),因为此时它已经有了密钥,只是还没验证客户端身份。
客户端的第二轮发送
客户端验证证书链、域名和 CertificateVerify 签名,校验服务器的 Finished,然后发送:
EndOfEarlyData:只有发过且被接受了 0-RTT 数据时才发;Certificate和CertificateVerify:只有服务器发过 CertificateRequest 时才发,没有证书的客户端发一条空的 Certificate;Finished。
应用数据可以和这一轮一起发出,所以 HTTP 请求在 ClientHello 之后 1 个往返就上路了。
NewSessionTicket 与会话恢复
握手完成后,服务器可以发一张或多张 NewSessionTicket。每张票据是一段不透明数据,附带有效期(最长 604800 秒,即 7 天)、年龄混淆值和 nonce。下次连接时客户端把票据放进 pre_shared_key。使用 psk_dhe_ke 模式时,恢复握手仍会做一次新的 (EC)DHE,保留前向保密,而服务器可以省掉 Certificate 和 CertificateVerify。见会话恢复。
多台服务器挂在负载均衡后面时(比如阿里云 SLB、腾讯云 CLB 后面的多台 Nginx),票据加密密钥要么在各台之间同步,要么在负载均衡上统一终止 TLS,否则请求落到另一台机器上就恢复不了会话。
0-RTT 早期数据
持有允许 0-RTT 的票据时,客户端可以在第一轮就发应用数据,用客户端早期数据密钥加密。省下一个往返,但有两个弱点:它相对票据密钥不具备前向保密,而且攻击者可以把它重放给服务器。TLS 1.3 规范建议服务器做防重放处理,RFC 8470 规定了 HTTP 的处理方式:服务器可以返回 425 Too Early,代理转发早期数据时加上 Early-Data: 1 头。只有 GET 这类幂等请求才适合用 0-RTT 发送。
一张图看懂密钥调度
TLS 1.3 的所有密钥都来自三次 HKDF-Extract(RFC 5869)。简化如下:
PSK(没有就用全 0)
|
v
HKDF-Extract ----> Early Secret ----> binder key、client_early_traffic_secret
|
Derive-Secret("derived")
|
(EC)DHE ----> HKDF-Extract ----> Handshake Secret
| +--> client_handshake_traffic_secret(CH..SH 的哈希)
| +--> server_handshake_traffic_secret
Derive-Secret("derived")
|
全 0 ----> HKDF-Extract ----> Main Secret
+--> client/server_application_traffic_secret_0
| (CH..服务器 Finished 的哈希)
+--> exporter_secret
+--> resumption_secret -> 票据 PSK
每个流量密钥都绑定在某一时刻的握手哈希上,密钥因此和实际交换的消息一一对应。长连接后续还可以用 KeyUpdate 消息把应用密钥向前滚动更新。
图中用的是 RFC 9846 的命名:它把 RFC 8446 里带 "master" 的叫法改成了 "main" 或更短的名字,Master Secret 改称 Main Secret,exporter_master_secret、resumption_master_secret 改称 exporter_secret、resumption_secret。RFC 9846 还禁止在不同连接之间复用 key share,限制了 KeyUpdate 消息的数量,并把“在达到密钥使用上限之前发起 KeyUpdate”提升为必须遵守的要求。
TLS 1.2 与 TLS 1.3 握手对比
| TLS 1.2(RFC 5246,现已被 RFC 9846 取代) | TLS 1.3(RFC 8446,现为 RFC 9846) | |
|---|---|---|
| 完整握手往返数 | 2 | 1(遇到 HelloRetryRequest 为 2) |
| 会话恢复带数据 | 1 个往返 | 可以 0-RTT |
| 密钥交换 | RSA、DHE 或 ECDHE,放在 ServerKeyExchange | (EC)DHE 或混合算法,放在 key_share;没有静态 RSA |
| 证书对旁观者可见 | 可见,明文发送 | 不可见,加密发送 |
| 服务器如何证明持有私钥 | 签 ServerKeyExchange,或用 RSA 解密 | CertificateVerify 签整段握手记录 |
| ChangeCipherSpec | 必需 | 只是兼容中间设备的假消息 |
| 重协商 | 支持 | 删除,由 KeyUpdate 和握手后认证代替 |
用 OpenSSL 观察握手
openssl s_client -msg 会打印每条握手消息。下面是对一台运行 OpenSSL 3.6、使用 ECDSA 证书的测试服务器的实测输出(去掉了记录头行):
openssl s_client -connect example.com:443 -servername example.com -msg </dev/null 2>&1 | grep -E '^(<<<|>>>)'
>>> TLS 1.3, Handshake [length 0601], ClientHello
<<< TLS 1.3, Handshake [length 04ba], ServerHello
<<< TLS 1.3, ChangeCipherSpec [length 0001]
<<< TLS 1.3, Handshake [length 0006], EncryptedExtensions
<<< TLS 1.3, Handshake [length 0193], Certificate
<<< TLS 1.3, Handshake [length 004f], CertificateVerify
<<< TLS 1.3, Handshake [length 0034], Finished
>>> TLS 1.3, ChangeCipherSpec [length 0001]
>>> TLS 1.3, Handshake [length 0034], Finished
-trace(OpenSSL 编译时带 enable-ssl-trace 才有,Homebrew 版本带了)会逐个解码扩展:
ClientHello, Length=1533
extension_type=server_name(0), length=16
extension_type=supported_groups(10), length=18
extension_type=signature_algorithms(13), length=54
extension_type=supported_versions(43), length=5
extension_type=psk_key_exchange_modes(45), length=2
extension_type=key_share(51), length=1258
NamedGroup: X25519MLKEM768 (4588)
NamedGroup: ecdh_x25519 (29)
ServerHello, Length=1206
cipher_suite {0x13, 0x02} TLS_AES_256_GCM_SHA384
extension_type=supported_versions(43), length=2
extension_type=key_share(51), length=1124
NamedGroup: X25519MLKEM768 (4588)
CentOS 7 自带的 OpenSSL 1.0.2 不支持 TLS 1.3,看不到这些消息;macOS 自带的是 LibreSSL,参数也不完全相同。想在 Wireshark 里看加密部分,给 s_client 加 -keylogfile keys.txt,或给浏览器设置 SSLKEYLOGFILE 环境变量,再把文件导入 Wireshark。更多命令见连接测试命令手册。
常见问题
TLS 1.3 握手要几个往返?
完整握手 1 个往返;服务器需要发 HelloRetryRequest 时是 2 个。会话恢复配合 0-RTT 可以在第一个包里就带上应用数据。TLS 1.2 完整握手需要 2 个往返。
TLS 1.3 里服务器证书是加密传输的吗?
是的。Certificate 消息在 ServerHello 之后发送,用握手密钥加密,被动抓包看不到证书链。但 ClientHello 里的 SNI 域名仍是明文,除非启用了 ECH。
抓包看到 TLS 1.3 连接里有 ChangeCipherSpec,正常吗?
正常。这是中间设备兼容模式:双方故意发一条无意义的 ChangeCipherSpec,让连接在老防火墙眼里像 TLS 1.2 的会话恢复。在 TLS 1.3 里它没有任何密码学作用。
怎么用 Wireshark 解密 TLS 1.3 握手?
让客户端导出密钥日志:openssl s_client 加 -keylogfile keys.txt,浏览器或 curl 设置环境变量 SSLKEYLOGFILE,然后在 Wireshark 的 TLS 协议设置里指定这个文件。只在自己的测试环境这样做,密钥日志等同于会话密钥。