4. TLS 1.2握手详解:ClientHello消息结构、ServerHello与密码套件协商、服务器证书与CertificateRequest
好,咱们正式开始啃TLS 1.2握手协议。这一章是整个课程的核心地基。你想想看,客户端和服务器第一次见面,怎么互相确认身份?怎么商量用什么加密算法?这些全在握手阶段搞定。
我个人习惯把TLS握手比作一场「加密相亲」——双方先亮出自己会什么(Hello),然后挑一个双方都满意的方案(密码套件协商),最后出示身份证(证书)。今天我们就按这个流程,把每一步拆开来看。
核心要点:TLS 1.2握手共需2个RTT(往返时间),其中ClientHello和ServerHello是第一步,证书交换是第二步。理解这两个阶段,你就掌握了握手的骨架。
4.1 ClientHello:客户端的第一声招呼
握手一开始,客户端会发送一个ClientHello消息。说白了,就是告诉服务器:「我来了,我支持这些功能,你看哪个合适?」
我截取过一个真实的ClientHello包,结构大致如下:
Handshake Protocol: Client Hello
Version: TLS 1.0 (0x0301) ← 注意这里
Random: 1c 9b 3a 8f ... (32字节)
Session ID Length: 32
Session ID: a3 7f ...
Cipher Suites Length: 28
Cipher Suites: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
...
Compression Methods Length: 1
Compression Methods: null (0x00)
Extensions Length: 217
Extensions: ...
这里有个坑,我刚开始抓包时经常被搞晕——Version字段写的是TLS 1.0 (0x0301),但客户端实际支持的是TLS 1.2。为什么?因为TLS 1.2的ClientHello里,Version字段固定填0x0301(即TLS 1.0),真正的版本号在Supported Versions扩展里声明。这是为了兼容老服务器。
避坑指南:我曾经在排查一个握手失败问题时,看到ClientHello的Version是0x0301,就以为客户端只支持TLS 1.0。折腾了半天才发现,真正的版本在扩展里。记住:永远不要只看Version字段,要看Supported Versions扩展。
ClientHello里最重要的几个字段:
- Random:32字节随机数,用于后续生成会话密钥。每次握手都不一样。
- Cipher Suites:客户端支持的密码套件列表,按优先级排序。服务器会从中选一个。
- Extensions:扩展区域,承载了SNI(服务器名称指示)、ALPN(应用层协议协商)、Supported Groups(支持的椭圆曲线)等关键信息。
4.2 ServerHello与密码套件协商
服务器收到ClientHello后,会回复一个ServerHello。这个包的结构和ClientHello很像,但内容更精简——服务器只需要告诉客户端:「我选了这个版本、这个密码套件、这个随机数。」
密码套件协商是握手中最关键的一步。服务器从客户端提供的列表中,挑一个它自己也支持的。如果一个都不匹配,握手直接失败。
我见过一个真实案例:客户端只支持TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,但服务器只配置了TLS_RSA_WITH_AES_256_CBC_SHA。结果就是握手失败,浏览器报ERR_SSL_VERSION_OR_CIPHER_MISMATCH。排查起来其实很简单——抓包一看,ServerHello里直接返回了Alert。
| 密码套件示例 | 密钥交换 | 认证 | 加密 | MAC/PRF |
|---|---|---|---|---|
| TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | ECDHE | RSA | AES-128-GCM | SHA256 |
| TLS_RSA_WITH_AES_256_CBC_SHA | RSA | RSA | AES-256-CBC | SHA1 |
| TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 | DHE | RSA | AES-128-CBC | SHA256 |
你看这个表格,密码套件名字里其实包含了所有信息。比如TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
- ECDHE:使用椭圆曲线Diffie-Hellman密钥交换(前向安全)
- RSA:使用RSA证书进行身份认证
- AES-128-GCM:使用AES-128-GCM进行对称加密
- SHA256:使用SHA256作为PRF(伪随机函数)
个人经验:我建议你在抓包时,重点关注密码套件里的密钥交换算法。如果是RSA密钥交换(即套件名里只有RSA,没有DHE或ECDHE),说明握手不支持前向安全性。现在的主流实践是优先使用ECDHE。
4.3 服务器证书与CertificateRequest
ServerHello之后,服务器会发送Certificate消息,把自己的证书链发给客户端。证书里包含了服务器的公钥、颁发者信息、有效期等。
证书的结构是X.509格式,抓包看的话,主要关注这几个字段:
- Subject:证书持有者,比如
CN=*.example.com - Issuer:颁发者,比如
CN=DigiCert Global CA - Validity:有效期,注意检查是否过期
- Subject Public Key Info:公钥信息,包括算法(RSA/ECC)和密钥长度
如果服务器要求客户端也出示证书(比如双向认证场景),它会额外发送一个CertificateRequest消息。这个消息里包含了服务器可接受的CA列表和证书类型。
我记得有一次帮客户排查一个金融接口的问题,他们的API要求双向TLS认证。客户端一直报错,我抓包一看——服务器发了CertificateRequest,但客户端根本没发证书。原因是客户端代码里没配置客户端证书。加上之后,握手就通了。
关键点:CertificateRequest是可选的。绝大多数Web场景(比如HTTPS)都是单向认证——服务器发证书,客户端验证。只有银行、企业内部系统等场景才会用双向认证。
服务器发完Certificate(和可选的CertificateRequest)后,还会发一个ServerHelloDone消息。这个包没有内容,就是一个标记——告诉客户端:「我发完了,该你了。」
嗯,到这里,握手的第一阶段就结束了。客户端收到了服务器的证书、选好的密码套件、以及随机数。接下来,客户端要验证证书、生成预主密钥、然后发送自己的密钥交换信息。这些我们下一章再细聊。
抓包小技巧:用Wireshark过滤TLS握手时,可以直接用 tls.handshake.type == 11 过滤Certificate消息(type 11),用 tls.handshake.type == 13 过滤CertificateRequest(type 13)。这样能快速定位证书相关的问题。