3. TLS 1.2 与 TLS 1.3 的区别与演进

说实话,很多同学刚接触 TLS 时,总觉得版本号就是个数字,升级只是修补漏洞。但在我实际排查接口问题的经历里,版本不匹配恰恰是握手失败的常见原因。TLS 1.3 不是 1.2 的简单补丁,它改了握手的“剧本”。这一节我带你梳理清楚。

3.1 从 1.2 到 1.3:握手流程的瘦身

TLS 1.2 的完整握手要 2-RTT(两个往返)。客户端先发 ClientHello,服务端回 ServerHello + 证书 + ServerHelloDone,然后客户端再发 ClientKeyExchange、ChangeCipherSpec、Finished,服务端再回 ChangeCipherSpec、Finished。你想想看,一次连接光握手就耗掉两趟网络开销,高延迟场景下非常难受。

TLS 1.3 把流程压缩到 1-RTT,甚至 0-RTT(带早期数据)。核心变化是:客户端在 ClientHello 里直接把支持的密钥协商参数(KeyShare)发过去。服务端拿到后当场算出共享密钥,ServerHello 之后就能加密传输了。握手记录从 5-7 次消息减少到 2-3 次。

关键差异总结:1.2 是“先协商、再加密、后验证”;1.3 是“先密钥、再加密、边协商边验证”。

TLS 1.2 vs TLS 1.3 握手流程对比 TLS 1.2(2-RTT) ① ClientHello ② ServerHello + Certificate ③ ServerHelloDone ④ ClientKeyExchange + CCS ⑤ Finished(客户端) ⑥ CCS + Finished(服务端) TLS 1.3(1-RTT) ① ClientHello + KeyShare ② ServerHello + KeyShare ③ Encrypted Extensions ④ Certificate + CertificateVerify ⑤ Finished(服务端) ⑥ Finished(客户端) 1.2 的 CCS 消息在 1.3 中已废弃,所有握手除 ServerHello 外均加密传输

3.2 加密套件的“瘦身”与强迫症

TLS 1.2 时代支持海量密码套件,光是 RSA、DHE、ECDHE、AES-CBC、AES-GCM、CHACHA20 的组合就有几十种。我在项目中遇到过客户端发了一大串密码套件,服务端却选了一个已经废弃的 CBC 模式,结果被中间人攻击搞挂了。

TLS 1.3 直接砍掉了 90% 的套件,只保留 5 种:

  • 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

说白了,1.3 只支持 AEAD 加密模式(Authenticated Encryption with Associated Data),同时强制使用完美的前向安全(PFS)。RSA 密钥交换?再见。CBC 模式?再见。HMAC-SHA1?再见。

个人习惯:我排查握手失败时,第一件事就是看密码套件列表。如果客户端只发了 1.2 的套件(比如 TLS_RSA_WITH_AES_128_CBC_SHA),而服务端只启用了 1.3 套件,那肯定握手失败。很多老旧 SDK 就栽在这上面。

3.3 密钥交换算法:DHE 和 ECDHE 成为唯一选择

1.2 里 RSA 密钥交换是主流,但有个致命伤:服务端私钥一旦泄露,之前所有记录都能被解密。1.3 彻底禁用了 RSA 交换,只允许 DHE(有限域)和 ECDHE(椭圆曲线)。每次连接都生成临时密钥对,握手结束密钥就丢弃。这就是前向安全。

我曾经帮一个金融客户迁移到 1.3,他们服务端配置里还留着旧的 RSA 证书签名但不做密钥交换,结果客户端认证一直失败。排查了两小时,最后发现是 openssl 配置里没开 ECDHE 曲线。

3.4 证书验证的变化

1.2 的证书链是明文发送的,中间人可以抓包看到证书内容。1.3 把 Certificate 消息放到加密层之后——除了 ServerHello 和部分扩展,后续消息全加密。这样即使有人抓包,也看不到你的证书信息,有效防止了证书泄露和中间人篡改。

但这里有个坑:证书签名算法在 1.3 里也必须用支持 PFS 的算法。我见过有人用 SHA1 签名的证书,在 1.3 下直接报错。嗯,SHA1 已经被彻底拉黑了。

对比维度TLS 1.2TLS 1.3
握手往返次数2-RTT(完整握手)1-RTT / 0-RTT
密码套件数量30+5
密钥交换RSA / DHE / ECDHE仅 DHE / ECDHE
前向安全可选(RSA 不支持)强制
加密模式AEAD + CBC + 流式等仅 AEAD
证书传输明文加密后传输
会话恢复Session ID / Session TicketPre-shared Key (PSK) + 0-RTT
扩展支持可协商扩展(SNI, ALPN 等)扩展列表更严格,未知扩展直接失败

3.5 会话恢复机制的演进

1.2 的会话恢复靠 Session ID(服务端保存状态)或 Session Ticket(客户端保存加密票据)。问题在于恢复时仍然需要一次完整的 ServerHello 和证书验证,只是跳过密钥交换。

1.3 引入 Pre-shared Key(PSK)模式。首次握手后,服务端发一个 PSK 标识(由早期密钥派生出的身份标识)。下次客户端可以直接用 PSK 发起 0-RTT 握手,甚至连网络延迟都不等,客户端直接发送加密的应用数据。

避坑指南:我曾经在测试 0-RTT 时,发现客户端发送的 0-RTT 数据被服务端拒绝。原因是 PSK 有效期过期(默认仅 2 小时)。解决办法是缩短或延长 PSK 生命周期,同时服务端要配置 max_early_data_size。否则重放攻击或数据丢失会让你抓狂。

3.6 扩展机制的严格化

1.2 里扩展是可选的,解析遇到不认识的就忽略。1.3 不一样,协议定义了一系列强制扩展(如 supported_versions, key_share, signature_algorithms)。客户端如果缺少某个强制扩展,服务端直接丢回 “unrecognized_name” 或 “illegal_parameter” 告警。

我记得有一次排查一个 Android 客户端连不上服务器,抓包发现 ClientHello 里没有 key_share 扩展。原来那台设备系统自带 OpenSSL 版本太老,根本不支持 TLS 1.3。客户端假装支持 1.3,发了个 1.3 版本号,却没带 key_share,服务端直接断连。

所以调试时,一定要看清 ClientHello 里扩展列表的完整性。Wireshark 的 “TLS” 解析器有详细视图,按 Export Packet Dissections 可以导出扩展明细。

# 用 openssl 工具模拟 1.3 握手,看看扩展
openssl s_client -connect example.com:443 -tls1_3 -state
# 输出中会列出扩展协商结果:
# TLS 1.3 handshake ... Supported Versions: 0x0304
# Key Share: group X25519, curve ...
# Extended master secret: yes

3.7 混合版本环境下的兼容策略

现在很多服务端需要同时支持 1.2 和 1.3。客户端会发一个 supported_versions 扩展,列出自己能支持的版本列表。服务端挑最高的共同版本。如果客户端只支持 1.2,它可能根本不会发 supported_versions(或者只发 0x0303),服务器就降级到 1.2 握手。

这个降级机制有个潜在陷阱:中间人可以把 supported_versions 里的最高版本改成 1.2,强制降级。1.3 协议本身有降级检测(通过特殊的 random 值标记),但前提是两端都实现正确。我在一些老版本 Nginx 上见过降级检测没写全,导致安全审计不通过。

一句话总结版本演进:TLS 1.3 不是 1.2 的补丁,而是从密码套件、握手流程、扩展机制到安全假设的全面重构。调试接口时,版本不匹配往往是“你看不见却最先出问题”的那一环。