TLS 握手协议详解:四次握手、密钥交换、会话票证

说实话,TLS 握手是整个加密通道的「入场券」。我见过不少团队在业务代码里把证书配好就以为万事大吉,结果握手阶段就埋下了性能或安全的雷。这一章咱们就把它拆开看透。

四次握手:从 ClientHello 到 Finished

标准的 TLS 1.2 握手是四次交互(不算最后的 ChangeCipherSpec)。你想想看,客户端和服务端在公开信道上交换消息,却能协商出只有双方知道的对称密钥——这就是握手的精髓。

具体流程我总结成下面这张表,方便你对照抓包看:

步骤 方向 消息 携带关键信息
1 C → S ClientHello TLS 版本、密码套件列表、随机数(Client Random)、扩展
2 S → C ServerHello 选中的密码套件、服务端随机数(Server Random)、会话 ID
3 S → C Certificate + ServerKeyExchange* + ServerHelloDone 证书链、密钥交换参数(如 DH 公钥)、签名
4 C → S ClientKeyExchange + [ChangeCipherSpec] + Finished 预主密钥(RSA 加密)或 DH 公钥、握手哈希验证
5 S → C [ChangeCipherSpec] + Finished 服务端握手验证

实战提示:ChangeCipherSpec 严格来说不属于握手协议(是单独的记录层协议),但抓包时你总是看到它出现在 Finished 之前。我个人习惯把这一步算进握手,方便团队沟通。

密钥交换:RSA 和 ECDHE 的抉择

密钥交换是握手的核心动作。常见的两种:RSA 和 (EC)DHE。我一直说,选错方案可能直接让「前向安全性」变成纸上谈兵。

RSA 密钥交换

客户端生成一个 46 字节的预主密钥(Pre-Master Secret),用服务端证书里的 RSA 公钥加密,发给服务端。服务端用私钥解密,然后双方各自用 Client Random + Server Random + 预主密钥计算出主密钥。

缺点很明显:私钥一泄露,所有历史会话都能被解密。我曾经审计过一个金融项目,他们用了五年 RSA 密钥交换,私钥管理还特别松散——想想都后怕。

ECDHE 密钥交换

双方各自生成临时椭圆曲线密钥对,交换公钥,通过 ECDH 计算出共享的预主密钥。这个预主密钥是临时的,用完就丢,私钥泄露只影响未来会话,不影响过去。

说白了,这就是「前向安全性」的保障。我现在接手的项目一律强制要求 ECDHE,除非有遗留系统兼容的硬伤。

核心对比

  • RSA:不提供前向安全性,私钥是静态的。
  • ECDHE:提供前向安全性,临时密钥每次握手随机生成。
  • ECDHE 需要额外一次服务端签名(用证书私钥对 DH 参数签名),但可以省略证书验证的环节——没有 ECDHE 签名,中间人可以伪造密钥。

会话票证:省掉一次完整握手

完整握手需要两次往返(2-RTT)。如果客户端之前连过同一台服务端,可以用会话复用跳过大部分步骤。早期方案是 Session ID 服务端缓存,但分布式场景很难同步。后来有了会话票证(Session Ticket)。

原理很简单:服务端把会话状态(主密钥、密码套件等)加密成票证,发给客户端。下次握手时客户端把票证带回来,服务端解密重用。

注意:票证由服务端加密,密钥必须安全存储。我遇到过有人把票证密钥写死在代码里,结果密钥泄漏后所有票证都可以伪造——这不是 TLS 的锅,是使用姿势的问题。

启用会话票证后,握手变成 1-RTT 甚至 0-RTT(TLS 1.3 支持)。实战中我建议至少打开 Session Ticket,对长连接频繁重建的场景效果立竿见影。

SVG:TLS 1.2 完整握手流程

下面这张图把四次握手和密钥交换串起来了,蓝色节点是核心加密参数,红色节点是安全注意事项。你可以对照抓包时的消息顺序来理解。

TLS 1.2 完整握手流程(ECDHE 密钥交换) 客户端 服务端 ① ClientHello(版本、套件、随机数) ② ServerHello + Certificate + ServerKeyExchange ③ ClientKeyExchange(DH 公钥) ④ ChangeCipherSpec + Finished(服务端) 密钥派生 预主密钥 ← ECDHE 共享秘密 主密钥 ← PRF(预主密钥 + 两个随机数) 会话票证 加密的主密钥状态 💡 说明 实际抓包中,服务端可能在 ServerHello 后立即发送 Certificate 和 ServerKeyExchange;客户端 Finished 属于第一个加密记录。 ECDHE 方案中,ClientKeyExchange 只携带 DH 公钥,不加密。

避坑指南:我踩过的握手雷区

  • 密码套件顺序不匹配:服务端选第一个客户端支持的套件,但客户端可能把弱套件排前面。我曾经在客户现场发现服务端配置的优先级顺序和客户端不一致,导致协商出 RC4——还好只是测试环境。
  • 证书链不完整:服务端只发了叶子证书,没有中间 CA。Android 某些低版本直接握手失败。记得把中间证书和根证书一起发(根可略,但中间必须带)。
  • 随机数重复:客户端随机数如果重用,结合特定攻击可以探测会话。虽然概率低,但我在代码审计中见过有人用 time() 当随机数——这是自杀式写法。
  • 会话票证过期:票证有有效期,但有些实现不检查 ticket_lifetime,导致服务端解密失败后静默回退到完整握手。抓包时看到突然多了一轮握手,大概率是票证过期。

实用调试命令:用 OpenSSL 可以模拟握手全过程,我经常用这行命令检查服务端是否支持 ECDHE:

openssl s_client -connect yourserver:443 -tls1_2 -cipher ECDHE 2>/dev/null | grep -i "Server Temp Key"

如果输出显示 EC 曲线和密钥长度,说明 ECDHE 生效。没输出?那就是 RSA 了。

小结:握手的本质是信任传递

四次握手不只是交换随机数。它完成了三件事:身份验证(证书)、密钥协商(预主密钥)、会话复用(票证)。你想想看,没有证书的握手就像匿名聊天——能加密但不知道对方是谁。没有 ECDHE 的握手就像写日记还共享密码本——过去的事都能被翻出来。