03:TLS 1.3 vs 1.2——少了四步,中间人更难了

📅 2026/7/30 20:32:08 👁️ 阅读次数 📝 编程学习
03:TLS 1.3 vs 1.2——少了四步,中间人更难了

大家好,我是毛衣哥。上一期我们把 HTTP 扒光了,这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子,七步拆完。

上一期我们把 TLS 1.2 的七步握手拆成了零件。这一期我们看看 TLS 1.3 改了什么——以及这些改动对中间人意味着什么。

如果你已经读懂了 TLS 1.2 的七步握手,那理解 TLS 1.3 就太容易了。因为 1.3 要做的事情只有一件:

什么都加密,能少跑一趟就少跑一趟。


先问一个问题:TLS 1.2 有什么不好?

TLS 1.2 从 2008 年用到 2018 年,十年间没出过大问题。但它有几个设计上的"历史包袱":

问题一:两轮往返(2-RTT)太慢了

TLS 1.2 握手的完整流程:

客户端 → 服务器:ClientHello 服务器 → 客户端:ServerHello + Certificate + ServerKeyExchange + ServerHelloDone 客户端 → 服务器:ClientKeyExchange + ChangeCipherSpec + Finished 服务器 → 客户端:ChangeCipherSpec + Finished ↑ 到这里才能发第一个 HTTP 请求

数一下:从客户端发出 ClientHello,到服务器发出 ChangeCipherSpec,经历了两次往返(2-RTT)。

对于远距离网络(比如中国到美国,RTT 可能 200ms),两次往返就是 400ms 的额外延迟——这还没算上服务器的处理时间。

问题二:太多"遗骸"

TLS 1.2 支持 37 种密码套件。但实际上常用的就那么三四种。剩下的 30 多种,要么是安全强度不够的旧算法(RC4、3DES),要么是没必要的变体。

多一个选项就多一个攻击面。历史上 TLS 的漏洞有相当一部分是由于"向后兼容旧版"导致的。

问题三:握手消息大部分是明文

TLS 1.2 里,从 ClientHello 到 ServerHello 到 Certificate,全是明文。中间人可以读到:

  • 你访问的域名(SNI)
  • 你用的浏览器/操作系统特征(通过密码套件列表和 TLS 扩展的排列组合可以识别,这叫 TLS 指纹)
  • 你的证书内容
  • 你的服务器配置

TLS 1.3 做了什么手术

改动一:砍密码套件,从 37 个砍到 5 个

TLS 1.3 的密码套件只有 5 种——准确地说,实际生效的只有 3 种:

TLS_AES_128_GCM_SHA256 (0x1301) TLS_AES_256_GCM_SHA384 (0x1302) TLS_CHACHA20_POLY1305_SHA256 (0x1303)

名字变短了。因为 TLS 1.3 把密钥交换算法和签名算法从密码套件里拆了出去——密码套件只包含:

  • 对称加密算法:AEAD(Authenticated Encryption with Associated Data),同时提供机密性和完整性
  • HMAC 算法:用于密钥派生

密钥交换算法在扩展中协商(比如supported_groups扩展里说你支持 x25519、secp256r1 等)。

为什么砍这么多?因为实践证明,在 TLS 1.2 的 37 个套件里,每年总有那么一两个被发现漏洞,然后推出一堆补丁。与其继续打补丁,不如直接把不安全的算法砍掉。

改动二:握手脚程减半,从 2-RTT 降到 1-RTT

TLS 1.3 的握手简化成 4 个 TCP 包:

