2. 正常 TLS 握手流程详解:ClientHello、ServerHello、证书交换、密钥生成
TLS 握手就像是两个陌生人见面时互相验明正身、再悄悄约定一个只有他俩知道的暗号。我做了这么多年流量分析,见过的握手包少说有上百万个,但真正“干净”的流程其实就那么几条。今天咱们就把这条“干净的路”走一遍。
2.1 第一步:ClientHello —— 我先打个招呼
客户端发起连接时,第一个包永远是 ClientHello。说白了就是客户端喊一声:“嘿,我想跟你加密聊天,这是我支持的版本、加密套件列表,你看哪个合适?”
我在一次渗透测试项目中见过一个奇葩情况——客户端发来的 ClientHello 里居然同时包含了 SSLv3 和 TLS 1.3 的版本号,那明显是被改包工具动过手脚的。正常来说,这里只会有客户端最高支持的 TLS 版本。
ClientHello 核心字段:
Protocol Version: 0x0303 (TLS 1.2)
Random: 32 bytes (用于后续密钥派生)
Session ID: (可选,用于会话复用)
Cipher Suites: [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ...]
Extensions: (SNI, ALPN, Supported Groups 等)
记住,Random 字段是后面生成主密钥的原料之一,不能重复。我见过某些嵌入设备因为随机数生成器挂了,每次握手都用同一个 Random,结果被直接重放攻击——这是个很典型的坑。
2.2 第二步:ServerHello —— 服务端回应
服务端收到 ClientHello 后,会从你给的候选列表里挑一个它自己也支持的组合,然后发回 ServerHello。嗯,这里要注意:如果服务端一个都不支持,它就会直接发一个 Alert 关闭连接,不会跟你啰嗦。
我个人习惯先看 ServerHello 里选的 Cipher Suite 到底是什么。举个例子,如果客户端列了一堆 ECDHE 套件,服务端却选了个静态 RSA 密钥交换的套件,那要么是配置有问题,要么是中间人在捣鬼。
| 字段 | 含义 | 常见值 |
|---|---|---|
| Version | 最终使用的 TLS 版本 | 0x0303 (TLS 1.2) / 0x0304 (TLS 1.3) |
| Random | 服务端生成的 32 字节随机数 | 和客户端 Random 一起参与密钥计算 |
| Session ID | 会话复用标识 | 可为空,或与客户端一致 |
| Cipher Suite | 选定的加密套件 | 如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
| Compression Method | 压缩方式(现在通常为 null) | 0x00 |
2.3 第三步:证书交换 —— 亮出你的身份证
服务端接着会发送自己的 证书(Certificate 消息)。这步是 TLS 握手中最容易被误判的地方。你想想看,证书链如果中间少了一级,或者证书过期了,客户端通常会直接拒绝连接。
我遇到过一种诡异情况:服务端发了证书,但 Certificate 消息后面紧跟着一个空的 CertificateRequest——这意味着服务端要求客户端也提供证书进行双向认证。很多开发者在写客户端时根本没处理这个分支,导致连接卡住。嗯,这个坑我踩过,后来调试了整整一个下午才发现问题出在服务器侧的配置上。
检查证书链的要点:
- 证书是否在有效期内?
- 证书是否被吊销?(可检查 CRL/OCSP)
- 证书链根是否被客户端信任?
- 服务器域名是否匹配证书的 Subject/SubjectAltName?
我曾经用 Wireshark 抓到一个银行 App 的流量,发现它竟然直接信任了自签名证书——后来确认是测试环境忘了切回来。
2.4 第四步:ServerHelloDone —— 服务端说完啦
证书发完之后,服务端会发一个 ServerHelloDone。它没有额外内容,就是一个信号:“我这边发完了,客户端你继续。” 这个包很薄,经常被流量分析的同学忽略,但如果用 TLS 1.3 的话,这一步就不存在了。
2.5 第五步:客户端证书与密钥交换(可选 + 必须)
如果服务端要求客户端证书,这里客户端会发送自己的 Certificate 和 ClientKeyExchange。但更常见的情况是:服务端没要求客户端证书,那么客户端直接发 ClientKeyExchange 就够了。
对于 TLS 1.2 及以下版本,ClientKeyExchange 的内容取决于服务器选的密钥交换算法:
- RSA 密钥交换:客户端生成一个 48 字节的预主密钥(Pre-Master Secret),用服务器的公钥加密后发给服务端。
- DHE/ECDHE 密钥交换:客户端发送自己的 Diffie-Hellman 公钥参数,后续双方各自计算共享密钥。
我个人更倾向看 ECDHE 的流程,因为前向安全性更好。现在大多数正规站点都会优先选择 ECDHE,如果你抓到某个银行还在用纯 RSA 密钥交换……嗯,那这家银行的安全团队可能需要加个班。
警钟: 预主密钥必须是强随机数!我见过一个 IoT 设备因为随机数生成器芯片坏了,每次产生的预主密钥都一样,导致所有会话的对称密钥都可被预测。这问题如果被攻击者抓到,整个设备的加密形同虚设。
2.6 第六步:ChangeCipherSpec + Finished —— 开始加密啦
密钥协商完后,双方都会发送 ChangeCipherSpec 消息,意思就是:“我接下来要切换成刚才商定的加密方式了!” 注意这个消息本身不加密,但它有一个单字节的值 0x01。
紧跟着 ChangeCipherSpec 之后发送的是 Finished 消息。Finished 消息的内容是之前所有握手消息的 HMAC 校验和,并且会用刚刚协商好的密钥进行加密。这是整个握手过程中第一个加密的消息,用来验证握手过程没被篡改。
我调试过一个奇怪的问题:客户端和服务端都发了 ChangeCipherSpec,但客户端发的 Finished 解密失败。查了半天发现是因为双方支持的签名哈希算法不一致——服务端用了 SHA256,客户端却算了个 SHA1 的哈希。你想想看,这种微妙的差异很容易被流量分析工具当成“异常握手”上报,其实只是配置没对齐。
2.7 完整的交互流程
把以上步骤串起来,就是下面这张图。我建议你多盯几遍,每次分析异常流量时都在脑海里过一遍这个流程,自然就能发现不对劲的地方。
2.8 密钥生成的数学魔法
握手最后一步是双方各自用 预主密钥(Pre-Master Secret)、客户端随机数 和 服务端随机数 经过伪随机函数(PRF)计算出同样的主密钥(Master Secret),然后派生出多个对称密钥用于加密和 HMAC。
具体来说:
Master Secret = PRF(Pre-Master Secret, "master secret", ClientRandom + ServerRandom)[0..47]
Key Block = PRF(Master Secret, "key expansion", ServerRandom + ClientRandom)
然后从中切出:
- 客户端写密钥 (Client Write Key)
- 服务端写密钥 (Server Write Key)
- 客户端 IV (如果需要)
- 服务端 IV (如果需要)
- 客户端 HMAC 密钥
- 服务端 HMAC 密钥
这个派生过程看起来复杂,但你只需要知道一点:只要预主密钥不被泄露,中间人就算抓到所有握手流量,也无法计算出后续的对称密钥。这就是 TLS 的安全根基。
一个小技巧: 做流量分析时,如果发现客户端和服务端的 Finished 消息解密失败,往往不是密钥协商错了,而是某一方用了不同类型的 PRF(比如 TLS 1.2 要求用 P_SHA256,但老旧实现可能还在用 MD5+SHA1 的联合 PRF)。我曾经踩过一次,那台服务器跑着 OpenSSL 0.9.8,差点把我逼疯。