2. TLS握手协议详解:客户端Hello、服务器Hello、证书交换、密钥协商

好,咱们正式进入握手协议的世界。这部分是整个TLS的心脏,也是最容易踩坑的地方。

握手协议说白了就干三件事:确认身份、协商密码、生成会话密钥。整个流程大概需要两次往返(TLS 1.2),也就几毫秒的事。但就是这几毫秒,我见过无数生产环境出问题,都是握手阶段挂了。

2.1 第一回合:客户端Hello

握手的第一步,客户端发一个 ClientHello 消息给服务端。

这个包长得挺规矩的,包含以下核心字段:

  • TLS版本:客户端支持的最高版本,比如 TLS 1.2
  • 随机数:32字节随机种子,后面用来算密钥
  • 密码套件列表:客户端支持的所有CipherSuite,按优先级排序
  • 扩展字段:SNI、ALPN、支持的签名算法等

我个人的经验:我第一次抓包分析TLS时,就栽在密码套件列表上。客户端列了20多个套件,服务端一个都不支持,直接握手失败。

来看一个典型的ClientHello抓包,我用Wireshark截的:

TLSv1.2 Record Layer: Handshake Protocol: Client Hello
    Handshake Type: Client Hello (1)
    Version: TLS 1.0 (0x0301)
    Random: 1c 9b a2 3f 8d 4e ...
    Cipher Suites Length: 58
    Cipher Suites (29 suites)
        Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
        Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
        Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
        ...
    Extensions Length: 218
    Extension: server_name (len=23)
    Extension: supported_groups (len=12)
    Extension: signature_algorithms (len=30)

注意一下这里:很多同学以为ClientHello里填的是TLS 1.2,其实版本字段写的是0x0301(TLS 1.0)。这是历史遗留问题。服务端收到后会根据实际能力再决定用哪个版本。

2.2 第二回合:服务器Hello

服务端收到ClientHello后,也不是全盘接收。它会从客户端提供的列表中挑一个自己能支持的组合,然后回复 ServerHello。

ServerHello的字段和ClientHello基本对称:

  • 选择的TLS版本:我选TLS 1.2
  • 服务器随机数:也是32字节,用于密钥计算
  • 选择的密码套件:比如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  • Session ID:如果要做会话复用,这里填一个ID

一个坑:有一次我在项目中遇到客户端和服务端版本协商失败,查了半天发现服务端配置的TLS版本列表里没写TLS 1.2,只写了1.0和1.1。但其实客户端只带了1.2,两边直接不匹配,握手失败。所以,服务器支持的版本别设得太死。

ServerHello发完,服务端紧接着会发 Certificate、ServerKeyExchange(如果套件需要)、ServerHelloDone。

2.3 证书交换:你真的是你吗?

这一步的核心是身份验证。服务端把自己的证书链发给客户端。

证书链的结构是这样的:

  1. 服务器证书(叶子证书)
  2. 中间CA证书(可能多个)
  3. 根CA证书(一般不发送)

客户端收到后会做一系列校验:

  • 证书是否过期
  • 证书域名是否匹配
  • 签名链是否可信
  • 证书撤销状态(可选)

我曾经遇过一个非常隐蔽的问题:某个服务端发了完整的证书链,但中间证书过期了。客户端在校验时发现签名链断了,直接拒绝连接。排查了大半天,才注意到那个不起眼的中间证书有效期。所以,证书链里的每一张证书都要在有效期内。

有些场景还会做客户端证书验证(双向认证)。这时服务端会在CertificateRequest消息里要求客户端也发证书。不过大多数Web场景只做单向认证。

2.4 密钥协商:真正的加密从这里开始

证书交换完成后,就到了最关键的密钥协商。当前主流方案是 ECDHE(临时椭圆曲线Diffie-Hellman)。

ECDHE交换过程大致如下:

