2. TLS 握手流程:ClientHello 到证书验证
说实话,TLS 握手这部分很多工程师一上来就被绕晕了。但它的核心逻辑其实不复杂——就是「双方商量好算法、交换密钥、验明身份」。我在做性能优化时发现,不少连接慢的问题就出在握手阶段。所以这一章,我带你拆解完整握手的每一步。
2.1 完整握手全景
先看一张全貌图,心里有个底。我把握手分成了四个阶段,后面逐一说明。
嗯,这张图里箭头上的文字基本就是握手的骨架。下面我按顺序讲细节。
2.2 ClientHello – 客户端亮出底牌
客户端先发个包,告诉服务端「我能用什么算法、支持什么版本、这是我的随机数」。别看只是一条消息,里面塞了不少东西:
- 协议版本:比如 TLS 1.2 或 1.3,如果不匹配就会协商降级。
- 随机数 (Client Random):32 字节,后面生成会话密钥要用。
- 密码套件列表:一堆 cipher suite,比如
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384。 - 扩展 (Extensions):SNI、ALPN、supported_groups 等等。这些坑不少,后面再细说。
2.3 ServerHello 与证书下发
服务端从客户端给的列表里挑一个,然后发回自己的选择。同时带上证书链——这就是身份证明。
ServerHello 里有什么?
- 选中的密码套件
- 服务端的随机数 (Server Random)
- 会话 ID(如果做 session resumption)
证书 (Certificate)
证书一般不是单张,而是一条链:从服务器证书到中间 CA,再到根 CA。客户端拿到后会逐级验证签名、有效期、是否被撤销。
这里有个常见坑:证书链不完整。我见过不少生产环境只发了服务器自己那张,中间证书没带,结果手机端验证失败。你说为什么?因为很多系统默认不信任中间 CA。
SSLCertificateChainFile 或 ssl_trusted_certificate,别漏了。
2.4 密钥交换 – 双方各自算出主密钥
这一步是握手最核心的环节。双方要协商出一个只有他俩知道的对称密钥。
两种主流密钥交换方式
| 方式 | 如何工作 | 是否前向安全 |
|---|---|---|
| RSA 密钥交换 | 客户端用服务端公钥加密 premaster_secret,服务端用私钥解密 |
❌ 不提供前向安全 |
| (EC)DHE 密钥交换 | 双方交换临时 DH 公钥,各自计算共享秘密 | ✅ 提供前向安全 |
注意,现在大部分场景用的都是 ECDHE。RSA key exchange 在 TLS 1.2 里已经被标记为「不建议使用」了。你想想看,如果私钥泄露,那么以往所有用那次公钥加密的会话都能被破解,多吓人。
我曾在某个银行项目中遇到过老系统只支持 RSA 交换,硬扛了几个月才迁到 ECDHE。那段时间每次做安全审计都提心吊胆。
具体步骤(以 ECDHE 为例)
- 服务端在 ServerHello 之后发
ServerKeyExchange消息,里面包含椭圆曲线参数、临时 ECDHE 公钥,以及用证书私钥做的签名。 - 客户端收到后验证签名,然后用服务端临时公钥和自己的临时私钥生成
premaster_secret。 - 双方再用
premaster_secret+ 两个随机数计算主密钥 (Master Secret) =PRF(pre_master_secret, "master secret", ClientRandom + ServerRandom)[0..47]。 - 最后从这个主密钥衍生出各种工作密钥(加密密钥、MAC 密钥、IV 等)。
关键理解: premaster_secret 只有客户端和服务端知道,即使中间人拿到证书也没法算出这个秘密。这就是 ECDHE 前向安全的根源。
2.5 证书验证 – 客户端检查服务端身份
证书验证不是服务端做的事,它完全由客户端负责。收到服务端证书链后,客户端会做以下检查:
- 数字签名验证:用上级 CA 的公钥验证证书签名。
- 有效期:当前时间是否在证书的 valid from – valid to 之间。
- 域名匹配:证书的 Common Name (CN) 或 Subject Alternative Name (SAN) 是否与访问的主机名一致。
- 证书吊销状态:通过 OCSP 或 CRL 检查是否已被吊销。
- 信任链:最终要能追溯到客户端信任的根证书。
这里经常出问题的是域名匹配。举个例子,有些人把多域名塞在 SAN 里,但客户端严格匹配的时候却把二级域名当一级域名用了,哇,直接报警。我的建议是:做证书前先用 openssl x509 -text 查一下 SAN 字段。
2.6 收尾 – ChangeCipherSpec 与 Finished
密钥算出来之后,双方都需要告诉对方「我要开始用新密钥加密了」。这个标记消息叫 ChangeCipherSpec(注意,它严格来说不是 TLS 握手协议的一部分,是另一个 Content Type)。之后紧跟着一条 Finished 消息,里面是对整个握手消息的 HMAC 校验值,用于验证握手过程没有被篡改。
嗯,这里有一个容易忽视的点:Finished 消息本身也是用新密钥加密的。所以如果解密失败,说明密钥交换出错了,那就直接断连。我在调试时经常抓包看 Finished 消息的对齐情况,一旦不匹配就说明 Premaster Secret 两边的计算结果不一样。
2.7 小结 – 你该记住什么
- 完整握手需要 2 轮 RTT(不含 TCP 三次握手)。
- ClientHello 和 ServerHello 协商版本、套件、随机数。
- 证书验证决定了你是否信任这个服务器。
- 现代推荐用 ECDHE 密钥交换,别再用 RSA 交换了。
- ChangeCipherSpec + Finished 保证整个握手没有被中间人动过手脚。