4. TLS 握手协议(1.2 版):ClientHello、ServerHello、证书交换、密钥协商(RSA/ECDHE)完整流程

好,咱们进入正题。TLS 1.2 握手,说白了就是客户端和服务器之间的一场「身份确认 + 密钥交换」的对话。我刚开始接触这块时,总觉得握手就是来回发几条消息,太简单了。直到自己动手抓包分析,才发现每一步都有讲究,少一个字段都可能握手失败。

这一章,我会带你走一遍完整的握手流程。咱们从 ClientHello 开始,一路走到 Finished 消息。你想想看,如果连握手都搞不清楚,后面怎么去排查 HTTPS 问题?

4.1 握手全景图

先看整体流程。TLS 1.2 握手分为两大阶段:

  • 第一阶段:Hello 阶段。双方交换随机数、密码套件列表、压缩算法等。
  • 第二阶段:认证与密钥协商。服务器发送证书,双方协商出主密钥。

下面这张图,是我自己画的一个简化流程。建议你多看几遍,把每个消息的顺序记在脑子里。

核心要点:TLS 1.2 握手至少需要 2 个 RTT(往返时间)。如果启用会话恢复,可以降到 1 个 RTT。

TLS 1.2 完整握手流程(RSA 密钥交换) 客户端 服务器 ① ClientHello ② ServerHello ③ Certificate ④ ServerHelloDone ⑤ ClientKeyExchange ⑥ ChangeCipherSpec ⑦ Finished ⑧ ChangeCipherSpec + Finished 应用数据 应用数据

4.2 ClientHello:客户端发起握手

握手的第一步,客户端先开口。ClientHello 消息里包含几个关键字段:

字段 说明 我的经验
Version 客户端支持的最高 TLS 版本,比如 0x0303 表示 TLS 1.2 有些老旧客户端会写 0x0301(TLS 1.0),服务器得兼容处理
Random 32 字节随机数,用于后续密钥派生 这个随机数必须真随机,否则密钥可预测
Session ID 会话恢复用的标识,首次握手为空 我遇到过 Session ID 太长导致握手失败的情况
Cipher Suites 客户端支持的密码套件列表,按优先级排序 建议把 ECDHE 套件放在前面,安全性更高
Compression Methods 压缩算法列表,通常只支持 null CRIME 攻击后,压缩基本被禁用
Extensions 扩展字段,如 SNI、ALPN、supported_groups 等 SNI 扩展现在几乎是必须的,否则多域名服务器无法区分

个人习惯:我在配置 Nginx 时,会把 ECDHE 套件放在最前面,RSA 套件放在后面。这样既能保证安全性,又能兼容老旧客户端。

4.3 ServerHello:服务器回应

服务器收到 ClientHello 后,会从中选择一个密码套件和压缩算法,然后回复 ServerHello。这个回复里包含:

  • Version:双方协商后的 TLS 版本,通常是客户端支持的最高版本。
  • Random:服务器生成的 32 字节随机数。
  • Session ID:如果支持会话恢复,服务器会分配一个 Session ID。
  • Cipher Suite:服务器选中的密码套件,比如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
  • Compression Method:通常就是 null。

嗯,这里要注意:服务器选择的密码套件必须出现在客户端发送的列表中。如果服务器选了一个客户端不支持的套件,客户端会直接断开连接。我曾经在调试时遇到过这个问题,排查了半天才发现是密码套件列表不匹配。

4.4 证书交换:服务器的身份证明

ServerHello 之后,服务器会发送 Certificate 消息。这条消息里包含服务器的数字证书链。证书链的结构是这样的:

服务器证书(叶子证书)
  ├── 签发者:中间 CA 证书
  │     └── 签发者:根 CA 证书(自签名)
  └── 客户端需要验证整条链

客户端收到证书链后,会做以下几件事:

  1. 验证证书链:从叶子证书开始,逐级向上验证,直到信任的根 CA。
  2. 检查证书有效期:证书是否在有效期内。
  3. 检查域名匹配:证书中的 Common Name 或 Subject Alternative Name 是否与访问的域名一致。
  4. 检查吊销状态:通过 CRL 或 OCSP 检查证书是否被吊销。

避坑指南:我曾经遇到过一个案例,服务器发送的证书链不完整,缺少中间 CA 证书。结果客户端无法验证证书链,直接报错。所以,部署证书时一定要把完整的证书链配置上去。

4.5 密钥协商:RSA vs ECDHE

证书交换完成后,就进入密钥协商阶段。TLS 1.2 支持多种密钥交换算法,最常用的是 RSA 和 ECDHE。这两种方式有本质区别:

特性 RSA 密钥交换 ECDHE 密钥交换
前向安全性 不支持 支持
密钥生成方式 客户端生成预主密钥,用服务器公钥加密后发送 双方通过 ECDH 算法协商出预主密钥
性能 较快(非对称加密一次) 稍慢(需要 ECDH 计算)
安全性 私钥泄露后,所有历史通信可被解密 私钥泄露不影响历史通信
推荐程度 不推荐(除非兼容老旧设备) 强烈推荐

4.5.1 RSA 密钥交换流程

