SSL/TLS 协议概述与握手过程
SSL(Secure Sockets Layer)和 TLS(Transport Layer Security),说白了就是给网络通信加一把锁。你想想看,如果没有它们,你在网页上输入的密码、信用卡号,就相当于在大街上裸奔。我做全站 HTTPS 迁移那几年,见过太多团队只关注证书申请,却对协议本身一知半解,结果线上出了怪问题排到天亮。所以这一章,咱们把协议底层掰开揉碎聊一聊。
1. 从 SSL 到 TLS:一次不太彻底的改名
SSL 最早是 Netscape 搞出来的,版本从 1.0、2.0 到 3.0。后来 IETF 接手,觉得“SSL”这名字不够国际化(笑),就改叫 TLS 1.0——其实它和 SSL 3.0 差别不大。我记得刚入行时,老同事还管 TLS 叫 SSL,文档里也混着写。直到 TLS 1.2 才真正站稳脚跟,1.3 更是把性能和质量拉高了一个台阶。
| 版本 | 发布年份 | 现状 | 个人建议 |
|---|---|---|---|
| SSL 2.0 / 3.0 | 1995 / 1996 | 已废弃,存在已知漏洞 | 千万别用,哪怕兼容老设备也不值 |
| TLS 1.0 | 1999 | 现已弃用(PCI DSS 要求禁用) | 能关就关,我 2019 年帮客户审计时还抓到过 |
| TLS 1.1 | 2006 | 逐步淘汰 | 大部分业务可以跳过 |
| TLS 1.2 | 2008 | 当前主流 | 稳定、兼容性好,推荐主力部署 |
| TLS 1.3 | 2018 | 性能更优、更安全 | 能上就上,我目前新项目默认只开 1.2+1.3 |
2. 握手过程:四个阶段一步都不能错
握手是 SSL/TLS 最核心的部分。你把它理解成两个人见面先互相验明正身、商量暗号。我当年排错最头疼的就是握手失败——明明证书配好了,为什么还是握手失败?后面逐一拆解。
- ClientHello —— 客户端发个招呼:我支持哪些 TLS 版本、哪些加密套件、以及一个随机数。
- ServerHello —— 服务器从清单里挑一个版本、一个套件,也扔回一个随机数。
- 证书与密钥交换 —— 服务器把证书(含公钥)发过来,必要时还发一个密钥交换参数(比如 DH 临时公钥)。
- 握手完成与对称密钥确认 —— 双方用之前的随机数算出主密钥,然后互发一条“Finished”消息确认。之后所有数据都用这个对称密钥加密。
重点记住:前两步是明文,从第三步开始才有加密保护。所以中间人可以偷听证书内容,但伪造不了签名。
画个图看起来更清楚。下面这个流程图基本覆盖了 TLS 1.2 完整的握手过程——每次我给团队培训都拿它当提纲。
3. TLS 1.3 握手:少握手,快连接
TLS 1.3 最大的变化就是砍掉了不少冗余步骤。我刚开始用 1.3 时也有点不适应,但跑过压测后是真香——尤其对移动端用户,首屏加载时间直接少了近 200ms。
- 1-RTT 握手:把 ServerHello 和证书、密钥交换打包到一条消息里,客户端收到就能算密钥。
- 0-RTT 会话恢复:如果之前连过,可以直接带上加密数据(但有大坑,容易遭重放攻击,我在生产环境默认关了它)。
- 强制完美前向安全性:所有密钥交换必须用 DH(Diffie-Hellman),不再允许 RSA 密钥传输。
- 更短的加密套件列表:只支持 AEAD 套件(AES-GCM、ChaCha20-Poly1305),不带 HMAC 单独验证。
⚠️ 避坑: TLS 1.3 虽然快,但很多老 Nginx 版本、CDN 节点对 0-RTT 支持有 bug。我曾遇到用户在第一次握手后数据错乱,查了两天才定位到是 0-RTT 缓存撞了。
4. 密钥交换算法:到底是 RSA 还是 DH?
密钥交换决定了如何安全地生成双方都知道的那个“对称密钥”。我个人习惯把市面上常见的几种说清楚:
| 算法 | 原理 | 是否支持 PFS | 我踩过的坑 |
|---|---|---|---|
| RSA Key Transport | 客户端用服务器公钥加密预主密钥,服务器私钥解密 | ❌ 不支持(私钥泄露则历史会话全暴露) | 以前有个合规项目,客户要求必须关掉所有基于 RSA 的密钥交换 |
| DHE / ECDHE | 双方各生成临时 DH 参数,签名后交换 | ✅ 支持(每次握手密钥不同) | ECDHE 性能比 DHE 好很多,我一般直接上 X25519 |
| PSK (用于 TLS 1.3) | 预共享密钥(常见于 0-RTT) | 取决于 PSK 是什么 | 不要用固定 PSK,定期轮换 |
个人经验:我现在所有项目默认只开 ECDHE + TLS 1.2/1.3。RSA 密钥传输早该淘汰了——你想想看,如果有一天服务器私钥被拖了,之前所有加密的流量都能被解密,那场景简直不敢想。
5. 如何快速排查握手问题
拿到一个 HTTPS 连接异常,别急着翻日志。我习惯用一条命令先摸清情况:
openssl s_client -connect example.com:443 -tls1_2 -debug 2>&1 | less
输出里重点看这几行:
CONNECTED(00000003)—— 表示 TCP 连接成功Certificate chain—— 证书链有没有不完整Server Temp Key—— 确认是否用了 ECDHE 以及曲线类型Verify return code—— 如果不是 0,说明证书校验没通过
小技巧:如果想看握手每一步的报文,加上 -msg 参数。之前排查一个 CDN 缓存导致的握手混乱,就是靠抓报文里 ClientHello 的 Session ID 字段对比出来的。
6. 握手过程的常见失败场景
我在项目中遇到过不下十种握手失败的情况,挑最有代表性的三个说说:
- 证书链不完整 —— 浏览器报“NET::ERR_CERT_AUTHORITY_INVALID”。服务器只发了叶子证书,没发中间 CA。解决办法:把中间证书和根证书(如果有)拼进证书文件。
- Cipher Suite 不匹配 —— 客户端要用 AES-GCM,服务端只支持 AES-CBC。客户端直接报“handshake failure”。建议用
openssl ciphers -v检查双方支持列表。 - SNI 缺失 —— 单 IP 多域名时,服务器不知道你要访问哪个站点,可能会返回默认证书导致主机名不匹配。老版本 JDK、Python requests 库曾有这个问题,升级即可。
⚠️ 曾经有个教训:我帮一家电商平台做全站 HTTPS 迁移,上线后部分老年用户手机 APP 连不上。查了半天才发现他们 APP 用的 HttpClient 仅支持 TLS 1.0,服务端却只开了 1.2。最后妥协做法是保留 TLS 1.2 为主,额外开了 TLS 1.1 作为 fallback,同时限时通知用户升级 SDK。
小结
SSL/TLS 协议看上去复杂,核心就三件事:身份验证(证书)、密钥协商(握手)、数据加密(对称密钥)。握手里面的消息顺序、加密套件选择、密钥交换算法都是迁移时必须摸清的。这一章讲的是最通用的 TLS 1.2 握手和 1.3 的改进——后面几章我们会把证书管理和配置细节一一展开。你把这些基础打牢,以后遇到 HTTPS 相关的 glitch 心里就有底了。