TLS 1.3 握手过程详解:1-RTT 是怎么做到的

对比 TLS 1.2,梳理 TLS 1.3 握手中每条消息的作用,并用 SSLKEYLOGFILE 在 Wireshark 里解密握手后的加密内容。

DATE
LEN
982 字
TTL
3 min

TLS 1.3(RFC 8446)相比 1.2 做了大刀阔斧的简化:握手从 2-RTT 缩短到 1-RTT,删掉了 RSA 密钥交换、静态 DH 和一堆不安全的密码套件,而且从 ServerHello 之后的握手消息就全部加密了。这让抓包分析变难了,也更有意思了。

记录层

先看最外层的 TLS 记录(Record)头部,只有 5 个字节:

HDRTLS Record Header
012345678910111213141516171819202122232425262728293031
Content Type8
Legacy Version16
Length16
Length(续)
Fragment(握手消息 / 加密数据)
  • Content Type:22 握手(Handshake)、23 应用数据(Application Data)、21 告警(Alert)、20 ChangeCipherSpec;
  • Legacy Version:TLS 1.3 中这个字段被冻结为 0x0303(即 TLS 1.2),真正的版本号放在扩展里;
  • Length:后面数据的长度。

为什么版本号写的是 1.2?

互联网上有大量中间设备(防火墙、负载均衡)会检查版本号,看到不认识的值就直接丢包。为了“骗过”它们,TLS 1.3 在各处都伪装成 1.2,真实版本通过 supported_versions 扩展协商(0x0304)。

完整握手(1-RTT)

SEQTLS 1.3 完整握手
Client Server ClientHello(key_share, supported_versions, SNI) ServerHello(key_share) 双方已算出握手密钥,以下 {…} 为加密消息 {EncryptedExtensions} {Certificate} {CertificateVerify} {Finished} {Finished} 应用数据密钥就绪 [Application Data] [Application Data]

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_negotiationALPN,如 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.2TLS 1.3
完整握手2-RTT1-RTT
密钥交换RSA / DHE / ECDHE仅 (EC)DHE 和 PSK
前向保密取决于套件始终具备
证书明文加密
会话恢复Session ID / TicketPSK,可选 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)要解决的问题,下次再聊。
↑↓ 选择 · Enter 打开 · Esc 关闭