TLS/SSL 基础概念与工作原理
从 HTTP 到 HTTPS,我们到底在怕什么?
说实话,十年前我第一次接触网络编程,觉得 HTTP 挺好的——简单,直接,跑得快。直到有一次公司做内部工具,我在公网上传用户登录数据,抓包一看,用户名密码全是明文。那一刻我才意识到:不对,这玩意儿谁都看得见。
你想想看,未加密的 HTTP 就像在明信片上写密码。每个路由节点、每个 WiFi 热点都能抄走内容。我们真正要防的,是三类威胁:
- 窃听 —— 第三方截获通信内容,比如抓包看你的银行卡号
- 篡改 —— 中间人往响应里塞恶意脚本,你浏览器还一脸无辜
- 冒充 —— 钓鱼网站伪装成银行,你根本分不清真假
而 TLS/SSL 就是解决这些问题的标准方案。它工作在传输层之上、应用层之下,给 TCP 连接加了一层「加密隧道」。我在项目中见过太多人以为 HTTPS 只是「HTTP + 证书」,其实它背后的握手协商机制才是精髓。
TLS/SSL 是什么?
SSL 是 Netscape 在 90 年代搞出来的,后来标准化成了 TLS。目前主流是 TLS 1.2 和 TLS 1.3。我个人习惯把 TLS 看作两个阶段:
- 握手阶段 —— 双方确认协议版本、选择加密套件、交换密钥、验证身份
- 记录阶段 —— 用协商好的对称密钥,加密所有应用数据
说白了,非对称加密只在握手时用,握手完就换成对称加密。为什么?非对称太慢了,你拿 RSA 加密几百 K 的数据包,CPU 直接冒烟。
我建议你记住一个核心原则:握手的终极目标是让双方安全地共享一个对称密钥。这个密钥叫「预主密钥」(Pre-Master Secret),后续会派生出会话密钥。
小提示:TLS 1.3 把握手从 2-RTT 砍到了 1-RTT,甚至 0-RTT(有风险)。如果你从零接入,优先考虑 TLS 1.3。
TLS 握手流程详解
我们先看经典 TLS 1.2 完整握手。我会用实际项目里踩过的坑来讲解每一步。
| 步骤 | 方向 | 消息 | 我的经验 |
|---|---|---|---|
| 1 | Client→Server | ClientHello | 版本、密码套件列表、随机数 |
| 2 | Server→Client | ServerHello | 选定版本+套件,发服务器随机数 |
| 3 | Server→Client | Certificate | 服务器证书(含公钥) |
| 4 | Server→Client | ServerHelloDone | 到此为止,该你出牌了 |
| 5 | Client→Server | ClientKeyExchange | 用服务器公钥加密的预主密钥 |
| 6 | Both | ChangeCipherSpec | 告诉对方:我要切换加密了 |
| 7 | Both | Finished | 用会话密钥加密握手消息的 HMAC,验证完整性 |
我曾经调试一个「连接卡死」的 bug,抓包发现服务器发完 Certificate 后客户端没响应。查了两天,原因是服务器证书链中间 CA 没配全,客户端验证失败直接丢掉了连接。嗯,证书链千万别漏。
警告:预主密钥加密用的公钥长度有限制。如果你用 RSA 2048 位公钥,最多只能加密 256 字节的数据,且不能大于模长。所以握手消息里放的是「对称密钥种子」,不是真正的密钥。
握手完成后,客户端和服务器各自计算会话密钥:
master_secret = PRF(pre_master_secret, "master secret",
ClientHello.random + ServerHello.random)
key_block = PRF(master_secret, "key expansion",
ServerHello.random + ClientHello.random)
PRF 是伪随机函数,在 TLS 1.2 里用 SHA-256 做 HMAC。这里要注意,两个随机数必须都参与,否则如果有人能预测其中一个,就可以伪造密钥。
加密套件与安全参数
加密套件就是握手时双方选的那个「组合包」。TLS 1.2 的套件名字很长,例如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。拆开看就四个部分:
- 密钥交换算法 —— ECDHE(临时椭圆曲线 Diffie-Hellman),支持前向安全性
- 身份验证算法 —— RSA(用 RSA 签名验证证书)
- 对称加密算法+模式 —— AES 128 GCM(认证加密,AEAD)
- HMAC 哈希算法 —— SHA-256(用于 Finished 校验和 PRF)
我强烈建议你在 SDK 里禁用所有不带 PFS(前向安全)的套件,比如纯 RSA 密钥交换。为什么?如果服务器私钥泄露,攻击者可以解密之前所有录制的流量。用 DHE 或 ECDHE,每次握手生成临时密钥对,私钥丢了你也没法解历史数据。
另一个坑是 GCM 与 CBC 的选择。TLS 1.2 里 CBC 模式曾被曝出 Padding Oracle 攻击。我个人习惯直接砍掉 CBC,只用 GCM 或 ChaCha20。
证书体系与信任链
证书就是把公钥绑定到实体身份的东西。你不信服务器自己报名字,得有个大家公认的「介绍人」——CA(证书颁发机构)。流程大致是:
- CA 用自己的根证书私钥签发中间 CA 证书
- 中间 CA 签发你的服务器证书
- 客户端收到证书链,从叶子证书往上验证,直到信任的根证书
我记得有个同事图省事,直接把自签名证书丢到线上。结果 iOS 客户端死活连不上,因为系统根信任列表里没那个根证书。最后只好走 MDM 推送安装根证书,折腾了一周。
核心要点:证书验证必须检查四点——有效期、域名匹配、吊销状态(OCSP/CRL)、整条链的签名。缺一项都不安全。
这张图里每个箭头对应一次消息交互。你可能会问:怎么证书验证在握手里没画出来?其实客户端收到 Certificate 后会立即做链校验,不过它不产生新消息。如果验证失败,客户端直接发 Alert 并断开,不会有下一步。
最后提一句,TLS 1.3 精简了很多步骤,把 ServerHello 和 Certificate 合并,而且砍掉了静态 RSA 密钥交换。如果你的 SDK 支持,直接上 TLS 1.3,减少握手延迟。