TLS 1.3 握手过程详解:1-RTT 是怎么做到的
对比 TLS 1.2,梳理 TLS 1.3 握手中每条消息的作用,并用 SSLKEYLOGFILE 在 Wireshark 里解密握手后的加密内容。
- DATE
- LEN
- 982 字
- TTL
- 3 min
- LAYER
- L6 · 表示层
TLS 1.3(RFC 8446)相比 1.2 做了大刀阔斧的简化:握手从 2-RTT 缩短到 1-RTT,删掉了 RSA 密钥交换、静态 DH 和一堆不安全的密码套件,而且从 ServerHello 之后的握手消息就全部加密了。这让抓包分析变难了,也更有意思了。
记录层
先看最外层的 TLS 记录(Record)头部,只有 5 个字节:
- Content Type:
22握手(Handshake)、23应用数据(Application Data)、21告警(Alert)、20ChangeCipherSpec; - Legacy Version:TLS 1.3 中这个字段被冻结为
0x0303(即 TLS 1.2),真正的版本号放在扩展里; - Length:后面数据的长度。
为什么版本号写的是 1.2?
互联网上有大量中间设备(防火墙、负载均衡)会检查版本号,看到不认识的值就直接丢包。为了“骗过”它们,TLS 1.3 在各处都伪装成 1.2,真实版本通过 supported_versions 扩展协商(0x0304)。
完整握手(1-RTT)
ClientHello:先把密钥材料带上
1.2 的流程是“先商量用什么算法,再交换密钥”,所以需要两个来回。1.3 的思路是:客户端直接猜服务器会用哪种 (EC)DHE 群组,并提前把公钥放进 key_share 扩展。猜中了(绝大多数情况是 X25519),服务器一回复,双方就都能算出共享密钥。
ClientHello 中值得关注的扩展:
| 扩展 | 作用 |
|---|---|
supported_versions | 声明支持 TLS 1.3(0x0304) |
key_share | 客户端的 ECDHE 公钥 |
signature_algorithms | 支持的签名算法 |
server_name(SNI) | 要访问的域名,明文传输 |
psk_key_exchange_modes | 会话恢复(PSK)相关 |
application_layer_protocol_negotiation | ALPN,如 h2、http/1.1 |
笔记
如果服务器不支持客户端猜的群组,会回一个 HelloRetryRequest,让客户端换一个群组重发 ClientHello,这时就退化成了 2-RTT。
ServerHello 之后:一切都加密了
ServerHello 带回服务器的 key_share。从这一刻起,双方用 ECDHE 得到的共享秘密,经过 HKDF 派生出握手流量密钥,后续消息全部加密:
- EncryptedExtensions:不影响密钥协商的其他扩展(如 ALPN 结果);
- Certificate:服务器证书链——在 1.2 中这是明文,1.3 中被加密了;
- CertificateVerify:服务器用证书私钥对整个握手记录的哈希签名,证明自己确实持有私钥;
- Finished:对握手记录的 MAC,确保握手没被篡改。
客户端验证通过后发送自己的 Finished,握手结束。
和 TLS 1.2 比一比
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| 完整握手 | 2-RTT | 1-RTT |
| 密钥交换 | RSA / DHE / ECDHE | 仅 (EC)DHE 和 PSK |
| 前向保密 | 取决于套件 | 始终具备 |
| 证书 | 明文 | 加密 |
| 会话恢复 | Session ID / Ticket | PSK,可选 0-RTT |
0-RTT 的代价
使用 PSK 恢复会话时,客户端可以在第一个报文里就带上应用数据(0-RTT / Early Data)。但这些数据不具备抗重放保护,所以只适合幂等请求,比如 GET。
在 Wireshark 里观察
抓一次 HTTPS 访问后,用这些过滤器定位握手:
tls.handshake.type == 1 # ClientHello
tls.handshake.type == 2 # ServerHello
tls.handshake.extensions_server_name == "example.com" # 按 SNI 过滤
tls.record.content_type == 23 # 加密的应用数据
你会发现:ServerHello 之后只有一串 Application Data 记录——Certificate 等消息也被“伪装”成了应用数据(外层 Content Type 统一写 23,真实类型藏在加密内容的末尾)。
还经常能看到一个孤零零的 ChangeCipherSpec:它在 1.3 中已经没有意义,只是“中间设备兼容模式”下发送的假消息。
用 SSLKEYLOGFILE 解密
浏览器和 curl 支持把会话密钥导出到文件,Wireshark 读取后就能解密:
export SSLKEYLOGFILE="$HOME/sslkeys.log"
curl -s https://example.com/ -o /dev/null
# 或者用同一个终端启动浏览器
然后在 Wireshark 中:Preferences → Protocols → TLS → (Pre)-Master-Secret log filename,选择 sslkeys.log。
刷新后,那些 Application Data 会被解析成真正的 EncryptedExtensions、Certificate 和 HTTP/2 帧。
注意
密钥日志文件能解密对应的所有流量,实验结束后记得删除,也不要在日常浏览环境中长期开启。
小结
- 1-RTT 的秘诀是客户端在 ClientHello 里就发送 key_share;
- 记录层与 ServerHello 的版本号被冻结为 1.2,真实版本靠
supported_versions; - ServerHello 之后的握手消息全部加密,证书也不再明文可见;
- SNI 仍是明文,这正是 ECH(Encrypted Client Hello)要解决的问题,下次再聊。