TLS 握手基本流程:Cipher Suite 协商、证书交换、密钥交换详解
TLS 握手,说白了就是客户端和服务器在正式开始加密通信前,先互相确认身份、商量加密算法、生成共享密钥的过程。我处理过上百次握手失败的案例,每次排查的第一步,就是搞清楚握手卡在了哪个阶段。
整个握手可以拆成三个核心环节:Cipher Suite 协商、证书交换与验证、密钥交换与派生。下面我逐一展开,里面会穿插一些实战中踩过的坑。
1. Cipher Suite 协商——谁来决定加密方案?
握手的第一步,客户端会发一个 ClientHello 消息,里面带一个长长的列表——支持的密码套件(Cipher Suite)。说白了,就是客户端告诉服务器:“我会的这些加密算法组合,你挑一个咱俩都能用的。”
服务器收到后,从列表里选一个自己支持的,通过 ServerHello 回复。如果服务器的密码套件列表和客户端完全没有交集……嗯,握手直接就断了。我曾经在一个项目中,客户端的 OpenSSL 版本太老,只支持 RC4 和 3DES,而服务器已经禁用了所有弱算法,双方僵在那儿,日志里就显示 no shared cipher。
tls.handshake.type == 2 就能快速定位。
一个典型的密码套件名称长这样:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
| | | | | |
| 密钥交换 认证 对称加密 模式 哈希
我习惯把 Cipher Suite 的四个要素记在脑子里:密钥交换算法、认证算法、对称加密算法、MAC/PRF 算法。缺一不可。
2. 证书交换与验证——你真是你吗?
协商完密码套件后,服务器会发来它的证书链(Certificate 消息)。客户端拿到证书后要做三件事:
- 验证签名:用 CA 的公钥验签,确保证书未被篡改。
- 检查有效期:当前时间必须在证书的 valid_from 和 valid_to 之间。我见过很多因为服务器系统时间不准导致证书过期误报的案例。
- 检查域名:证书中的 Common Name (CN) 或 Subject Alternative Name (SAN) 必须匹配客户端请求的域名。
certificate unknown。解决方法很简单——让服务器配置完整的证书链。
如果验证全部通过,客户端会生成一个 pre-master secret(预主密钥),然后用服务器的公钥加密后发送回去——这就进入了第三阶段。
3. 密钥交换——安全地共享秘密
密钥交换的方式取决于之前协商的 Cipher Suite。我主要讲两种常见场景:
| 密钥交换算法 | 核心原理 | 常见场景 |
|---|---|---|
| RSA 密钥交换 | 客户端生成 pre-master secret,用服务器 RSA 公钥加密后发送 | 老旧系统,TLS 1.2 及以前 |
| (EC)DHE 密钥交换 | 双方各自生成临时 DH 参数,交换公钥,各自计算共享密钥 | 现代实践(提供前向安全性) |
如果是 ECDHE,服务器会先发 ServerKeyExchange 消息,带上椭圆曲线参数和签名。客户端验证签名后,生成自己的临时密钥对,通过 ClientKeyExchange 把公钥发过去。最后双方各自计算出相同的 pre-master secret。
我的经验:ECDHE 虽然安全,但在高并发场景下,椭圆曲线运算可能成为瓶颈。我优化过一个 HTTPS 网关,把椭圆曲线从 P-256 切换到 X25519,CPU 占用率下降了约 30%。当然前提是客户端也支持。
一旦 pre-master secret 生成完毕,双方会用它派生出对称加密的会话密钥(master secret -> key material)。然后各自发送 ChangeCipherSpec 和 Finished 消息,验证握手完整性。至此,TLS 握手完成,后续数据全部加密传输。
小结一下
三个环节缺一不可:密码套件协商决定了算法;证书交换建立了信任;密钥交换生成了会话密钥。你想想看,任何一个环节出问题,握手都会失败。我在排障时,总是先从 Wireshark 的握手流程图入手,看具体卡在哪个 message 上,然后对照这三个阶段定位根因。
- ClientHello 中的 Cipher Suites 列表是否完整?
- ServerHello 中是否返回了有效的 Cipher Suite?
- Certificate 消息中的证书链是否完整且可验证?
- ClientKeyExchange 消息是否被成功解密?
- Finished 消息中的 verify_data 是否匹配?