从 ClientHello 到 Finished:TLS 握手的每一步都在说什么

咱们搞 DLP 内容检测的人,为什么非得啃透 TLS 握手?

说白了,你连数据是怎么加密的、密钥在哪儿协商的都不知道,就没法在合适的位置把流量解出来做检测。我个人习惯是先用 Wireshark 抓个完整握手流程,对着看一遍,心里才有底。这一章咱们就把 ClientHello 到 Finished 这 7 步走一遍,重点讲每个消息里跟 DLP 检测相关的字段和坑。

握手流程全景:七个消息,一次往返

先给你一张我手绘的简图,把整个流程串起来。你想想看,客户端和服务器第一次见面,要谈好协议版本、加密算法、交换临时密钥,最后还要确认对方手里确实有私钥——这跟陌生人见面先亮身份、再对对暗号差不多。

TLS 1.2 完整握手流程(关键节点) 客户端 服务器 ① ClientHello (版本/密码套件/随机数) ② ServerHello + 证书链 + ServerKeyExchange ③ ClientKeyExchange (公钥/预主密钥) ④ ChangeCipherSpec + Finished (客户端) ⑤ ChangeCipherSpec + Finished (服务器) 实际 TLS 1.2 握手包含更多扩展(SNI、ALPN 等),这里仅展示核心 5 条消息

嗯,图上这 5 个阶段,对应客户端和服务器的 7 次消息交互。下面咱们逐个拆开看。

1. ClientHello——客户端的第一声招呼

ClientHello 是握手的第一步。客户端把自家的配置一股脑儿丢给服务器,包括:

  • 协议版本:比如 0x0303 表示 TLS 1.2,0x0304 表示 TLS 1.3。我记得早期有个项目,客户端明明支持 TLS 1.3,却因为旧版 JDK 的 bug,死活只发 0x0301(TLS 1.0),搞得服务端降级到弱加密套件。
  • 随机数(32 字节):客户端生成的随机数,后续密钥衍生都要用它。这个字段是强随机的,但我在一个加密机调试中遇到过伪随机种子被重复使用的情况——那次排查花了两天。
  • 密码套件列表:按优先级排序,通常几十个。做 DLP 检测时,我习惯重点关注是不是有 RC4、CBC 模式或 EXPORT 级别的弱套件被列出来。
  • 扩展:SNI(Server Name Indication)一定要重点关注。我曾经抓到一个内网被劫持的案例,ClientHello 里的 SNI 指向了 google.com,但实际目标 IP 是内网的恶意服务器——如果没有 SNI 字段,DLP 设备根本不知道要检测什么域名。
我的小技巧:在 Wireshark 里过滤 tls.handshake.type == 1 直接定位 ClientHello,然后展开 Extension: server_name 看 SNI。如果能拿到明文 SNI,DLP 策略就可以按域名做白名单检测,而不是全量解密。

2. ServerHello + 证书链——服务器亮明身份

服务器收到 ClientHello 后,选一个双方都支持的密码套件,然后发回自己的证书链。这里有三个关键点:

  1. 密码套件选择:服务器只选一个,客户端别无选择。如果服务器选了 TLS_RSA_WITH_AES_128_GCM_SHA256,那密钥交换就是 RSA 方式,客户端得用服务器公钥加密预主密钥。我自己不太建议在生产环境用 RSA 密钥交换,因为它不支持前向安全性(PFS)。
  2. 证书链:服务器会发完整的证书链,从服务器证书到根 CA。做 DLP 检测的设备需要验证证书是否有效(未过期、未被吊销、符合 SNI)。我参与过一个金融项目,业务方自己签了个自签名证书,结果 DLP 网关直接把所有流量弹成“证书无效”并阻断——其实正确的做法是给 DLP 设备导入信任链。
  3. ServerKeyExchange:如果使用的是 DHE 或 ECDHE 密钥交换,服务器会发这条消息,包含自己的临时公钥和签名。这个字段常被忽略,但签名验证失败会导致握手直接中断。
