3、TLS 握手流程详解

TLS 握手是整个安全通信的起点,也是最容易踩坑的地方。我当年第一次对接 HTTPS 接口时,就因为在握手阶段丢了一个扩展字段,导致服务端拒绝连接,查了整整一下午才找到原因。说白了,握手就是在客户端和服务端之间做三件事:协商加密套件、验证身份、生成会话密钥。今天咱们就把这个流程拆开揉碎了讲清楚。

3.1 基本流程:一次完整的四次握手

TLS 1.2 的握手需要两次往返(2-RTT),TLS 1.3 则优化到一次往返(1-RTT)。这里先讲经典的 TLS 1.2 流程,因为目前生产环境里还有大量系统在用。你想想看,如果连 1.2 的步骤都搞不清楚,遇到兼容性问题根本无从下手。

我习惯把握手分成四大阶段:

  1. 客户端问候:ClientHello 发出支持的版本、密码套件、随机数
  2. 服务端回复:ServerHello 选定版本和套件,并下发证书
  3. 密钥交换:双方交换 DH 参数或 RSA 加密的预主密钥
  4. 握手完成:ChangeCipherSpec 切换加密,Finished 做完整性校验

嗯,这里要注意:TLS 1.3 把 ServerHello 之后的步骤合并了,后面我会单独讲差异。

3.2 每一步在做什么?

第一步:ClientHello

客户端发送一个明文包,里面包含:

  • 客户端随机数(32 字节,用于后续密钥派生)
  • 支持的 TLS 版本列表(比如 1.2、1.3)
  • 密码套件列表(Cipher Suites,几十个)
  • 扩展字段(SNI、ALPN、支持的组等)

我曾经在一个嵌入式设备上遇到这种情况:设备只支持 TLS 1.0,但服务端已经关闭了 1.0。结果 ClientHello 发过去,服务端直接返回 Alert(protocol_version)。这类问题查日志最管用。

第二步:ServerHello + 证书 + ServerKeyExchange

服务端回复三个关键消息:

  1. ServerHello:选定版本、密码套件,并带上服务端随机数
  2. Certificate:X.509 证书链(叶子证书 + 中间 CA)
  3. ServerKeyExchange:如果使用 DHE 或 ECDHE,这里携带 DH 参数和签名

为什么要有 ServerKeyExchange?说白了,如果使用 RSA 密钥交换,这一步可以省略,因为公钥已经在证书里了。但为了前向保密(PFS),现在主流都是 ECDHE,服务端就得额外发 DH 参数并签名。

第三步:ClientKeyExchange + ChangeCipherSpec

客户端收到服务端参数后,生成预主密钥(pre-master secret),用服务端公钥加密发送(RSA 场景)或直接发送 DH 公钥(DHE 场景)。然后客户端发送 ChangeCipherSpec,表示后续消息全部加密。

紧接着发送 Finished 消息,内容是之前所有握手消息的 HMAC 摘要。服务端用相同方式验证,如果一致,则握手成功。

第四步:服务端也发送 ChangeCipherSpec + Finished

服务端验证客户端的 Finished 通过后,也切换加密算法,发送自己的 Finished。至此,双向认证完成,可以开始业务数据传输了。

3.3 TLS 1.2 与 1.3 的核心差异

做个表格对比,很多同学在迁移到 1.3 时容易忽略这些变化:

对比项 TLS 1.2 TLS 1.3
握手往返次数 2-RTT(1-RTT 可用 False Start 优化) 1-RTT(0-RTT 支持早期数据)
密钥交换 可选 RSA、DHE、ECDHE,可协商 强制 ECDHE(前向保密)
证书发送时机 ServerHello 后单独发送 合并到 EncryptedExtensions 之后的 Certificate 消息中加密发送
ChangeCipherSpec 显式发送 不再使用(兼容模式下可能有)
密码套件数量 几十种 5 种(AEAD 加密 + HKDF 密钥派生)
小提示:如果你在迁移 TLS 1.3 时发现客户端仍然走 1.2,多半是因为密码套件或扩展不支持。我建议先用 openssl s_client -tls1_3 -connect 测试,看哪一步失败。

3.4 密钥交换:前向保密是关键

我个人认为,理解密钥交换是掌握握手流程的核心。以 ECDHE 为例:

  • 服务端生成临时 ECDH 私钥/公钥对,将公钥和签名发给客户端
  • 客户端也生成临时密钥对,将公钥发给服务端
  • 双方通过 ECDH 算法计算出相同的共享密钥(pre-master secret)
  • 再基于两个随机数 + 预主密钥,通过 PRF 派生出会话密钥

为什么一定要用 ECDHE?因为如果使用 RSA 密钥交换,服务端的私钥一旦泄露,历史上所有的会话都能被解密。而 ECDHE 每次握手都生成临时密钥,私钥泄露只会影响当前会话。这也是现代安全基线强制要求前向保密的原因。

避坑指南:我曾经在 Windows 和 Linux 交叉编译的客户端中遇到过 ECDHE 曲线不兼容的问题。服务端只支持 secp256r1,但客户端默认走 secp384r1,协商失败。解决方案是在 ClientHello 的 supported_groups 扩展中手动指定曲线优先级。

3.5 证书验证环节容易被忽略的细节

服务端下发证书后,客户端必须验证:

  1. 证书有效期是否在当前时间范围内
  2. 证书链是否完整(叶子 → 中间 CA → 根 CA)
  3. 证书是否被吊销(CRL/OCSP)
  4. 域名是否匹配(Subject CN 或 SAN)

很多跨平台问题其实出在证书链不完整。比如某些 Android 设备没有安装中间 CA 证书,服务端没发中间证书,客户端就报错。我的经验是:不管服务端用了什么 CA,永远把完整的证书链一次性下发,别省。

3.6 完整握手流程总览

下面这张图展示了一次典型的 TLS 1.2 握手,包含 ECDHE 密钥交换。每个节点代表一个网络消息,箭头方向表示发送方向。

TLS 1.2 完整握手流程图(ECDHE) 客户端 (Client) 服务端 (Server) ClientHello (版本/套件/随机数) ServerHello (选定版本/套件/随机数) Certificate (X.509 证书链) ServerKeyExchange (ECDHE 公钥 + 签名) ServerHelloDone ClientKeyExchange (ECDHE 公钥) ChangeCipherSpec Finished (加密) ChangeCipherSpec Finished (加密) 握手完成后,后续应用数据全部使用对称加密(如 AES-GCM)传输。

关键总结:TLS 握手流程本质上是一次「协商 → 认证 → 密钥交换 → 验证」的对话。理解每一步的报文结构,就能在抓包时快速定位问题。我在日常排错时,最常用的工具就是 Wireshark 过滤 tls.handshake.type,一眼能看出哪一步超时或失败。