TLS握手协议详解:ClientHello, ServerHello, 证书验证, 密钥交换
要说TLS最核心的部分,那一定是握手。你想想看,握手之前两端什么都不知道——没有密钥、没交换过参数、你想传输数据都不知道怎么加密。所以刚开始交换的那些报文,直接决定了后续会话的安全性。我经历过很多奇怪的问题,最后都回到握手这一步来找根源。真的,搞定握手,你就懂了一半TLS。
ClientHello:客户端的第一声招呼
客户端发起连接后,首先发一条ClientHello。这条消息里它告诉服务器自己支持什么版本、什么密码套件、还有哪些扩展。我当年第一次用Wireshark抓握手包,看到ClientHello里长长一串列表,当时就懵了。但后来发现,其实这里面最重要的字段就几个。
- TLS版本:客户端最高支持的版本。比如TLS 1.2或1.3。
- Cipher Suites:按优先级排序的密码套件列表。我建议最少提供10个以上,否则服务端可能选到你不想要的弱套件。
- Random:32字节随机数,用于后续密钥衍生。注意不是随意填的,必须真随机。
- Session ID / Session Ticket:用于会话复用。这个后面章节会细讲。
- Extensions:比如SNI、ALPN、支持的椭圆曲线等。嗯,这里要注意——现在很多网站不支持SNI的客户端直接拒绝。
ServerHello与Certificate:服务端的回应
服务端收到ClientHello后,会从客户端支持的列表里挑一个版本和一套密码套件,然后发回ServerHello。同时,如果要求双向认证(很少见),服务器也会请求客户端证书。但最常见的场景是服务端带上自己的证书链一起发给客户端。
这里有一个很多人容易忽略的细节:ServerHello中的Random必须独立生成,不能和客户端一样。我遇到过甲方工程师直接复制客户端的Random过来,结果算出来的密钥两边不一致——握手直接断开。
证书验证:客户端的信任决策
客户端收到证书后,会按以下步骤验证:
- 检查有效期:是不是在有效期内?不在?直接拒绝。
- 验证签名:用CA的公钥验签。
- 检查吊销状态:通过CRL或OCSP。这里是个性能痛点,OCSP stapling能解决,后面章节讲。
- 域名匹配:证书里的CN或subjectAltName必须匹配客户端访问的域名。
我当初实习的时候,有个同事把证书挂到了错误的域名上,结果花了三小时才发现。从那以后我每次部署证书前都会手打一遍 openssl x509 -in cert.pem -text -noout 看域名字段。
密钥交换:协商最终的对称密钥
证书验证通过后,开始真正的密钥交换。具体过程取决于ServerHello里选中的密码套件。我整理了一个常用对比表:
| 密码套件类型 | 密钥交换方式 | 前向安全性 | 常见场景 |
|---|---|---|---|
| RSA密钥交换 | 客户端生成pre-master secret,用服务器公钥加密传输 | ❌ 无 | 老旧系统(不建议使用) |
| DHE_RSA | 服务端生成DH参数,用RSA签名;客户端生成自己的DH值 | ✅ 有 | 兼容性优先的现代场景 |
| ECDHE_RSA/ECDSA | 类似DHE但使用椭圆曲线,性能更好 | ✅ 有 | 主流推荐(我基本只用这个) |
说白了,现在的握手我几乎只见ECDHE。原因很简单:前向安全、密钥协商速度还快。我记得有一次给一个金融客户调优,把RSA换成了ECDHE_X25519,握手时间降了30%。
最后一步:双方用pre-master secret和两个Random通过PRF(伪随机函数)计算出主密钥(master secret)。然后各自生成加密密钥、MAC密钥等。完成后发送ChangeCipherSpec,并验证Finished消息。如果Finished里的校验值能对上,握手成功。否则,哪个环节错了,连接就断。
核心流程一览
下面这张SVG图展示了TLS 1.2完整握手的主要消息流(省略了重协商等复杂情况)。你可以对照着理解每一步在做什么。
你看,整个握手本质上就是客户端和服务端互相确认身份、协商参数、生成共享密钥的过程。每一步都有自己的使命,缺一不可。理解这些细节,对你后续做优化会非常有帮助——比如你知道哪个环节最耗时,就可以针对它下手。