步骤 消息方向 内容
1. 服务端发送 Server → Client 椭圆曲线参数 + 服务端公钥(签名)
2. 客户端发送 Client → Server 客户端公钥
3. 双方计算 - 使用ECDH算法计算出预主密钥(Pre-Master Secret)
4. 派生 - 通过PRF将预主密钥 + 客户端随机数 + 服务器随机数 → 主密钥
5. 生成工作密钥 - 从主密钥派生出加密密钥、MAC密钥、IV等

这里有个关键点:ECDHE的私钥是临时生成的,用完就丢。所以即使未来有人拿到了服务器的长期私钥,也无法解密以前抓到的会话数据。这就是前向安全性(Perfect Forward Secrecy)。

如果是RSA密钥交换(旧方案),客户端直接生成预主密钥,用服务端公钥加密后发给服务端。但这种方式不具备前向安全性,我建议所有新系统都不要用。

来看伪代码展示一下密钥计算:

// 服务端
serverPrivateKey, serverPublicKey = generateECDHKeyPair()
// 将serverPublicKey签名后发给客户端

// 客户端
clientPrivateKey, clientPublicKey = generateECDHKeyPair()
// clientPublicKey发给服务端

// 两边各自计算
preMasterSecret = ECDH_agree(clientPrivateKey, serverPublicKey) 
                 // 或者 ECDH_agree(serverPrivateKey, clientPublicKey)
                 // 结果相同

// 派生主密钥
masterSecret = PRF(preMasterSecret + clientRandom + serverRandom, "master secret")

// 派生会话密钥
keyBlock = PRF(masterSecret, "key expansion", serverRandom + clientRandom)
clientWriteKey = keyBlock[0:16]
serverWriteKey = keyBlock[16:32]
// 还有IV、MAC密钥等

说白了就是:握手阶段产生两个随机数(客户端和服务器各一个),再通过ECDHE交换得到预主密钥。这三个种子一混合,最后通过PRF函数不断拉伸、混合、再拉伸,得到最终的工作密钥。

2.5 握手完成与Change Cipher Spec

密钥协商完成后,双方会互相发一个 Change Cipher Spec 消息,告诉对方:后面我要开始加密通信了。

紧接着发一个 Finished 消息,里面是所有握手消息的摘要(用刚才协商好的密钥加密)。

如果双方都能正确解密并验证Finished消息,握手正式完成。从此以后的数据就全加密了。

我的一个调试技巧:如果TLS连接建立了但业务数据传不过去,我通常会先看看握手阶段是否完整。在Wireshark里看,是否有Finished消息,以及加密后的数据是否正常。很多时候问题出在握手阶段没走完,但客户端以为已经连接成功了。

整个TLS 1.2握手流程,我把它们画成了一张流程图,方便你对照理解:

TLS 1.2 握手协议核心流程 客户端 服务端 ① ClientHello 版本 + 随机数 + 密码套件 → ② ServerHello 选择版本 + 套件 + 随机数 ③ Certificate 证书链 + 签名 ④ ServerKeyExchange 椭圆曲线 + 公钥(签名) ⑤ ServerHelloDone ← ⑥ ClientKeyExchange 客户端公钥(未签名) → ⑦ 双方计算预主密钥 → PRF派生主密钥 → 生成会话密钥 ⑧ ChangeCipherSpec + Finished ⑨ ChangeCipherSpec + Finished ↔

你把这个流程记在心里,后面讲配置和排错时,你会发现大部分问题都能对号入座。握手协议的核心其实就是:用非对称加密交换对称密钥。

具体来说,握手协议解决了三个问题:

  • 你是谁:通过证书验明正身
  • 我们用什么加密:通过密码套件协商确定算法
  • 密钥怎么来:通过ECDHE或RSA等手段,双方安全地算出同一个密钥

这三件事搞定了,后面的加密通信就水到渠成了。嗯,接下来我们可以深入看看每种密钥交换算法的细节,以及如何在实际项目中选择最合适的配置。