三、TLS 1.2 握手流程:七步走,少一步都不行
先把话说在前头——TLS 1.2 的握手流程,说白了就是客户端和服务器之间一套严谨的「互相验明正身 + 商量加密方案」的过程。
我入行那会儿,面试官最喜欢问的就是:「你把 TLS 握手从头到尾背一遍。」当时觉得烦,后来排查过几次线上问题才明白——这七个步骤,少一步,安全就垮了。
咱们走一遍完整流程。每一步我都会顺带讲一个「我在项目里踩过的坑」,帮你少走弯路。
3.1 握手全景图
先看这张流程图。不用急着记细节,跟着箭头过一遍就行。
七个步骤,我拆成三段来讲:协商阶段、密钥交换阶段、握手验证阶段。这样你脑子里的脉络会清晰很多。
3.2 第一步:ClientHello —— 客户端亮出家底
握手一开始,客户端先发一条消息给服务器,意思很明确:
「我是谁不重要——但我带来了我支持的所有密码套件、TLS 版本和随机数,你看看哪个能用。」
ClientHello 的核心载荷:
- TLS 版本号(客户端能支持的最高版本)
- 客户端随机数(32 字节,前 4 字节是时间戳,后 28 字节随机生成)
- 密码套件列表(Cipher Suites,比如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
- 扩展字段(SNI、ALPN、supported_groups 等)
我曾经在一次性能压测中发现,客户端密码套件列表太长(超过 30 个),导致服务器端解析耗时暴增。后来我们统一精简到 8 个最常用的套件,握手时间直接降了 15%。
3.3 第二步:ServerHello —— 服务器拍板
服务器收到 ClientHello 后,会做两件事:
第一,从客户端发来的密码套件列表里选一个自己也能支持的。
第二,生成自己的 32 字节随机数(服务器随机数),一并返回。
你想想看,如果服务器发现客户端列的所有套件它都不认识——握手直接中断,连个错误提醒都不给。很多新手排查故障时,第一步就是去抓包看双方「密码套件交集」是不是空的。我建议你把这个方法焊死在自己的问题排查清单里。
| 字段 | 说明 | 常见取值 |
|---|---|---|
| Server Version | 服务器最终选用的 TLS 版本 | TLS 1.2 (0x0303) |
| Random | 服务器随机数,32 字节 | 由安全随机数生成器产生 |
| Cipher Suite | 选定的密码套件 | 如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
| Compression Method | 压缩方法(TLS 1.2 后基本不用) | 0x00(不压缩) |
小技巧: 用 Wireshark 抓包时,直接过滤 tls.handshake.type == 2,一眼就能定位到 ServerHello。看其中的 Cipher Suite 字段,就能知道服务器最终选了什么加密策略。
3.4 第三步:证书 —— 服务器亮出「身份证明」
这一步很关键。服务器把自己的证书链(Certificate Chain)发给客户端。
为什么要发一整个链,而不是只发叶子证书?
因为客户端的信任锚点(Root CA)通常内嵌在操作系统里,但中间证书和叶子证书得由服务器告诉客户端。如果只发叶子证书,客户端没法验证它是不是由受信任的根签发的。
警告: 证书链不完整是 HTTPS 排障中最常见的问题之一。
我曾经在给一家金融公司做渗透测试时,发现他们的 API 网关只发了服务器证书,没带上中间证书。结果 iOS 客户端连不上,老版本 Android 也报错。检查方法很简单——用 openssl s_client -connect yoursite.com:443 -showcerts,看看输出里有没有完整的三个证书块。
证书里包含哪些关键信息?
- 公钥(RSA 或 ECC)
- 颁发者(Issuer)
- 主体(Subject)——也就是域名
- 有效期
- 签名(由上一级 CA 签发)
3.5 第四步:ServerKeyExchange —— 传递密钥交换参数
这一步不是必选的。它取决于第三步选的密码套件:
- 如果用的是 DHE(临时 Diffie-Hellman)或 ECDHE(临时椭圆曲线 Diffie-Hellman),服务器需要把自己的 DH 参数(质数 p、生成元 g、公钥等)发给客户端。
- 如果用的是 RSA 密钥交换(不推荐,因为不提供前向安全性),这步可以省略——密钥交换直接在 ClientKeyExchange 里完成。
我个人建议全面使用 ECDHE。RSA 密钥交换在 2025 年的今天,已经可以被认为是有风险的方案了。为什么?因为它没有前向安全性(Perfect Forward Secrecy)。
3.6 第五步:ClientKeyExchange —— 客户端交出「预主密钥」
现在轮到客户端了。
服务器已经把它的公钥(或者 DH 参数)传过来了。客户端的任务就一个:生成一个 48 字节的预主密钥(Pre-Master Secret),然后用服务器的公钥加密后发回去。
只有拥有对应私钥的服务器才能解密出这个预主密钥。这就是 TLS 的核心安全逻辑——一次一密,中间人截到加密报文也解不开。
3.7 第六步:ChangeCipherSpec —— 换密码了
这一步非常短,但意义重大。客户端或服务器说一声:
「从现在开始,我发的所有数据都用刚才协商好的密钥加密了。」
ChangeCipherSpec 的协议号是 0x14(Content Type),消息体只有一个字节:0x01(表示 "pending state" 变成了 "current state")。
严格来说,ChangeCipherSpec 不属于 TLS 握手协议(Handshake Protocol),它属于 TLS 记录协议(Record Protocol)里的一个特殊告警。这也是 TLS 1.3 把它砍掉的原因之一。
我记得有一次排查线上问题,发现 ChangeCipherSpec 报文在防火墙处被拦截了,导致后续所有加密数据都无法正常解密。最后定位出来是防火墙的「深层包检测」把 0x14 这个 Content Type 当成了恶意流量。
3.8 第七步:Finished —— 验证握手完整性
最后一步,双方各自计算一个 verify_data:用之前协商好的主密钥,把整个握手过程的哈希值算一遍,加密后发给对方。
对方收到后,用自己的计算结果对比一下——
「嗯,中间的报文没被篡改过。」
Finished 消息是第一条真实加密的消息。如果解密失败,连接会立刻断开,客户端会收到一个 bad_record_mac 告警。
到这里,握手彻底结束。双方都拥有了相同的对称密钥,后面的应用数据传输全是加密的——HTTPS 里的那个 S 终于生效了。
3.9 一张表总结全流程
| 步骤 | 方向 | 内容 | 是否加密 |
|---|---|---|---|
| ① ClientHello | C → S | 客户端随机数、密码套件列表 | 明文 |
| ② ServerHello | S → C | 服务器随机数、选定的套件 | 明文 |
| ③ 证书 | S → C | 公钥证书链 | 明文 |
| ④ ServerKeyExchange | S → C | DH 参数或 ECDHE 公钥 | 明文 |
| ⑤ ClientKeyExchange | C → S | 加密的预主密钥 | 用服务器公钥加密 |
| ⑥ ChangeCipherSpec | C → S | 切换加密状态 | 明文(但后续加密) |
| ⑦ Finished | C → S | 握手完整性验证 | 加密 |
如果你哪天接到一个紧急工单——「HTTPS 连接失败,报错 SSL_ERROR_HANDSHAKE_FAILURE」,脑子里马上过一遍这张表。从第①步开始查,走完第⑦步,一般都能定位到原因。