网络-17 TLS 1.3
TLS 1.3 于 2018 年 8 月由 RFC 8446 定稿,是 TLS 十年来最大的一次改动。它不是在 1.2 上加功能,而是做了一次相当激进的删减——把历史包袱里所有不安全的选项直接从协议里拿掉了。
前置阅读:HTTPS。那篇里"TLS握手流程"一节讲的是 TLS 1.2 的 RSA 密钥交换,本文讲它被替换成了什么。
1. 相比 TLS 1.2 改了什么
一句话概括:完整握手从 2-RTT 降到 1-RTT,删除 RSA 静态密钥交换和旧算法,默认走具备前向保密的临时 (EC)DHE。 会话恢复的 PSK-only 模式是一个需要单独说明的例外。
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| 握手延迟 | 2-RTT | 1-RTT,会话恢复可 0-RTT |
| 密钥交换 | RSA / DH / ECDHE 都允许 | 首次完整握手使用临时 (EC)DHE;恢复支持 PSK 或 PSK+(EC)DHE |
| 前向保密 | 可选(取决于套件) | 强制 |
| 密码套件数量 | 300+ 种组合 | 5 种 |
| 对称加密 | 允许 CBC、RC4、3DES | 只允许 AEAD |
| 握手加密范围 | 证书等信息明文 | ServerHello 之后全部加密 |
| 会话恢复 | Session ID / Session Ticket | 统一为 PSK |
2. 删掉了什么
这是理解 TLS 1.3 最快的角度。以下内容从协议中彻底移除,不是"不推荐"而是"不存在":
- RSA 密钥交换——它是缺乏前向保密的根源。移除它意味着服务器私钥泄露不再危及历史流量。
- 静态 DH(非临时的 DH)——同理,没有前向保密。
- RC4、3DES、DES——已被证明不安全的分组/流密码。
- CBC 模式——一系列 padding oracle 攻击(BEAST、Lucky13)的温床。1.3 只保留 AEAD(AES-GCM、AES-CCM、ChaCha20-Poly1305),加密和完整性校验一体完成。
- SHA-1、MD5——已被攻破的摘要算法。
- TLS 层压缩——CRIME 攻击的成因。
- 重协商(Renegotiation)——历史上多次漏洞来源,改用 Key Update 机制替代。
- 自定义 DHE 参数组——1.2 里服务器可以自选 DH 组,配置不当(组太小、非安全素数)就会出问题(Logjam 攻击)。1.3 只允许从预定义的少数几个组里选。
密码套件也被重新定义了。TLS 1.2 的套件名把四件事绑在一起:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
└密钥交换┘ └签名┘ └对称加密┘ └摘要┘
组合爆炸导致了 300 多种套件,其中大量是不该用的。TLS 1.3 的套件只描述"对称加密 + 摘要",密钥交换和签名算法改为独立协商:
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256
总共就这 5 种。 这种"少即是安全"的设计思路,是 TLS 1.3 最值得学习的地方——把错误配置的可能性从协议层面消灭掉,而不是指望每个运维都懂密码学。
3. 1-RTT 握手
TLS 1.2 的握手要两个往返:第一个 RTT 协商算法(ClientHello / ServerHello),第二个 RTT 交换密钥(ClientKeyExchange / Finished)。
TLS 1.3 的关键洞察是:既然密钥交换只剩 (EC)DHE 一种,而可选的 DH 组也只有几个,那客户端完全可以"赌一把",在第一个包里就把 DH 公钥带上。
客户端 服务端
| |
| ClientHello |
| + supported_versions (我支持 1.3) |
| + key_share (我猜你用 X25519,这是我的公钥) |
| --------------------------------------------> |
| |
| ServerHello |
| + key_share (我的 DH 公钥) |
| ---- 以下内容已加密 ---- |
| {EncryptedExtensions} |
| {Certificate} |
| {CertificateVerify} |
| {Finished} |
| <-------------------------------------------- |
| |
| {Finished} |
| [Application Data] ← 客户端此时即可发数据 |
| --------------------------------------------> |
双方在第一个往返里就交换完了 DH 公钥,各自算出共享秘密,第二个包开始就已经是加密的了。注意 {Certificate} 也在加密范围内——TLS 1.2 里证书是明文传输的,中间设备能看到你访问的是哪个域名对应的证书,1.3 把它藏起来了(SNI 仍是明文,需要 ECH 扩展来解决)。
如果客户端猜错了 DH 组,服务端会回一个 HelloRetryRequest 要求重猜,退化成 2-RTT。实践中客户端优先猜 X25519,命中率很高。
4. 0-RTT 与它的风险
对于之前连接过的服务器,客户端手里有上次握手留下的 PSK(Pre-Shared Key)。TLS 1.3 允许它在第一个包里就带上应用数据,用 PSK 派生的密钥加密:
ClientHello + key_share + pre_shared_key + [Early Data] ---->
延迟直接降到 0-RTT——数据和握手同时出发。
但 0-RTT 有一个无法根治的缺陷:不防重放。
原因很直接:服务端在收到这个包时还没有和客户端完成任何交互,没有任何"新鲜性"信息可以用来判断这是首次发送还是攻击者录制后重放的。服务端可以维护一个已用 PSK 的集合来缓解,但在多机分布式部署下同步这个集合的成本很高,且时间窗口内仍有漏洞。
所以规范明确要求:0-RTT 只能用于幂等操作。实践中的做法是只允许 GET 这类安全方法走 0-RTT,POST/PUT/DELETE 一律等握手完成。如果你在 Nginx 上开 ssl_early_data on;,务必配合:
ssl_early_data on;
# 让后端能识别这是 early data 请求,以便拒绝非幂等操作
proxy_set_header Early-Data $ssl_early_data;
否则一次转账请求被重放两次就不是延迟优化的问题了。
5. 中间设备兼容
TLS 1.3 部署初期遇到一个现实问题:大量老旧的防火墙、负载均衡器会对 TLS 握手做深度检查,看到不认识的握手格式直接掐断连接。
规范因此加入了 middlebox compatibility mode:TLS 1.3 的握手包在外观上故意伪装成 TLS 1.2——ClientHello 里的 legacy_version 字段仍然写 0x0303(即 1.2),真实版本藏在 supported_versions 扩展里;还会发送无意义的 ChangeCipherSpec 记录,只为让中间设备觉得"这是我认识的 1.2 握手"。
这是个很有意思的工程妥协:协议为了能被部署,不得不对既有基础设施撒谎。 同样的思路在 QUIC 里也能看到(见 HTTP/3 与 QUIC)。
6. 实战
查看协商结果
# 直连测试,看 Protocol 和 Cipher 两行
openssl s_client -connect www.cloudflare.com:443 -tls1_3 </dev/null 2>/dev/null \
| grep -E "Protocol|Cipher"
# curl 强制 1.3
curl -v --tlsv1.3 --tls-max 1.3 https://www.cloudflare.com -o /dev/null
Nginx 配置
# 1.3 需要 OpenSSL 1.1.1+ 编译
ssl_protocols TLSv1.2 TLSv1.3;
# 注意:这个指令只对 1.2 及以下生效,1.3 的套件不由它控制
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# 1.3 下建议关掉,让客户端自己选(移动端选 ChaCha20 更省电)
ssl_prefer_server_ciphers off;
Go
Go 从 1.13 起默认启用 TLS 1.3。有一个容易踩的坑:
cfg := &tls.Config{
MinVersion: tls.VersionTLS13,
// 注意:这个字段对 TLS 1.3 完全无效!
// Go 有意不允许配置 1.3 的密码套件,因为 5 种都是安全的,
// 给出配置入口只会让人有机会配错。
CipherSuites: []uint16{...}, // ← 1.3 下被忽略
}
这个设计和 TLS 1.3 本身"减少可配置项"的哲学是一致的。想确认实际协商结果,在握手完成后读 ConnectionState:
resp, _ := client.Get("https://example.com")
state := resp.TLS
fmt.Println(tls.VersionName(state.Version)) // TLS 1.3
fmt.Println(tls.CipherSuiteName(state.CipherSuite)) // TLS_AES_128_GCM_SHA256
fmt.Println(state.DidResume) // 是否走了会话恢复
7. 小结
TLS 1.3 的改进可以归到三条主线:
- 更快:1-RTT 起步,会话恢复 0-RTT,握手延迟减半;
- 更安全:完整握手使用临时 (EC)DHE,只留 AEAD,握手加密范围扩大到证书;PSK-only 恢复模式不提供前向保密;
- 更简单:删掉几十年积累的可选项,把"配错的可能性"从协议层面消除。
第三条最容易被忽略,但它才是前两条能真正落地的原因——TLS 1.2 时代绝大多数安全事故不是算法被攻破,而是配置错了。
8. 本章面试题
TLS 1.3 相比 1.2 有哪些改进?
三条主线:
- 更快——握手从 2-RTT 降到 1-RTT,会话恢复可 0-RTT;
- 更安全——删除 RSA 静态密钥交换,完整握手使用临时 (EC)DHE;只保留 AEAD 加密,握手加密范围扩大到证书。补充:PSK-only 恢复模式不提供前向保密,PSK+(EC)DHE 才提供;
- 更简单——密码套件从 300+ 种精简到 5 种,删掉几十年积累的不安全选项。
第三条最容易被忽略但最关键:TLS 1.2 时代绝大多数安全事故不是算法被攻破,而是配置错了。把错误配置的可能性从协议层面消除,才是前两条能落地的原因。
什么是前向保密?为什么 TLS 1.3 强制它?
前向保密(Forward Secrecy):即使服务器私钥日后泄露,攻击者也无法解密之前录下的历史流量。
- RSA 密钥交换(无前向保密):客户端用服务器公钥加密 Premaster secret 发过去。整个会话密钥完全由这次 RSA 加密保护——私钥泄露后,所有历史流量都能被解开。
- (EC)DHE 协商(有前向保密):双方交换临时公钥,各自算出共享秘密,这个秘密从未在网络上传输。服务器私钥只用于签名(证明身份),不参与密钥传输。密钥对是一次性的(E = Ephemeral),会话结束即丢弃。
TLS 1.3 直接把 RSA 密钥交换从协议中删除了,前向保密从"最佳实践"变成"强制要求"。
代价:抓包调试变难——有服务器私钥也解不出流量,只能靠 SSLKEYLOGFILE(见 网络-21)。
TLS 1.3 为什么能做到 1-RTT?
关键洞察:既然密钥交换只剩 (EC)DHE 一种,可选的 DH 组也只有几个,客户端完全可以"赌一把"——在第一个包(ClientHello)里就把自己的 DH 公钥(key_share)带上。
服务端在 ServerHello 里回自己的 key_share,双方立即各自算出共享秘密,第二个包开始就已经加密了(证书等信息都在加密范围内)。
如果客户端猜错了 DH 组,服务端回 HelloRetryRequest 要求重猜,退化成 2-RTT。实践中优先猜 X25519,命中率很高。
0-RTT 有什么风险?怎么用才安全?
风险:无法防重放。
原因很直接——服务端收到这个包时还没和客户端完成任何交互,没有任何"新鲜性"信息可以判断这是首次发送还是攻击者录制后重放的。服务端可以维护已用 PSK 集合来缓解,但分布式部署下同步成本很高,时间窗口内仍有漏洞。
规范明确要求:0-RTT 只能用于幂等操作。 实践中只允许 GET 这类安全方法,POST/PUT/DELETE 一律等握手完成。
Nginx 开启时必须配合传递标识,让后端能拒绝非幂等操作:
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;
这与 HTTP 方法的幂等性是同一个问题,见 网络-12。
TLS 1.3 删掉了哪些东西?为什么"删"这么重要?
彻底移除(不是"不推荐"而是"不存在"):RSA 密钥交换、静态 DH、RC4/3DES/DES、CBC 模式、SHA-1/MD5、TLS 层压缩、重协商、自定义 DH 参数组。
每一项都对应过历史漏洞:CBC → BEAST/Lucky13,压缩 → CRIME,自选 DH 组 → Logjam,RSA 密钥交换 → 无前向保密。
密码套件也重新定义了——TLS 1.2 的套件名把密钥交换、签名、对称加密、摘要四件事绑在一起,组合爆炸出 300 多种;TLS 1.3 的套件只描述"对称加密 + 摘要",密钥交换和签名独立协商,总共只有 5 种。
“少即是安全”:与其指望每个运维都懂密码学,不如从协议层面让人无法配错。Go 的 tls.Config.CipherSuites 对 TLS 1.3 完全无效也是同样的设计哲学。
参考资料:
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 7568 — Deprecating Secure Sockets Layer Version 3.0
xingliuhua