3、TLS握手协议:握手流程、ClientHello、ServerHello、证书交换、密钥协商

TLS握手是整个安全通道的“开篇”。说白了,双方一上来谁都不信任谁,得先亮明身份、商量好怎么加密,然后再开始传数据。我最早做HTTPS抓包时,看着那一堆Change Cipher Spec、Finished头都大,后来亲手调过一次跨版本兼容性bug才真正搞懂每一步的意义。

握手流程总览

一个完整的TLS 1.2握手,大致走下面这几步。注意这里说的是最典型的“完全握手”,如果你用了会话恢复,步骤会少很多。

  1. 客户端发ClientHello:告诉服务器我支持哪些协议版本、密码套件、扩展信息。
  2. 服务器回ServerHello:挑一个双方都认的版本和套件,顺便带上自己的随机数。
  3. 服务器发Certificate:把公钥证书链丢过来,证明自己是谁。
  4. 服务器发ServerKeyExchange(可选):如果密码套件需要额外的密钥参数(比如DHE),就在这一步补充。
  5. 服务器发CertificateRequest(可选):如果服务器要求客户端也出示证书。
  6. 服务器发ServerHelloDone:表示“我说完了,该你了”。
  7. 客户端发ClientCertificate(可选):只有在服务端要求时才发。
  8. 客户端发ClientKeyExchange:传递密钥协商所需的参数,比如预主密钥。
  9. 客户端发CertificateVerify(可选):证明客户端持有私钥。
  10. 双方都发ChangeCipherSpec:通知对方接下来要开始加密通信了。
  11. 双方都发Finished:用协商好的密钥加密一段验证数据,确保前面没人篡改。

关键点:第4步的ServerKeyExchange只出现在DHE、ECDHE这类需要临时密钥的密码套件中。如果是RSA密钥交换,服务器直接用证书里的公钥加密,不需要这一步。

ClientHello —— 客户端的“自我介绍”

握手的第一步,客户端发一个明文的消息。结构其实很简单,但里面藏着不少坑。我个人习惯用Wireshark抓包时,先看Version字段和Cipher Suites列表。

TLS 1.2 Record Layer: Handshake
  Handshake Type: Client Hello (1)
  Version: TLS 1.0 (0x0301)   // 注意!这里版本号占位是0x0301,但实际支持的版本在Supported Versions扩展里
  Random: 32字节随机数
  Session ID: 可为空(第一次连接)
  Cipher Suites: 客户端支持的套件列表(按优先级排序)
  Compression Methods: 通常是0x00(NULL)
  Extensions: 长度可变,比如SNI、Supported Groups、Key Share等

你可能会问,为什么Version字段写的是TLS 1.0(0x0301)而不是TLS 1.2(0x0303)?这是历史遗留问题。为了兼容旧实现,很多客户端故意用低版本号占位,真正的版本通过Supported Versions扩展(RFC 8446)声明。

小技巧:我在排查“TLS握手失败”时,第一件事就是看ClientHello的Cipher Suites列表。有时候是服务端不支持客户端列表里的任何一个,直接发Alert断连。用Wireshark过滤tls.handshake.type == 1就可以定位。

ServerHello —— 服务端的“回应”

服务端收到ClientHello后,挑一个它认可的最高版本和密码套件。注意:服务端必须从客户端提供的列表里选,不能自己瞎定。

  • Version:双方协商后的最终版本,比如TLS 1.2(0x0303)。
  • Random:服务端生成的32字节随机数,独立于客户端。
  • Session ID:如果支持会话恢复,服务端会分配一个ID;否则为空。
  • Cipher Suite:最后选定的那个套件,比如TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
  • Extensions:服务端也会回复一些扩展,比如协商Named Group、密钥分片等。

我记得有一次调试,客户端只支持TLS 1.3,但发的是TLS 1.2格式的ClientHello(因为兼容性)。服务端误判为1.2,结果握手失败。这里的关键就是ServerHello里的版本必须和ClientHello中真正的最高版本一致。

证书交换 —— 信任的基础