// TLS 1.3 握手的伪代码(对比 1.2 的七步) function tls_13_handshake(client_conn, server_conn): // 客户端:第一次发包就同时做两件事 client_hello = ClientHello { version: 0x0304, // TLS 1.3 random: generate_32_bytes(), cipher_suites: [0x1301, 0x1302, 0x1303], // 只有 3 个! extensions: [ key_share: { // 一步到位,DH 公钥夹带了 group: x25519, key_exchange: generate_dh_public() }, supported_versions: [0x0304, 0x0303] // 支持 1.3 和 1.2 ] } send(client_conn, client_hello) // 服务器:计算 → 加密 → 回复 server_hello = receive(client_conn) // 服务器收到了客户端的 KeyShare,已算出握手密钥 handshake_secret = derive_handshake_secret( client_hello.key_share, server_hello.key_share ) // Certificate 是加密的!不像 1.2 是明文 encrypted_cert = receive(client_conn) certificate = decrypt(encrypted_cert, handshake_secret) // 客户端完成 finish = receive(client_conn) verify(finish, handshake_secret) // 验证握手完整性 // 客户端:可以发 HTTP 请求了! http_request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" send_encrypted(client_conn, http_request, traffic_secret) // 总共:4 个往返消息 vs TLS 1.2 的 7 个

简化后的流程:

客户端 → 服务器:ClientHello + KeyShare(客户端第一次发包就带上了自己的 DH 公钥!) 服务器 → 客户端:ServerHello + KeyShare + Certificate + Finished(都是加密的!) 客户端 → 服务器:Finished + 第一条 HTTP 请求(可以跟 Finished 一起发!) 服务器 → 客户端:HTTP 响应

看到了吗?握手和数据传输只隔了一个 RTT。客户端在第三次发包时,已经开始发 HTTP 请求了。而在 TLS 1.2 里,这个点还在发 Finished 确认密钥。

具体变化是:

TLS 1.3 的 ClientHello 多了 KeyShare 扩展:

Extension: key_share (type=0x0033) Key Share Entry: Group: x25519 Key Exchange Type: x25519 Key Exchange Length: 32 Key Exchange: (32 字节的公钥值——客户端第一次发包就带来了!)

客户端第一次说话时,就已经生成了自己的 DH 密钥对,把公钥放进了 ClientHello 里。

服务器收到后,如果它支持这个曲线,直接用自己的 DH 私钥 + 客户端的 DH 公钥计算出握手密钥,立刻回复加密后的 ServerHello + Certificate。

这个跟 TLS 1.2 的本质区别是:在 1.2 里,ClientKeyExchange 是第五步,在 Certificate 之后。服务器必须先发 Certificate 让客户端知道自己的公钥,客户端才能用它加密 Pre-Master Secret。但在 1.3 里,客户端提前发自己的 KeyShare,服务器在发了自己的 KeyShare 后,双方立即就能计算出握手密钥,然后 Certificate 消息是加密的。

改动三:更多握手消息被加密了

在 TLS 1.2 里:

  • ClientHello → 明文
  • ServerHello → 明文
  • Certificate → 明文!
  • ServerKeyExchange → 明文!
  • ClientKeyExchange → RSA 模式下加密,DH 模式下明文
  • Finished → 加密

在 TLS 1.3 里:

  • ClientHello → 明文(但 ECH 正在推进把它也加密)
  • ServerHello → 明文
  • Certificate → 加密了!(用握手密钥加密)
  • 所有后续消息 → 加密

这意味着什么?

在 TLS 1.2 里,中间人不需要解密任何内容就能看到证书。它能看到你用的是 Let’s Encrypt 的证书还是 DigiCert 的证书,你的证书什么时候过期。

在 TLS 1.3 里,Certificate 被加密了。中间人需要持有握手密钥才能看到证书内容。而握手密钥的计算需要双方的 DH 私钥——中间人没有,所以看不到。

这是一个设计上的大进步:1.3 在设计时就把"什么时候加密"这个问题的答案从"握手完成之后"提前到了"可能被攻击的敏感信息产生时"。

改动四:0-RTT——“第一次包就带数据”

如果客户端之前连接过同一个服务器,这就更夸张了。

TLS 1.3 支持 0-RTT(Zero Round Trip Time Resumption):

客户端 → 服务器:ClientHello + KeyShare + 0-RTT Data(加密的 HTTP 请求!) 服务器 → 客户端:ServerHello + KeyShare + Certificate + Finished + HTTP 响应

客户端在第一个 TCP 包里就带了 HTTP 请求。

