2. TLS 握手过程详解:ClientHello、ServerHello、证书交换、密钥协商

咱们做安全审计,最怕的是啥?是黑盒。你只看到一堆加密流,但不知道握手阶段到底交换了哪些参数。有一次我在排查一个 HTTPS 性能问题,发现服务端证书链居然有 7 层,握手耗时多了一倍。从那以后我就养成了习惯——必须把握手过程拆开看。

这一章,我就带你走进 TLS 握手的四个核心阶段。嗯,不复杂,但细节很重要。

核心知识点:TLS 1.2 完整握手共 4 步(ClientHello → ServerHello + 证书 → 密钥交换 → Finished),审计日志应捕捉每一步的关键参数。

2.1 握手概览:一张图理清流程

我先给一张流程图,你对照着看后面的文字,会清晰很多。

TLS 1.2 完整握手流程 Client ClientHello ClientKeyExchange ChangeCipherSpec Finished Server ServerHello Certificate ServerHelloDone ChangeCipherSpec Finished ① 协商版本、 密码套件、 随机数 ② 证书链、 ServerHelloDone ③ 客户端生成 Pre-Master Secret 并用公钥加密发送 ④ 双方分别计算 Master Secret

这个流程里我标了四个阶段,咱们一个一个来聊。

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 这种老古董,我基本可以断定这台客户端的安全基线有问题。

避坑提示: 注意 Random 字段里的时间戳。我曾见过一个客户端系统时间歪了 10 年,导致握手后证书验证失败。日志里记的时间戳反而帮你定位了问题来源。

2.3 ServerHello + 证书交换:服务端的选择

服务端收到 ClientHello,会从客户端列表里挑一个它自己也能支持的密码套件。说白了,最终用哪个加密算法,是服务端说了算。你想想看,如果服务端弱配了一套已废弃的套件,客户端根本没机会反驳。

ServerHello 回复的内容和 ClientHello 结构类似,但多了几个关键项:

  • 选择的密码套件(只能一个)
  • 选择的压缩算法(现在全是 null)
  • 服务端随机数(同样 32 字节,审计时记得比对客户端随机数,看看生成方式是否合理)
  • 后面紧跟着 Certificate 消息,把服务端证书链发过来。

证书这块,审计日志里要记什么?

  1. 证书序列号:唯一标识
  2. 颁发者 和 主题(CN 或 SAN)
  3. 有效期:起止时间,是否已过期
  4. 签名算法:比如 sha256WithRSAEncryption
  5. 公钥类型和长度:RSA 2048?ECDSA P-256?还是弱爆了的 1024 位?
  6. 证书链深度:是否包含中间 CA
⚠ 曾经遇到的坑:有一次客户报“部分移动端无法访问”。我查了审计日志,发现服务端把根证书也发到了证书链里——根证书通常不应该在握手阶段传输,很多客户端直接忽略并报错。从那以后,我每次看到 Certificate 消息都会检查链的节点数。

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 的选择结果、证书链详情、密钥交换方式,这四个环节的审计日志足以帮你做大多数安全分析和性能诊断。别小看那几个随机数和加密套件标识,它们就是你的侦探放大镜。