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 完成密钥协商和握手确认。

TLS 1.2 完整握手流程图 TLS 1.2 完整握手流程图 客户端 (Client) 服务端 (Server) ① ClientHello TLS 版本、密码套件列表、随机数 ② ServerHello 选定版本、密码套件、随机数 ③ Certificate 服务端证书链 ④ ServerKeyExchange DH 参数 / ECDHE 公钥 ⑤ ServerHelloDone ⑥ ClientKeyExchange 客户端 DH 公钥 / 预主密钥 ⑦ ChangeCipherSpec ⑧ Finished 切换加密套件 握手完整性校验 ⑨ ChangeCipherSpec ⑩ Finished 说明:①-⑤ 为第一个 RTT(服务端 -> 客户端),⑥-⑩ 为第二个 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) —— 客户端本地信任

客户端验证证书时,会做以下几件事:

  1. 证书链校验:从叶子证书开始,逐级向上验证签名,直到信任的根 CA。
  2. 有效期检查:确认证书没有过期,也没有「生效」。
  3. 域名匹配:检查证书的 Subject Alternative Names (SAN) 是否包含当前访问的域名。
  4. 吊销状态检查:通过 CRL 或 OCSP 确认证书没有被吊销。

核心要点:证书交换是 TLS 握手中最耗时的环节之一。证书链越长,客户端需要验证的签名就越多。我建议生产环境中,证书链不要超过 3 层(叶子 + 中间 CA + 根 CA),否则会影响握手性能。

如果服务端选的是 DHE 或 ECDHE 密钥交换算法,在 Certificate 之后还会发送 ServerKeyExchange 消息。这条消息里包含 DH 参数或 ECDHE 公钥,以及服务端用私钥对这些参数的签名。客户端收到后,会用证书里的公钥验证签名,确保参数没有被篡改。

最后,服务端会发送 ServerHelloDone 消息,表示:「我这边该发的都发完了,轮到你了。」

4.5 密钥协商:双方如何达成一致

这是握手中最核心的部分。客户端收到服务端的参数后,开始计算密钥。

以最常用的 ECDHE 为例,流程是这样的:

  1. 客户端生成自己的 ECDHE 临时密钥对(私钥 + 公钥)。
  2. 客户端发送 ClientKeyExchange 消息,把自己的 ECDHE 公钥发给服务端。
  3. 现在,双方都拥有了对方的 ECDHE 公钥和自己的私钥。通过 ECDH 算法,它们可以计算出相同的 预主密钥 (Pre-Master Secret)。
  4. 客户端和服务端各自用预主密钥、客户端随机数、服务端随机数,通过 PRF(伪随机函数)派生出 主密钥 (Master Secret)。
  5. 主密钥再进一步派生出会话密钥:客户端写密钥、服务端写密钥、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 握手其实就做了三件事:

  1. 协商:通过 ClientHello 和 ServerHello 确定版本、密码套件等参数。
  2. 认证:通过证书交换验证服务端(或客户端)的身份。
  3. 密钥协商:通过 DH 或 RSA 等方式,安全地生成共享密钥。

而 Finished 消息,就是给整个握手过程加了一把「锁」——确保所有消息都没有被篡改。理解了这条主线,TLS 1.2 握手就不再是黑盒了。