3、TLS握手协议详解:握手流程、密钥交换
TLS 握手,说白了就是客户端和服务器在正式传输数据前,先互相打个招呼、亮明身份、商量好用什么锁来加密。这个过程直接决定了后续通信的安全性。
我早期做支付接口对接时,就因为没搞清楚握手的细节,调试了整整一个通宵。那会儿才深刻体会到——理解握手,就是理解 TLS 的基础。你别看它步骤多,其实核心就三件事:身份认证、密钥协商、参数确认。
3.1 握手阶段概览
一个完整的 TLS 1.2 握手,通常需要 2 个往返(2-RTT)。TLS 1.3 精简到 1 个往返(1-RTT)。我们今天以更常见的 TLS 1.2 为例来拆解。
你可以把握手想象成一次面试:
- ClientHello —— 求职者递上简历,说明自己会什么(支持的加密套件)。
- ServerHello —— 面试官回话,挑一个双方都会的技能。
- 证书 + 密钥交换 —— 面试官出示工牌(数字证书),双方敲定一个只有彼此知道的暗号(预主密钥)。
- Finished —— 双方确认刚才的对话没有被篡改过。
嗯,这里要注意:TLS 1.3 去掉了独立的 ChangeCipherSpec 报文,并默认使用 ECDHE 进行密钥交换。如果你还在维护老旧系统,建议尽快迁移。
3.2 握手第一阶段:Hello 消息
握手的第一步,永远是从客户端开始的。
3.2.1 ClientHello
客户端发送给服务器的第一条消息。里面包含哪些信息?我给你列个表:
| 字段 | 说明 | 我遇到的坑 |
|---|---|---|
| 版本号 | 客户端支持的最高 TLS 版本 | 有些老设备只发 TLS 1.0,现在基本被拒绝连接 |
| 随机数 | 32 字节的随机数,用于后续密钥生成 | 必须真随机,否则会被预测 |
| 会话 ID | 用于会话复用,可以为空 | 我遇到过一个负载均衡配置错误,导致会话 ID 频繁失效 |
| 密码套件列表 | 客户端支持的加密算法组合 | 顺序很重要!服务器一般选第一个它认识的 |
| 扩展字段 | 如 SNI、ALPN、支持的安全参数等 | SNI 是必须的,否则多域名服务器没法正确返回证书 |
我个人习惯在抓包时先看 ClientHello 里的密码套件列表,如果看到 `TLS_RSA_WITH_AES_128_CBC_SHA` 这种老古董,基本能判断对手是十年前的设备。
提示: 如果你的服务器有多个域名,一定要确保 ClientHello 中携带了 SNI (Server Name Indication) 扩展。否则服务器不知道给哪个证书,直接返回默认证书或报错。
3.2.2 ServerHello
服务器收到 ClientHello 后,会做两件事:
- 从客户端的密码套件列表里,挑一个它支持的。通常是第一个匹配上的。
- 生成自己的 32 字节随机数,连同选中的版本号、密码套件一同发回去。
这里有个细节:服务器选择的密码套件,直接决定了后续的密钥交换算法。比如选了 `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`,就意味着用 ECDHE 做密钥交换、RSA 做签名、AES-128-GCM 做对称加密、SHA256 做哈希验证。
为什么需要两个随机数(客户端和服务器各一个)?你想想看,如果只用一方的随机数,很容易被另一方主导。两边各出一半随机性,最终生成的密钥才够「不可预测」。
3.3 密钥交换与身份验证
这是握手最核心、最容易出错的部分。说白了,就是双方要商量出一个只有他们俩知道的「共享秘密」,同时还要确认对方就是他声称的那个人。
3.3.1 服务器证书发送
ServerHello 之后,服务器紧接着发送自己的数字证书。这个证书里包含:
- 服务器公钥
- 证书颁发机构(CA)的数字签名
- 证书有效期、域名等信息
客户端收到证书后,会做一次「证书链验证」。简单说就是检查这个证书是不是由受信任的 CA 签发的,域名对不对,有没有过期或吊销。
警告: 千万不要跳过证书验证!我曾经在一个内网项目里,为了图省事把客户端证书验证关了。结果一个月后被人中间人攻击,所有流量被截获。那之后我再也不敢偷懒了。
3.3.2 密钥交换算法对比
目前主流的密钥交换算法就两种:RSA 和 ECDHE。我建议你在新项目中尽量别用 RSA 做密钥交换,它不具备前向安全性。
什么叫前向安全性?就是即使服务器私钥泄露了,之前记录的流量也无法被解密。ECDHE 能做到,RSA 不行。
| 算法 | 前向安全性 | 性能 | 我的建议 |
|---|---|---|---|
| RSA 密钥交换 | ❌ 无 | 中等 | 不要用,特别是 HTTPS |
| ECDHE | ✅ 有 | 较高(比 RSA 快) | 首选的密钥交换方式 |
| DHE | ✅ 有 | 较低(计算量大) | 可以用,但 ECDHE 更优 |
ECDHE 的流程,我简单说一下:
- 服务器生成一对临时 ECDHE 密钥对(私钥 + 公钥),把公钥发给客户端。
- 客户端也生成一对临时 ECDHE 密钥对,把公钥发给服务器。
- 双方分别用对方的公钥和自己私钥,通过 ECDH 算法计算出相同的「预主密钥」。
注意这里用的是「临时」密钥对。每次握手都会重新生成,所以即使服务器长期私钥泄露,之前的握手密钥也无法被复原。这就是前向安全性的精髓。
3.3.3 预主密钥与主密钥生成
有了预主密钥(Pre-Master Secret)之后,双方会用它加上之前交换的两个随机数,通过一个伪随机函数(PRF)生成 主密钥(Master Secret)。
主密钥长度固定为 48 字节。这个主密钥不会直接用来加密数据,它会被进一步派生出:
- 客户端写入 MAC 密钥
- 服务器写入 MAC 密钥
- 客户端写入加密密钥
- 服务器写入加密密钥
- 客户端写入 IV
- 服务器写入 IV
你看,所有秘密都藏在「预主密钥 + 双方随机数」这个源素材里。只要预主密钥不泄露,所有派生密钥就是安全的。
3.4 握手完成与验证
密钥派生完成后,双方各自发送一条 ChangeCipherSpec 消息(在 TLS 1.2 中),告诉对方:「我接下来要用刚才商量的密钥加密了」。然后紧跟着发送 Finished 消息。
Finished 消息的内容,是之前所有握手消息的 MAC 摘要。接收方会自己计算一遍,如果一致,证明握手过程没有被篡改。如果不一致,直接断开连接。
重点: Finished 消息是整个握手的「最终确认」。如果 Finished 不匹配,说明握手过程中有人动了手脚。我当年排查线上问题时,发现是因为某个中间设备自作聪明地修改了 ServerHello 里的密码套件,导致 Finished 校验失败。那真是罕见的场景。
3.5 握手流程全景图
下面我把整个 TLS 1.2 握手的流程用一张图总结出来。这张图你完全可以贴到自己的笔记里,以后排查时对照着看。
这张图我画了两遍才满意。核心就是 8 步,从 ClientHello 开始,到双方的 Finished 校验结束。只要中间任何一步出错,整个握手就会失败。客户端会直接报错或断开连接。
你可以用 `openssl s_client -connect example.com:443 -tls1_2 -msg` 命令,实地抓一次握手过程。`-msg` 参数能打印出所有握手消息的十六进制内容,对排查问题特别有用。