第二章 TLS 握手流程详解:一次完整的握手过程

很多人觉得 TLS 握手很神秘。说白了,它就是客户端和服务器在正式传输数据前,先互相打个招呼、验明正身、商量好加密方式的过程。我刚开始调接口时,看到 "handshake failure" 直接懵了,后来才明白——不懂握手细节,定位问题就是瞎猜。

2.1 握手核心目标:三个问题

一次完整的 TLS 握手要解决三个问题:

  • 身份确认——服务器是不是它声称的那个?客户端要不要验证?
  • 密钥协商——双方怎么安全地商量出一个对称密钥,不被中间人偷听?
  • 算法一致——双方都支持哪些加密套件、TLS 版本,取哪个交集?

我早期犯过一个低级错误:服务端只配了 TLS 1.3,客户端却只支持 TLS 1.2,握手直接失败。你看,连版本都没对齐,后面的事根本谈不了。

2.2 4次握手还是2次?聊聊 TLS 版本差异

目前主流版本是 TLS 1.2 和 TLS 1.3。业内习惯把 TLS 1.2 称为“4次握手”(实际是 4 个 Flight,每个 Flight 可能包含多条消息),而 TLS 1.3 精简到“2次握手”。我把两者的关键差异整理成一张表,方便你对照:

对比项 TLS 1.2 TLS 1.3
握手往返次数 2 RTT(协商 + 密钥交换分离) 1 RTT(密钥协商前置到 ClientHello 中)
支持的加密套件数量 数十种,需协商 仅 5 种,强制 AEAD
前向安全性 可选(部分套件没有) 强制 DH/ECDH 密钥交换
Change Cipher Spec 消息 显式发送 不再单独发送(兼容模式保留)
会话恢复方式 Session ID / Session Ticket PSK + (D)HE 预共享密钥扩展
实战提示: 我建议你在测试环境抓包时,先确认服务器和客户端的 TLS 版本。很多 "握手失败" 其实就是版本不兼容。用 Wireshark 过滤 tls.handshake.version 看一眼就知道。

2.3 TLS 1.2 一次完整握手拆解(以 RSA 密钥交换为例)

虽然 TLS 1.3 越来越普及,但现实生产环境里 TLS 1.2 依然大量存在。我个人习惯把 TLS 1.2 握手过程拆成四个阶段来记:

  1. ClientHello(客户端打招呼)—— 我支持 TLS 1.2,这是我的随机数、加密套件列表、扩展信息。
  2. ServerHello + 证书 + ServerHelloDone(服务器回应)—— 好,我们用 TLS 1.2 + ECDHE-RSA-AES128-GCM-SHA256,这是我的证书,拿去吧。
  3. 客户端验证证书 + 发送密钥交换参数 + ChangeCipherSpec + Finished(关键的一步)—— 证书可信,我来生成预主密钥,加密发给你。后面开始加密通信。
  4. 服务器发送 ChangeCipherSpec + Finished(确认加密通道)—— 我也准备好了,用你的密钥解密 Finished 校验成功。握手掌声!

一个实战坑点: 我曾经遇到一个接口,客户端总是报 "certificate unknown"。抓包后发现服务器发来的证书链不完整,缺少中间证书。客户端没有完整的根证书缓存,验证失败。所以证书链必须完整传过去,别省那条中间证书。

2.4 TLS 1.3 的"1-RTT"握手到底快在哪?

TLS 1.3 最大的变化:它在 ClientHello 里就直接带上了密钥交换的参数。服务器拿到后,立刻算出共享密钥,在 ServerHello 之后就能发加密数据了。整个握手只消耗 1 个 RTT。

你想想看,如果客户端之前和服务器握过手(有 PSK 缓存),甚至能做到 0-RTT——在发送第一个请求的同时就发数据。当然,0-RTT 有重放攻击风险,生产上要谨慎开启。

我记得有次给一个金融 App 调优,把 TLS 1.2 改成 1.3 后,首屏加载时间从 1.8 秒降到 1.1 秒。用户体验提升非常明显。

2.5 必须理解的关键概念:三个秘密

不管哪个版本,握手过程中都会产生三个关键秘密:

  • Pre-Master Secret(预主密钥)—— 客户端生成,用服务器公钥加密传输。服务器解密后,双方各自计算 Master Secret。
  • Master Secret(主密钥)—— 由 Pre-Master + 客户端随机数 + 服务器随机数通过 PRF(伪随机函数)导出。这是所有密钥材料的根。
  • Session Keys(会话密钥)—— 从 Master Secret 进一步派生出来的加密密钥、HMAC 密钥、IV 等。实际加密数据时用的就是它们。
注意: 如果服务器私钥泄漏,攻击者可以解密所有用 RSA 密钥交换的会话历史流量。这也是为什么现在都推荐使用 (EC)DHE 密钥交换——它有前向安全性,私钥泄漏不影响过去会话。

2.6 握手全过程流程图

下面这张 SVG 图把 TLS 1.2 和 TLS 1.3 的握手流程放在一起对比,这样你一眼就能看出差异:

TLS 握手流程对比图(左侧TLS 1.2,右侧TLS 1.3) 客户端 服务器 ClientHello ServerHello + Cert + Done ClientKeyExchange + CCS + Finished ChangeCipherSpec + Finished 应用数据(加密) TLS 1.3 ClientHello + KeyShare ServerHello + Cert + Finished (+ 应用数据) 说明:TLS 1.3 将密钥协商前置到第一次往返中,因此只需要 1-RTT 即可开始传输应用数据

2.7 握手失败时,你应该盯住哪些消息?

作为一个接口测试工程师,你不需要把每条 TLS 消息的二进制格式背下来,但一定要知道在哪层排查。我列了一个排查优先级清单:

  1. ClientHello —— 确认客户端发出去的版本、加密套件列表是否正常。有时候是客户端库版本太旧,发了个 TLS 1.0,服务端直接拒绝。
  2. ServerHello —— 服务器选择了哪个版本、哪套加密套件。如果服务器返回的是 "no shared cipher",大概率是两端套件交集为空。
  3. 证书 —— 检查服务器证书是否过期、域名是否匹配、证书链是否完整。我用 openssl 调试命令 openssl s_client -connect example.com:443 -showcerts 能快速看到。
  4. Finished 消息 —— 如果握手在 Finished 校验时失败,说明前面密钥协商出错了。重点检查预主密钥的加密/解密过程是否一致。
我的调试习惯: 先用 curl 带 -v 参数跑一次,看看握手到哪一步断了。如果 curl 显示 "SSL connection error",再上 Wireshark 抓包,按 tls.handshake.type 过滤。我碰到过最奇怪的一个 case:服务器时钟快了 5 分钟,客户端验证证书有效期时发现证书 "not yet valid",握手失败。修好 NTP 就解决了。

好了,这一章我们走通了 TLS 握手的全过程。理解了这些基础之后,下一节我们真正开始聊——握手失败时,那些常见错误码到底意味着什么。记住,抓包是你最好的朋友,别只盯着应用层日志看。