2. TLS 握手过程详解:ClientHello、ServerHello、证书交换、密钥协商
咱们做安全审计,最怕的是啥?是黑盒。你只看到一堆加密流,但不知道握手阶段到底交换了哪些参数。有一次我在排查一个 HTTPS 性能问题,发现服务端证书链居然有 7 层,握手耗时多了一倍。从那以后我就养成了习惯——必须把握手过程拆开看。
这一章,我就带你走进 TLS 握手的四个核心阶段。嗯,不复杂,但细节很重要。
核心知识点:TLS 1.2 完整握手共 4 步(ClientHello → ServerHello + 证书 → 密钥交换 → Finished),审计日志应捕捉每一步的关键参数。
2.1 握手概览:一张图理清流程
我先给一张流程图,你对照着看后面的文字,会清晰很多。
这个流程里我标了四个阶段,咱们一个一个来聊。
2.2 ClientHello:客户端的第一声招呼
握手开始,客户端先发一个 ClientHello。这里面带的参数挺多,审计时你得重点关注几个。
| 参数 | 说明 | 审计关注点 |
|---|---|---|
| Version | 客户端最高支持的 TLS 版本 | 是否低于 TLS 1.2 |
| Random | 32 字节随机数(28 字节随机 + 4 字节时间戳) | 随机数质量、时间戳合理性 |
| Session ID | 会话复用标识(可为空) | 是否启用会话复用 |
| Cipher Suites | 客户端支持的密码套件列表 | 是否包含弱套件(如 RC4、CBC-MAC) |
| Compression Methods | 压缩算法(现多为 null) | 是否允许压缩(CRIME 攻击风险) |
| Extensions | 扩展字段(SNI、ALPN、Supported Groups 等) | 是否支持 TLS 1.3、是否携带 SNI |
我个人习惯,在看审计日志时先扫一遍 Cipher Suites 列表。如果看到 TLS_RSA_WITH_3DES_EDE_CBC_SHA 这种老古董,我基本可以断定这台客户端的安全基线有问题。
2.3 ServerHello + 证书交换:服务端的选择
服务端收到 ClientHello,会从客户端列表里挑一个它自己也能支持的密码套件。说白了,最终用哪个加密算法,是服务端说了算。你想想看,如果服务端弱配了一套已废弃的套件,客户端根本没机会反驳。
ServerHello 回复的内容和 ClientHello 结构类似,但多了几个关键项:
- 选择的密码套件(只能一个)
- 选择的压缩算法(现在全是 null)
- 服务端随机数(同样 32 字节,审计时记得比对客户端随机数,看看生成方式是否合理)
- 后面紧跟着 Certificate 消息,把服务端证书链发过来。
证书这块,审计日志里要记什么?
- 证书序列号:唯一标识
- 颁发者 和 主题(CN 或 SAN)
- 有效期:起止时间,是否已过期
- 签名算法:比如 sha256WithRSAEncryption
- 公钥类型和长度:RSA 2048?ECDSA P-256?还是弱爆了的 1024 位?
- 证书链深度:是否包含中间 CA
2.4 密钥协商:真正的「握手」
证书交换完后,客户端和服务端已经互相知道对方有哪些加密能力了。接下来就是算出一个只有双方知道的对称密钥——Master Secret。
这里我重点讲最常见的两种密钥交换方式:RSA 密钥交换 和 ECDHE 密钥交换。
| 方式 | 流程简介 | 前向安全性 | 审计关键字段 |
|---|---|---|---|
| RSA Key Exchange | 客户端生成 46 字节 Pre-Master Secret,用服务端公钥加密,发过去;服务端用私钥解密。 | 否(私钥泄漏可解密所有历史流量) | 加密后的 Pre-Master Secret 长度、公钥长度 |
| ECDHE Key Exchange | 双方交换 ECDHE 临时公钥,通过 ECDH 算法协商出 Pre-Master Secret。 | 是(临时密钥不持久存储) | 椭圆曲线类型(prime256v1 等)、临时公钥点坐标 |
你可能会问:“为什么现在推荐 ECDHE?就因为前向安全性?” 对,主要是这个原因。我经常跟运维团队说,如果你的服务还在用 RSA 密钥交换,一旦私钥泄露,过去几年的加密流量全变成明文。而 ECDHE 每次握手都生成临时密钥对,私钥没了也不影响历史会话。
2.4.1 Pre-Master Secret 到 Master Secret 的推导
不管用哪种方式,双方最终都能算出一个相同的 Pre-Master Secret(在 ECDHE 里是 ECDH 共享密钥)。然后各自利用客户端随机数、服务端随机数、Pre-Master Secret,通过 PRF(伪随机函数)生成 Master Secret,再进一步派生出加密密钥、MAC 密钥和 IV。
// 推导过程(TLS 1.2)
master_secret = PRF(pre_master_secret, "master secret",
ClientHello.random + ServerHello.random)[0..47];
// 然后生成密钥块
key_block = PRF(master_secret, "key expansion",
ServerHello.random + ClientHello.random);
// 从中切分出:
client_write_MAC_key = key_block[0..19] // 部分套件
server_write_MAC_key = key_block[20..39]
client_write_key = key_block[40..55] // 假设 AES-128
server_write_key = key_block[56..71]
client_write_IV = key_block[72..83] // 固定 IV(部分模式)
server_write_IV = key_block[84..95]
审计日志里不需要记录整个密钥块,但一定要记录推导时使用的随机数版本、摘要算法(如 SHA-256)。这是因为如果未来怀疑某个会话被破解,可以通过日志中的随机数反推密钥块(当然,前提是你有 Pre-Master Secret)。
实践建议:在日志中记录 ClientHello.random、ServerHello.random 和 选中的密码套件 三个字段,就可以唯一确定一次握手的密钥派生路径。我曾用这个方法帮开发团队定位过一次“不同环境密钥不一致”的奇怪 bug。
2.5 握手结束:ChangeCipherSpec 与 Finished
密钥协商完成后,双方各自发送 ChangeCipherSpec 消息,告诉对方“接下来我要用刚协商的密钥加密了”。然后发送 Finished 消息——里面包含对整个握手消息的 HMAC 摘要,用来验证握手过程没有被篡改。
审计日志里,Finished 消息本身一般不记录具体内容(太长了),但可以记录 是否收到了合法的 Finished。如果 Finished 验证失败,说明中间有人动了握手包。嗯,那一刻通常意味着你已经被 MITM 了。
总结一下这一章的核心:TLS 握手不是一个黑盒,ClientHello 的密码套件列表、ServerHello 的选择结果、证书链详情、密钥交换方式,这四个环节的审计日志足以帮你做大多数安全分析和性能诊断。别小看那几个随机数和加密套件标识,它们就是你的侦探放大镜。