服务端发完ServerHello后,紧跟着就是Certificate消息。它包含一个X.509证书链,从服务器证书开始,到根证书(或者中间CA)结束。客户端收到后要验证:

  1. 检查证书是否在有效期内。
  2. 用上级CA的公钥验证签名。
  3. 检查证书是否被吊销(CRL或OCSP)。
  4. 验证证书中的域名是否匹配当前访问的域名(SNI匹配)。

有个容易忽略的点:证书链必须完整。我在一个金融项目中遇到服务端只发了服务器证书,没发中间CA,导致某些Android设备握手失败。因为Android的系统信任存储里可能没有那个中间CA,需要服务端把完整链发下来。

特别注意:如果服务端要求客户端证书,会额外发一个CertificateRequest,里面列出它信任的CA列表。客户端必须提供一个由这些CA签发的证书,否则握手会失败。我曾经在一个内部系统里碰过这种事——客户端没配证书,服务端直接断连,查了半天才发现是双向认证没开。”

密钥协商 —— 安全的种子

证书交换之后,双方需要共同生成一个对称密钥。这个过程叫密钥协商。TLS 1.2里主要有两种:RSA密钥交换和(EC)DHE。

算法 工作原理 前向保密 个人评价
RSA 客户端生成预主密钥,用服务器公钥加密后传过去。 ❌ 不提供 简单但过时,一旦私钥泄露,所有历史会话都可被解密。
DHE/ECDHE 双方各自生成临时密钥对,交换公钥部分,通过DH算法计算共享密钥。 ✅ 提供 我强烈推荐。即使长期私钥泄露,之前握手的会话仍然安全。
PSK 使用预共享密钥直接派生主密钥。 取决于是否结合DHE IoT场景常用,但PSK需要安全分发。

你想想看,为什么现在几乎所有安全建议都要求禁用RSA密钥交换?因为不具备前向保密。如果一个攻击者今天拿到了服务器的私钥,他可以用之前记录的所有RSA握手中的密文解密出预主密钥,然后得到会话密钥,历史流量一览无余。而ECDHE每次握手都使用临时密钥,用完即弃,私钥泄露影响不到过去。

具体到ECDHE密钥协商的流程:

  • 服务端在ServerKeyExchange消息里发送自己的ECDHE临时公钥、选用的椭圆曲线(比如secp256r1),以及用服务器证书私钥对这个参数的签名。
  • 客户端验证签名,证明消息确实来自服务器(防止中间人篡改)。
  • 客户端也生成自己的临时密钥对,在ClientKeyExchange消息里发送公钥。
  • 双方各自用对方的公钥和自己的私钥计算DH共享秘密,这就是预主密钥。

然后预主密钥加上两边的Random值,通过PRF导出主密钥,再导出会话密钥。具体公式我就不列了,你理解核心是:只要攻击者拿不到任何一方的临时私钥,就无法计算共享秘密。

一句话总结:TLS握手的本质是在不安全的信道里,通过证书交换建立信任,再通过密钥协商建立一个只有双方知道的临时对称密钥。后面所有加密都靠这个钥匙。

握手流程图

下面这张图把典型的TLS 1.2完全握手串起来,方便你对照看每一步的先后关系。

TLS 1.2 完全握手流程图 TLS 1.2 完全握手流程图 客户端 服务端 ① ClientHello ② ServerHello ③ Certificate ④ ServerKeyExchange (可选) ⑤ ServerHelloDone ⑥ ClientKeyExchange ⑦ ChangeCipherSpec (双方互发) ⑧ Finished (双方互发) 握手完成后开始加密通信

图中需要注意:ServerKeyExchange这一步只在DHE/ECDHE套件中出现,RSA套件没有。另外,ChangeCipherSpec和Finished都是双方各自独立发送的,并非服务器先发客户端后发,而是双方在各自收齐对方消息后触发。

实战建议:如果你用OpenSSL命令行,可以加-msg参数看到每个握手消息的十六进制内容,配合Wireshark一起分析效果最佳。我每次调试SSL/TLS问题都是两边的抓包同时看,对照时间戳确认哪一步出的问题。