4. TLS 握手协议:从 Hello 到握手完成

握手协议,说白了就是客户端和服务器正式通信前的“对表”环节。我记得刚接触 TLS 时觉得这步特神秘,后来亲手抓过几次包才明白——其实就是双方互相确认身份、协商密码套件、交换临时密钥。嗯,这里有个坑:很多人把握手和加密混为一谈,实际上握手的结束才是加密通信的开始。

4.1 握手到底在解决什么问题?

没有握手,双方连用哪种加密算法都不知道,更别提验证证书了。我个人习惯把握手拆成三件事:

  • 身份确认:服务器(有时也包括客户端)把自己的证书甩过来,客户端验证签名是否有效,是不是被 CA 信任。
  • 密码套件协商:客户端说“我能用这些算法”,服务器从中挑一个最符合安全策略的。
  • 临时密钥交换:用非对称加密或 DH 交换一个预主密钥,后续推导出对称密钥。

这三个目标必须按顺序完成,而且每一步都不能被中间人篡改。我之前帮一个客户排查过“证书没问题但就是连不上”的情况,最后发现是密码套件列表里少了服务端支持的算法——说白了就是客户端太旧,只带了 TLS 1.0 的套件,而服务端只允许 1.2。这种问题抓包一看就知道。

4.2 ClientHello:第一个招呼

客户端发起握手,最先发的是一个明文报文——ClientHello。为什么是明文?因为它只包含公开信息。结构很简单:

Client Hello
  Version: TLS 1.2 (0x0303)
  Random: 32 字节客户端随机数
  Session ID: 可选,用于恢复会话
  Cipher Suites: 支持的套件列表(如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
  Compression Methods: 通常只支持 null
  Extensions: SNI、ALPN、supported_groups、key_share 等

你想想看,客户端随机数为什么重要?它参与了后续主密钥的推导,而且每次握手必须不同。如果随机数重复了,密钥就可能重复——嗯,我在某次渗透测试中就看到过一个实现 bug,它把随机数写死成了 0,结果所有连接都用了相同的密钥,简直是个后门。

扩展字段(Extensions)现在是握手中的重头戏。SNI 让服务器知道客户端想访问哪个域名,不然一台服务器挂多个证书就尴尬了。ALPN 负责协商上层协议,比如 HTTP/2 还是 HTTP/1.1。supported_groups 告诉你客户端支持哪些椭圆曲线,服务器只能在里面选。

避坑提示:我曾经见过某些负载均衡器把 ClientHello 里的 SNI 解析错了,导致后端选错证书,客户端直接报证书域名不匹配。解决方法是让负载均衡器透传 SNI 或者统一用通配符证书。

4.3 ServerHello 和证书交换

服务器收到 ClientHello 后,回复 ServerHello。它主要做两件事:

  • 选定版本和套件:服务器在客户端列表里挑一个自己支持的,如果都不支持就发个握手失败警告。
  • 送出自己的随机数:也是 32 字节,和客户端随机数一起用于密钥推导。

紧接着就是 Certificate 消息。服务器把自己的证书链(叶子证书+中间证书)发过来。客户端要验证:

  1. 证书是否在有效期内。
  2. 签名是否由上一级 CA 签发,直到信任锚点。
  3. 域名是否匹配(用 SNI 里的域名比对 Subject 或 SAN)。

这里有个极易踩坑的点:证书链必须完整。很多服务器只发了叶子证书,中间证书没带。大部分浏览器可以自动下载补充,但一些定制化的客户端不会去下载,直接报错。我之前调试一个嵌入式设备时就被这个坑过——花了半天才发现服务器没配中间证书。

如果是 DHE 或 ECDHE 密钥交换,服务器还会发 ServerKeyExchange,里面包含 DH 参数和签名。如果是静态 RSA 交换(TLS 1.2 以前),就不需要这步,因为公钥直接写在证书里。

4.4 密钥交换:预主密钥的诞生

拿到服务器的公钥或 DH 参数后,客户端要生成一个 预主密钥(Pre-Master Secret)。有两种典型方式:

交换方式核心动作安全性
RSA 密钥交换客户端生成 46 字节随机数,用服务器公钥加密后发给服务器无前向安全性——私钥泄露,所有历史流量都能解密
(EC)DHE 密钥交换客户端用服务器 DH 参数生成自己的 DH 密钥对,计算共享密钥作为预主密钥具备前向安全性——临时私钥用完即弃,不影响之前会话

我现在强烈建议只使用(EC)DHE。你想想看,要是私钥被偷了,RSA 交换的历史流量全完蛋,但 DHE 的临时私钥只存在内存里,关机或换连接后就没了。我曾经帮一个银行客户升级老系统,他们以前用 RSA 交换,后来换了 ECDHE,算是松了一口气。

客户端发送 ClientKeyExchange 后,双方都有预主密钥了。然后各自通过 PRF(伪随机函数)计算:

主密钥 = PRF(预主密钥, "master secret", 客户端随机数 + 服务器随机数)
工作密钥 = PRF(主密钥, "key expansion", 服务器随机数 + 客户端随机数)

工作密钥会拆成:客户端写密钥、服务器写密钥、IV 等。注意,这里的随机数顺序在不同版本里可能不同,TLS 1.2 和 1.3 都有细微调整,实现时必须严格按照 RFC 来。

警告:预主密钥必须完全随机,且每次握手独立。我曾经遇到过某个库因为随机数生成器种子没更新,导致重复生成了相同的预主密钥,结果两个会话用了完全相同的对称密钥——这比用弱密码还恐怖。

4.5 Finished:握手完成确认

在 ChangeCipherSpec 消息(通知对方接下来要加密通信)之后,双方各自发送 Finished 消息。Finished 的内容是之前整个握手消息的哈希值(用协商好的算法),并用新协商的对称密钥加密。对方收到后解密并验证哈希,如果匹配就说明握手没有被篡改。

为什么需要这一步?假设之前有中间人篡改了 ClientHello 或者证书,哈希对不上,Finished 验证就会失败,连接直接断开。这其实是一道“加密的完整性校验”。我自己做实现时,Debug 阶段最常干的事就是把握手消息的哈希打印出来对比——如果两边算出来的不一致,十有八九是某个字段拼错了。

握手一旦完成,双方就可以开始用工作密钥加密应用数据了。注意,Finished 消息本身就已经是加密发送的,所以从这个时刻开始,后面的所有记录都是 ciphertext。

TLS 1.2 握手流程 TLS 1.2 握手协议流程图 客户端 服务器 ① ClientHello ② ServerHello + 随机数 ③ Certificate(证书链) ④ ServerKeyExchange(DH 参数+签名) ⑤ ClientKeyExchange(DH 公钥/加密预主密钥) ⑥ ChangeCipherSpec + Finished ⑦ ChangeCipherSpec + Finished 握手完成,开始加密通信

要点快记:

  • ClientHello 只暴露客户端能力(版本、套件、随机数),不含机密
  • 服务器必须在 ServerHello 里明确选定版本和套件,不能模糊回应
  • 证书链完整性是关键漏配项,中间证书丢失会导致验证失败
  • 实际握手产生的密钥分为主密钥和工作密钥,工作密钥再细分为读写方向和 IV
  • Finished 消息是握手完整性的“加密锁”,哈希对不上立刻断开

这一章的内容就到这里。握手协议虽然看着步骤多,但只要抓住“身份确认、参数协商、密钥交换”三条线,你就能看清每一步的作用。下次遇到握手失败,先看 ClientHello 里的套件列表和服务端回的 ServerHello,基本能定位 80% 的问题。