4. TLS 1.2 握手过程详解:ClientHello、ServerHello、证书交换、密钥协商、Finished 消息
TLS 1.2 的握手,说白了就是客户端和服务器在正式传输数据之前,互相确认身份、商量好加密方式、并生成共享密钥的过程。我刚开始接触这个流程时,觉得它步骤多、消息杂,很容易搞混。但后来我发现,只要抓住「先打招呼、再验身份、最后协商密钥」这条主线,整个流程就清晰多了。
这一章,我们就把 TLS 1.2 握手的五个核心阶段拆开来看。我会结合自己踩过的坑,帮你把每个消息的作用和细节理清楚。
4.1 握手全景图:一次完整的对话
我们先从整体上看看一次完整的 TLS 1.2 握手长什么样。下面这张图,我建议你多看几遍,后面所有的细节都会落在这张图上。
核心要点:TLS 1.2 完整握手需要 2 个 RTT(网络往返)。第一个 RTT 交换 Hello 和证书,第二个 RTT 完成密钥协商和握手确认。
嗯,这张图信息量不小。我们接下来一个一个拆解。
4.2 ClientHello:客户端的第一声招呼
握手的第一步,永远是客户端先开口。它发送一个 ClientHello 消息,告诉服务端:「嘿,我想跟你建立安全连接,这是我支持的选项。」
ClientHello 消息里包含哪些关键字段?我列个表给你看:
| 字段 | 说明 | 我的经验 |
|---|---|---|
| Version | 客户端支持的最高 TLS 版本,比如 0x0303 表示 TLS 1.2 | 有些老旧客户端会谎报版本,服务端要做好兼容 |
| Random | 32 字节随机数(4 字节时间戳 + 28 字节随机值) | 这个随机数很重要,后面生成主密钥要用到它 |
| Session ID | 会话复用标识,第一次握手时为空 | 我遇到过 Session ID 太长导致握手失败的情况 |
| Cipher Suites | 客户端支持的密码套件列表,按优先级排序 | 建议把最安全的套件放最前面 |
| Compression Methods | 压缩算法列表,现在基本只用 null | CRIME 攻击后,压缩基本被废弃了 |
| Extensions | 扩展字段,比如 SNI、ALPN、Supported Groups 等 | 现代 TLS 的很多特性都靠扩展实现 |
小技巧:我个人习惯在客户端发送 ClientHello 时,把 supported_groups 扩展里加上 x25519 和 secp256r1。前者性能更好,后者兼容性更广。两个都带上,让服务端选。
4.3 ServerHello:服务端的回应
服务端收到 ClientHello 后,会从客户端提供的选项里挑一套自己支持的,然后回复 ServerHello。说白了,这就是在说:「好,我们就用这套方案。」
ServerHello 的结构跟 ClientHello 很像,但字段更少:
- Version:服务端最终选定的 TLS 版本。注意,它不能高于客户端提供的版本。
- Random:服务端生成的 32 字节随机数。跟客户端的随机数一起,用于后续密钥派生。
- Session ID:如果服务端支持会话复用,这里会返回一个 ID。
- Cipher Suite:服务端从客户端列表里选定的一个密码套件。
- Compression Method:选定的压缩算法,现在基本都是 null。
这里有个细节我提醒你注意:服务端选定的密码套件,决定了后续握手流程的走向。比如,如果选的是 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,那后面就需要走 ECDHE 密钥交换;如果选的是 TLS_RSA_WITH_AES_128_CBC_SHA,那就是传统的 RSA 密钥交换。
避坑指南:我曾经在项目中遇到过一个问题:服务端选了一个客户端不支持的密码套件。后来发现是客户端在 ClientHello 里漏发了某个套件,但服务端却以为它支持。所以,客户端和服务端的密码套件列表一定要对齐,否则握手会直接失败。
4.4 证书交换:身份的证明
服务端发完 ServerHello 后,紧接着就会发送 Certificate 消息。这条消息里包含服务端的数字证书链。客户端收到后,会验证这个证书链是否可信。
证书链的结构一般是这样的:
服务器证书 (Leaf Certificate)
↑ 由中间 CA 签发
中间 CA 证书 (Intermediate CA)
↑ 由根 CA 签发
根 CA 证书 (Root CA) —— 客户端本地信任
客户端验证证书时,会做以下几件事:
- 证书链校验:从叶子证书开始,逐级向上验证签名,直到信任的根 CA。
- 有效期检查:确认证书没有过期,也没有「生效」。
- 域名匹配:检查证书的
Subject Alternative Names (SAN)是否包含当前访问的域名。 - 吊销状态检查:通过 CRL 或 OCSP 确认证书没有被吊销。
核心要点:证书交换是 TLS 握手中最耗时的环节之一。证书链越长,客户端需要验证的签名就越多。我建议生产环境中,证书链不要超过 3 层(叶子 + 中间 CA + 根 CA),否则会影响握手性能。
如果服务端选的是 DHE 或 ECDHE 密钥交换算法,在 Certificate 之后还会发送 ServerKeyExchange 消息。这条消息里包含 DH 参数或 ECDHE 公钥,以及服务端用私钥对这些参数的签名。客户端收到后,会用证书里的公钥验证签名,确保参数没有被篡改。
最后,服务端会发送 ServerHelloDone 消息,表示:「我这边该发的都发完了,轮到你了。」
4.5 密钥协商:双方如何达成一致
这是握手中最核心的部分。客户端收到服务端的参数后,开始计算密钥。
以最常用的 ECDHE 为例,流程是这样的:
- 客户端生成自己的 ECDHE 临时密钥对(私钥 + 公钥)。
- 客户端发送
ClientKeyExchange消息,把自己的 ECDHE 公钥发给服务端。 - 现在,双方都拥有了对方的 ECDHE 公钥和自己的私钥。通过 ECDH 算法,它们可以计算出相同的 预主密钥 (Pre-Master Secret)。
- 客户端和服务端各自用预主密钥、客户端随机数、服务端随机数,通过 PRF(伪随机函数)派生出 主密钥 (Master Secret)。
- 主密钥再进一步派生出会话密钥:客户端写密钥、服务端写密钥、IV 等。
我画个简图帮你理解:
客户端随机数 + 服务端随机数 + 预主密钥
↓
PRF 函数
↓
主密钥 (48 字节)
↓
PRF 函数
↓
┌───────────┼───────────┐
↓ ↓ ↓
客户端写密钥 服务端写密钥 IV 等
小技巧:预主密钥是 DH 计算出来的,但 DH 计算的结果可能不是 48 字节。TLS 1.2 规定,预主密钥要经过 PRF 扩展成 48 字节,再参与主密钥派生。这个细节很容易被忽略,但如果你自己实现 TLS 协议栈,就一定要注意。
4.6 Finished 消息:握手的最终确认
密钥协商完成后,客户端会发送 ChangeCipherSpec 消息,告诉服务端:「接下来我要用协商好的密钥加密了。」然后立即发送 Finished 消息。
Finished 消息的内容,是对整个握手消息的 HMAC 摘要。具体来说:
- 客户端计算从
ClientHello到ClientKeyExchange所有握手消息的哈希值。 - 用主密钥和
client finished标签,通过 PRF 生成一个verify_data。 - 把这个
verify_data用协商好的加密算法加密后发送。
服务端收到后,会做同样的计算,然后比对。如果一致,说明握手过程中没有人篡改消息,密钥协商也是正确的。
服务端也会做同样的操作:发送 ChangeCipherSpec 和 Finished。客户端验证服务端的 Finished 消息后,握手才算真正完成。
避坑指南:我曾经在调试一个 HTTPS 代理时,发现握手总是在 Finished 阶段失败。排查了半天,发现是代理在转发握手消息时,不小心修改了某个字段。因为 Finished 消息校验的是整个握手过程的完整性,任何中间人修改都会导致校验失败。所以,如果你在实现中间件或代理,一定要保证握手消息的透传,不要自作主张修改任何字段。
4.7 小结:握手的核心逻辑
回顾一下,TLS 1.2 握手其实就做了三件事:
- 协商:通过
ClientHello和ServerHello确定版本、密码套件等参数。 - 认证:通过证书交换验证服务端(或客户端)的身份。
- 密钥协商:通过 DH 或 RSA 等方式,安全地生成共享密钥。
而 Finished 消息,就是给整个握手过程加了一把「锁」——确保所有消息都没有被篡改。理解了这条主线,TLS 1.2 握手就不再是黑盒了。