这怎么做到的?因为第一次连接时,服务器给了客户端一个"会话票证"(Session Ticket),里面包含了一个预共享密钥(PSK)。下次连接时,客户端直接用这个 PSK 加密数据,在第一个包里就发送。

但 0-RTT 有个安全风险——重放攻击。

假设客户端用 0-RTT 发了请求:

POST /transfer HTTP/1.1 Content-Type: application/json {"to": "alice", "amount": 1000}

中间人把这个包复制了一份,再发一遍。服务器收到两个同样的请求(因为它还保有 PSK,可以解密这两个请求),如果服务端没有做幂等处理——钱被转了两次。

所以 0-RTT 只能用于幂等操作(GET 查询),不能用于 POST 转账。


TLS 1.3 的降级保护

这是一个很多人没注意到但非常重要的设计。

假设客户端支持 TLS 1.3,它发了 ClientHello(版本 = 1.3)。

如果服务器只支持 1.2,它回复 ServerHello(版本 = 1.2)。

如果中间人截获了客户端的 ClientHello,把它改成了 1.2(假装客户端只支持 1.2),那么服务器就会回复 1.2 的握手——双方就被降级了。

这叫"降级攻击",中间人把安全的 1.3 降到了可能含有漏洞的 1.2。

TLS 1.3 怎么防这个?

服务器在处理 ClientHello 时,如果在它支持的 1.3 扩展中但客户端说"我只用 1.2",服务器会在 ServerHello 的随机数末尾写入一个特殊的降级标记

TLS 1.2 降级标记:44 4F 57 4E 47 52 44 01("DOWNGRD" + 固定字节) TLS 1.1 降级标记:44 4F 57 4E 47 52 44 00

如果客户端发的是 1.3(它实际支持),但收到 ServerHello 里包含了降级标记——说明服务器被强制降了级。客户端直接终止连接,不跟降级后的服务器握手。

但如果客户端真的只支持 1.2,它不会检查降级标记——它本来就只支持 1.2。所以这个机制既不误伤老客户端,又能防住降级攻击。


对中间人的实际影响

TLS 1.3 让中间人的日子更难了,但没到完全没法过的程度。

更难之处:

  • Certificate 被加密了,中间人没法从握手阶段直接读取证书信息
  • 密码套件选择少了,攻击面缩小了
  • Forward Secrecy 变成标配(RSA 密钥交换被移除)
  • 降级保护让你没法强行降到 1.2

但中间人还是能做这些事:

  • 看到 ClientHello 的 SNI(域名还是明文的,除非 ECH 普及)
  • 看到 IP 地址(总能知道你在跟谁通信)
  • 抓到握手完成后,如果拿到了会话密钥(装好证书 + SSLKEYLOGFILE),看到的解密后的内容量是一样的

不过——对一般用户来说,TLS 1.3 的最大意义不是安全,是快。

打开一个 HTTPS 网站,第一次连接只多花一个 RTT(200ms 内),之后的连接(0-RTT 会话恢复)几乎是瞬间完成。你感觉不到"加密的代价"了。


下期预告:CA 信任链——一张证书怎么做到让全世界都信它。我们会从根证书一直挖到网站证书,看看这 60 年的信任体系到底是怎么运作的。

DumpAny 怎么做:TLS 1.3 的握手速度比 1.2 快了,但对中间人来说反而更复杂了——Certificate 消息是加密的、密码套件少了但更统一了。DumpAny 在实现对 1.3 的支持时,最大的改动是密钥派生逻辑(因为 1.3 的密钥体系跟 1.2 完全不同)。不过对用户来说,这些差异是不可见的——你看到的还是解密后的 HTTP 请求。


服务器客户端服务器客户端TLS 1.2(2-RTT)↑ 这里才能开始发 HTTPClientHelloServerHello + Certificate + KeyExchangeClientKeyExchange + CCS + FinishedCCS + Finished
服务器客户端服务器客户端TLS 1.3(1-RTT)↑ 一步到位ClientHello + KeyShare(把DH公钥夹带了!)ServerHello + KeyShare + Certificate(加密的!)+ FinishedFinished + HTTP 请求