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。
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 证书(自签名)
└── 客户端需要验证整条链
客户端收到证书链后,会做以下几件事:
- 验证证书链:从叶子证书开始,逐级向上验证,直到信任的根 CA。
- 检查证书有效期:证书是否在有效期内。
- 检查域名匹配:证书中的 Common Name 或 Subject Alternative Name 是否与访问的域名一致。
- 检查吊销状态:通过 CRL 或 OCSP 检查证书是否被吊销。
避坑指南:我曾经遇到过一个案例,服务器发送的证书链不完整,缺少中间 CA 证书。结果客户端无法验证证书链,直接报错。所以,部署证书时一定要把完整的证书链配置上去。
4.5 密钥协商:RSA vs ECDHE
证书交换完成后,就进入密钥协商阶段。TLS 1.2 支持多种密钥交换算法,最常用的是 RSA 和 ECDHE。这两种方式有本质区别:
| 特性 | RSA 密钥交换 | ECDHE 密钥交换 |
|---|---|---|
| 前向安全性 | 不支持 | 支持 |
| 密钥生成方式 | 客户端生成预主密钥,用服务器公钥加密后发送 | 双方通过 ECDH 算法协商出预主密钥 |
| 性能 | 较快(非对称加密一次) | 稍慢(需要 ECDH 计算) |
| 安全性 | 私钥泄露后,所有历史通信可被解密 | 私钥泄露不影响历史通信 |
| 推荐程度 | 不推荐(除非兼容老旧设备) | 强烈推荐 |
4.5.1 RSA 密钥交换流程
如果使用 RSA 密钥交换,流程是这样的:
- 客户端生成 46 字节的预主密钥(Pre-Master Secret)。
- 客户端用服务器证书中的公钥加密预主密钥。
- 客户端将加密后的预主密钥通过 ClientKeyExchange 消息发送给服务器。
- 服务器用私钥解密,得到预主密钥。
- 双方用预主密钥 + 客户端随机数 + 服务器随机数,通过 PRF 函数派生出主密钥。
核心问题:RSA 密钥交换没有前向安全性。如果服务器的私钥被泄露,攻击者可以解密所有历史通信。这也是为什么现在推荐使用 ECDHE 的原因。
4.5.2 ECDHE 密钥交换流程
ECDHE 的流程稍微复杂一些,但安全性更高:
- 服务器在 ServerHello 之后,发送 ServerKeyExchange 消息,包含 ECDH 参数(曲线类型、服务器公钥、签名)。
- 客户端验证签名后,生成自己的 ECDH 密钥对。
- 客户端通过 ClientKeyExchange 消息发送自己的 ECDH 公钥。
- 双方通过 ECDH 算法计算出共享的预主密钥。
- 后续步骤与 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 的工作原理,也能更高效地排查问题。