如果使用 RSA 密钥交换,流程是这样的:

  1. 客户端生成 46 字节的预主密钥(Pre-Master Secret)。
  2. 客户端用服务器证书中的公钥加密预主密钥。
  3. 客户端将加密后的预主密钥通过 ClientKeyExchange 消息发送给服务器。
  4. 服务器用私钥解密,得到预主密钥。
  5. 双方用预主密钥 + 客户端随机数 + 服务器随机数,通过 PRF 函数派生出主密钥。

核心问题:RSA 密钥交换没有前向安全性。如果服务器的私钥被泄露,攻击者可以解密所有历史通信。这也是为什么现在推荐使用 ECDHE 的原因。

4.5.2 ECDHE 密钥交换流程

ECDHE 的流程稍微复杂一些,但安全性更高:

  1. 服务器在 ServerHello 之后,发送 ServerKeyExchange 消息,包含 ECDH 参数(曲线类型、服务器公钥、签名)。
  2. 客户端验证签名后,生成自己的 ECDH 密钥对。
  3. 客户端通过 ClientKeyExchange 消息发送自己的 ECDH 公钥。
  4. 双方通过 ECDH 算法计算出共享的预主密钥。
  5. 后续步骤与 RSA 相同,用 PRF 派生主密钥。

你想想看,ECDHE 的好处是什么?即使服务器的私钥泄露了,攻击者也无法解密之前的通信,因为每次握手的 ECDH 密钥对都是临时生成的。这就是前向安全性。

我的建议:新项目一律使用 ECDHE。如果必须兼容老旧客户端,可以把 RSA 套件放在最后作为兜底。

4.6 主密钥派生与 Finished 验证

密钥协商完成后,双方都有了预主密钥。接下来,通过 PRF 函数派生出主密钥:

master_secret = PRF(pre_master_secret, "master secret",
                    ClientHello.random + ServerHello.random)[0..47]

主密钥长度为 48 字节。然后,双方用主密钥派生出会话所需的密钥材料:

  • 客户端写密钥:用于加密客户端发送的数据。
  • 服务器写密钥:用于加密服务器发送的数据。
  • 客户端 MAC 密钥:用于消息认证。
  • 服务器 MAC 密钥:用于消息认证。
  • 初始化向量:用于 CBC 模式或 GCM 模式的 nonce。

最后一步是 Finished 消息。客户端先发送 ChangeCipherSpec 通知服务器后续消息将加密传输,然后发送 Finished 消息。Finished 消息的内容是对整个握手消息的 HMAC 校验值。服务器验证通过后,也会发送自己的 ChangeCipherSpec 和 Finished 消息。

避坑指南:我曾经遇到过 Finished 消息验证失败的情况。排查后发现是客户端和服务器使用的 PRF 函数版本不一致。TLS 1.2 使用 SHA-256 作为 PRF 的哈希函数,而 TLS 1.1 使用 MD5+SHA1。如果版本协商有问题,Finished 校验就会失败。

4.7 完整握手示例(ECDHE)

下面是一个完整的 ECDHE 握手示例,我用 Wireshark 抓包整理出来的:

1. ClientHello
   Version: TLS 1.2 (0x0303)
   Random: 1a 2b 3c 4d ...
   Cipher Suites: [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ...]
   Extensions: supported_groups (x25519, secp256r1), signature_algorithms (rsa_pkcs1_sha256, ...)

2. ServerHello
   Version: TLS 1.2 (0x0303)
   Random: 5e 6f 7a 8b ...
   Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

3. Certificate
   Certificate: 服务器证书链(叶子证书 + 中间 CA)

4. ServerKeyExchange
   Curve: secp256r1
   Server Public Key: 04 12 34 56 ...
   Signature: 对上述参数的 RSA 签名

5. ServerHelloDone

6. ClientKeyExchange
   Client Public Key: 04 78 9a bc ...

7. ChangeCipherSpec

8. Finished
   Verify Data: 对握手消息的 HMAC 值

9. ChangeCipherSpec

10. Finished
    Verify Data: 对握手消息的 HMAC 值

11. 应用数据(加密传输)

看到没?ECDHE 比 RSA 多了一条 ServerKeyExchange 消息。这条消息里包含了服务器的 ECDH 公钥和签名,客户端需要验证签名后才能继续。

4.8 常见问题与排查思路

握手过程中可能遇到的问题,我列几个常见的:

问题 可能原因 排查方法
握手失败,客户端报错 密码套件不匹配、证书无效、版本不兼容 抓包分析,看哪条消息失败
证书验证失败 证书链不完整、域名不匹配、证书过期 用 openssl verify 检查证书链
Finished 校验失败 PRF 版本不一致、密钥派生错误 检查双方使用的 TLS 版本是否一致
ECDHE 握手慢 曲线选择不当、服务器性能不足 优先使用 x25519 曲线,性能更好

个人经验:排查握手问题,我最常用的工具就是 Wireshark 和 openssl s_client。用 openssl s_client -connect 可以手动触发握手,看到详细的握手过程。比如:

openssl s_client -connect example.com:443 -tls1_2 -debug

这个命令会输出握手过程中的所有消息,包括证书、密钥交换参数等。遇到问题,先跑一遍这个命令,基本能定位 80% 的问题。

好了,这一章的内容就到这里。TLS 1.2 握手虽然步骤多,但每一步都有明确的目的。理解了握手流程,你就能更好地理解 HTTPS 的工作原理,也能更高效地排查问题。