三、TLS 1.2 握手流程:七步走,少一步都不行

先把话说在前头——TLS 1.2 的握手流程,说白了就是客户端和服务器之间一套严谨的「互相验明正身 + 商量加密方案」的过程。

我入行那会儿,面试官最喜欢问的就是:「你把 TLS 握手从头到尾背一遍。」当时觉得烦,后来排查过几次线上问题才明白——这七个步骤,少一步,安全就垮了。

咱们走一遍完整流程。每一步我都会顺带讲一个「我在项目里踩过的坑」,帮你少走弯路。

3.1 握手全景图

先看这张流程图。不用急着记细节,跟着箭头过一遍就行。

TLS 1.2 握手总览 ① ClientHello ② ServerHello ③ 证书 (Certificate) ④ ServerKeyExchange (可选的) ⑤ ClientKeyExchange (预主密钥) ⑥ ChangeCipherSpec (更换密码) ⑦ Finished (握手完成) 🔒 握手完成 → 安全通道建立 图例: 握手协商 身份认证 密钥交换 客户端密钥 密码切换 握手验证

七个步骤,我拆成三段来讲:协商阶段、密钥交换阶段、握手验证阶段。这样你脑子里的脉络会清晰很多。

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」,脑子里马上过一遍这张表。从第①步开始查,走完第⑦步,一般都能定位到原因。