注意:TLS 1.3 里已经没有单独的 ServerKeyExchange 消息了,这部分内容合并在 ServerHello 之后的 EncryptedExtensions 里。如果你用老设备做 DLP 检测,务必要确认固件支持 TLS 1.3。

3. ClientKeyExchange——交出预主密钥

客户端根据服务器选好的密钥交换算法,生成预主密钥(Pre-Master Secret),然后发给服务器。

  • RSA 方式:客户端生成 48 字节的预主密钥,用服务器证书里的公钥加密后送出。服务器用私钥解密。这种方式的隐患是:如果攻击者事后拿到了服务器私钥,就能解密之前所有流量的会话密钥。所以我在设计 DLP 架构时,会要求后端必须禁用 RSA 密钥交换,强制走 ECDHE。
  • (EC)DHE 方式:客户端发自己的临时公钥,双方通过 DH 算法算出相同的预主密钥。即使私钥被盗,也只能解密后续的会话,不影响以前。

这时候客户端和服务端已经各自拿到了预主密钥,结合 ClientHello 和 ServerHello 里的随机数,开始推导主密钥和会话密钥。推导过程用 PRF(伪随机函数),细节不展开了,但你要知道:这些密钥后续会被用于对称加密和 MAC。

4. ChangeCipherSpec + Finished——验证一切

这是一个转折点。客户端发送 ChangeCipherSpec 通知对方:“从下一条消息开始,我将使用之前协商好的加密参数。” 紧接着发 Finished 消息,这条消息是用会话密钥加密并加上 MAC 的。Finished 的内容是对之前所有握手消息的 HMAC 摘要(用 verify_data 字段)。

服务器收到后,自己也算一遍摘要,比对一致,反过来也发一套 ChangeCipherSpec + Finished 给客户端。至此握手完成,可以开始安全传输应用数据了。

我曾在一次排障中发现:客户端 Finished 消息一直报 MAC 错误,但抓包看明文握手日志又都对。后来发现是硬件加速卡的 AES 引擎在 GCM 模式下丢了一个 nonce 字节——这种硬件 bug 极难定位,好在通过对比不同机器上的 Finished verify_data 发现了差异。

几个实战中容易踩的坑

问题场景根因避坑建议
客户端 Finished 校验失败握手消息被中间设备修改(如 IPsec 网关改包)启用 TLS 端点验证,让 DLP 设备做代理模式
证书链不完整导致握手失败服务器只发了叶子证书,没发中间 CA用 openssl s_client 检查完整链,强制配置中间证书
SNI 与实际证书域名不匹配多域名共用 IP,DDoS 或 CDN 配置错误在 DLP 策略中同时匹配 SNI 和证书 CN/SAN
密码套件协商为 EXPORT 弱套件客户端列表包含弱套件,服务器选择后降级在 DLP 网关侧强制禁用 EXPORT/RC4/DES

从 DLP 视角看:哪一步最适合做内容检测?

你可能会想:我们是不是非得等到 Finished 之后才能解密数据?其实不一定。

我个人建议两种模式:

  1. 透明代理(或中间人):DLP 设备充当 TLS 代理,在 ClientHello 之后自己完成与服务器的握手,再与客户端建立另一个 TLS 连接。这样设备手上就有会话密钥,随时可以解密后续数据。缺点是证书需要被客户端信任。
  2. 带外密钥注入:某些场景(如银行业),企业管控 TLS 服务器私钥,DLP 设备通过 KeyLogFile(如 SSLKEYLOGFILE 环境变量)或 PKCS#11 接口获取主密钥。这种情况下,设备只需捕获握手消息中的随机数,然后用主密钥推导会话密钥,做离线解密。我用过这种方案,优点是性能好,不需要改证书。
记笔记:不论哪种模式,你都得先理解 Finished 消息的 verify_data 是怎么算的。一旦握手验证通过,后续的加密数据就能用会话密钥解密——这也是 DLP 内容检测的黄金时机。