TLS:HTTPS 背后的加密握手
本文是《计算机网络学习笔记》系列的第五篇。有了 TCP,数据可以可靠地送达;但 TCP 是明文的,任何中间节点都能看到数据的内容,也能篡改它。TLS 正是为了解决这个问题而生——它架在 TCP 之上,为应用层提供加密、认证、完整性三重保障,是 HTTPS、SMTPS、FTPS 等所有安全协议的共同基石。
一、TLS 的身世:从 SSL 到 TLS 1.3
| 版本 | 年份 | 说明 |
|---|---|---|
| SSL 1.0 | 1994 | NetScape 内部设计,从未公开发布 |
| SSL 2.0 | 1995 | 第一个公开版本,存在严重安全漏洞 |
| SSL 3.0 | 1996 | 重新设计,但后来发现 POODLE 漏洞 |
| TLS 1.0 | 1999 | IETF 接手,SSL 3.0 的标准化继承者 |
| TLS 1.1 | 2006 | 修补若干漏洞 |
| TLS 1.2 | 2008 | 目前仍广泛使用,引入了更强的加密套件 |
| TLS 1.3 | 2018 | 当前最新版,握手速度更快,安全性更强 |
名字从 SSL 改成 TLS,不只是换个马甲——TLS 1.0 与 SSL 3.0 并不兼容,是一次实质性的重新设计。
TLS 在网络协议栈中的位置:技术上它运行在 TCP 之上、应用层之下,是对 TCP 的加密增强。从开发者角度看,把它理解为"加密版 TCP"最直观——用 TLS 替换 TCP 编程,多不了几个 API 调用,但数据从此在空中加密传输。
二、TLS 解决哪三个问题?
TLS 提供的三重保障,缺一不可:
| 保障 | 含义 | 对应机制 |
|---|---|---|
| 加密(Confidentiality) | 中间人看不到明文内容 | 对称加密(AES 等)+ 协商的会话密钥 |
| 认证(Authentication) | 确认服务器是你想连的那个,不是冒充的 | 数字证书 + CA 信任链 |
| 完整性(Integrity) | 数据在传输中没有被篡改 | HMAC 消息认证码 |
三者缺少任何一个都不安全:只加密不认证,可能被中间人换掉公钥(MITM 攻击);只认证不加密,明文照样被窃听;只加密不验完整性,密文可以被翻转(Bit-flipping 攻击)。
三、TLS 1.2 握手:四轮对话建立安全信道
TLS 握手发生在 TCP 三次握手完成之后。此时 TCP 连接已通,TLS 在这条明文 TCP 连接上再协商出一套加密参数。
客户端 (Client) 服务端 (Server)
| [TCP 三次握手,已完成] |
| |
| ① ClientHello |
| ——————————————————————————————————————> |
| |
| ② ServerHello + Certificate |
| + ServerHelloDone |
| <—————————————————————————————————————— |
| |
| ③ ClientKeyExchange |
| + ChangeCipherSpec |
| + Finished |
| ——————————————————————————————————————> |
| |
| ④ ChangeCipherSpec + Finished |
| <—————————————————————————————————————— |
| |
| [加密的应用层数据传输开始] |第一步:ClientHello
客户端主动发起,告诉服务端自己的"底牌":
- TLS 版本号:声明自己支持的最高版本(如 TLS 1.2);
- 客户端随机数(Client Random):32 字节的随机数,后续用于派生会话密钥;
加密套件列表(Cipher Suites):客户端支持的加密算法组合,由强到弱排列,例如:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA256 ...- SNI(Server Name Indication):扩展字段,明文告知服务器"我想访问的域名是
example.com"。这让一台服务器(一个 IP)可以托管多个 HTTPS 网站,根据 SNI 选择对应网站的证书发给客户端。
第二步:ServerHello + Certificate + ServerHelloDone
服务端收到 ClientHello 后,回复一系列消息:
ServerHello:
- 确认 TLS 版本:从客户端支持的版本中选一个,如 TLS 1.2;
- 服务端随机数(Server Random):同样是 32 字节,后续派生密钥用;
- 确认加密套件:从客户端的列表里选一个双方都支持的,如
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
Certificate:发送服务器的数字证书,证书内含:
- 服务器的公钥;
- 服务器的身份信息(域名、组织、有效期等);
- CA 的数字签名(证明这张证书是权威机构认证过的)。
ServerHelloDone:告知客户端"我的初始信息发完了"。
第三步:验证证书 + 密钥交换 + 切换加密
这是握手中最关键、最复杂的一步。
3a. 验证证书
客户端拿到证书后,必须验证它的真实性(否则中间人随便伪造一张证书就能欺骗你):
- 验证签名:用内置的 CA 根证书(浏览器/操作系统预装)验证证书上 CA 的签名是否合法;
- 验证域名:证书里的域名是否与自己访问的域名一致(防止 A 网站的证书被用来冒充 B 网站);
- 验证有效期:证书是否在有效期内;
- 验证吊销状态:证书是否被 CA 吊销(通过 CRL 或 OCSP 查询)。
全部通过,从证书里提取服务器的公钥。任何一项失败,浏览器弹出"您的连接不是私密连接"警告。
3b. 生成预主密钥并加密发送
客户端生成一个随机的预主密钥(Pre-Master Secret,PMS),用服务端的公钥加密后发送。只有持有对应私钥的服务端能解密——这保证了 PMS 只有双方知道。
此消息称为 ClientKeyExchange。
3c. 双方各自派生会话密钥
此时双方手里都有三样东西:
Client Random + Server Random + Pre-Master Secret
↓
PRF(伪随机函数)
↓
派生出 4 个密钥:
├── 客户端写入密钥 (Client → Server 方向的加密密钥)
├── 服务端写入密钥 (Server → Client 方向的加密密钥)
├── 客户端 MAC 密钥 (Client → Server 方向的完整性校验密钥)
└── 服务端 MAC 密钥 (Server → Client 方向的完整性校验密钥)为什么要两个随机数 + PMS 混合,而不是只用 PMS 一个?
因为仅靠 PMS 派生密钥,如果 PMS 本身不够随机(或者被攻击者预测),安全性就全靠它撑着。加入两个由双方各自独立生成的随机数,即使某一方的随机数生成器被攻破,另一方的随机数也能保住最终密钥的随机性。这叫熵混合,是密码学中的常见设计。
3d. ChangeCipherSpec + Finished
客户端发送:
- ChangeCipherSpec:宣告"从现在起,我发的包全部加密";
- Finished:包含从 ClientHello 到此刻所有握手消息的摘要(用新密钥加密),让服务端验证:密钥是对的,且握手过程未被篡改。
第四步:服务端 ChangeCipherSpec + Finished
服务端验证客户端的 Finished 通过后:
- 发送 ChangeCipherSpec;
- 发送 Finished(同样是握手消息摘要的加密版)。
客户端验证通过。
至此,TLS 握手完成。 后续所有通信——HTTP 请求、响应——都在这个加密信道里流动,外界只能看到一堆密文。
四、TLS 协议格式
TLS 有自己的记录层(Record Layer)协议,贯穿握手和数据传输的全程,不只是传数据时才用。
+--------+----------+--------+---~~~~~~~~~~~~~~~~---+--------+
| type | version | length | payload | HMAC |
| 1 byte | 2 bytes | 2 bytes| (variable) |variable|
+--------+----------+--------+---~~~~~~~~~~~~~~~~---+--------+| 字段 | 大小 | 说明 |
|---|---|---|
| 类型(Type) | 1 字节 | 0x14 = ChangeCipherSpec;0x15 = Alert;0x16 = Handshake;0x17 = Application Data |
| 版本(Version) | 2 字节 | 0x0301 = TLS 1.0;0x0303 = TLS 1.2(历史原因,TLS 1.3 的报文此处也填 0x0303) |
| 长度(Length) | 2 字节 | 后续 payload + HMAC 的总长度,单条记录最大 16KB |
| 数据(Payload) | 变长 | 加密后的载荷(明文长度 ≤ 16KB) |
| HMAC | 变长 | 用 MAC 密钥对"数据"计算的哈希,防止数据被篡改 |
一条 TLS 记录的封装流程:
应用层数据(可能很长)
↓ ① 分片(每片 ≤ 16KB)
片段
↓ ② 计算 HMAC,附在片段末尾
片段 + HMAC
↓ ③ 用加密密钥整体加密 → 密文
密文
↓ ④ 加拼 5 字节明文头部(type + version + length)
TLS 记录(可以安全传输)注意 HMAC 是在加密之前计算、之后一起加密的,这保证了"先认证、后加密"(MAC-then-Encrypt)的完整性保护。(TLS 1.3 改为了 AEAD 模式,加密和认证同步完成,更安全。)
关闭连接:将类型字段设为 Alert(0x15),发送一个 close_notify 警告,优雅地终止 TLS 会话,然后再由 TCP 四次挥手关闭底层连接。
五、数字证书与信任链
证书是什么?
数字证书相当于互联网上的身份证:它证明"这个公钥,确实属于 example.com,不是别人冒充的"。证书由证书颁发机构(CA, Certificate Authority) 签发,CA 相当于派出所。
信任传递逻辑:
浏览器/操作系统 → 内置信任 → 根 CA
根 CA → 签发信任 → 中间 CA
中间 CA → 签发 → 服务器证书(example.com)只要你信任根 CA,根 CA 信任中间 CA,中间 CA 信任 example.com 的证书,你就信任 example.com。这就是 证书信任链(Chain of Trust)。
常见的 CA 机构:
- DigiCert:商业 CA,企业常用
- Let's Encrypt:免费、自动化,个人和开发者首选
- GlobalSign:老牌商业 CA
申请证书的流程
你 CA
│ │
│ ① 生成密钥对(公钥 + 私钥) │
│ ② 生成 CSR(证书签名请求) │
│ CSR 包含:你的公钥 + 域名信息 │
│ ③ 提交 CSR ——————————————————> │
│ │ 验证你对该域名的控制权
│ │ (在域名的 DNS/服务器上放一个验证文件)
│ ④ 收到签名后的数字证书 <———————— │
│ (CA 用自己的私钥签了你的 CSR)│
│ ⑤ 把证书部署到服务器上 │Let's Encrypt + certbot 实战
Let's Encrypt 是目前最流行的免费 CA,推荐用 certbot 来自动化申请和续期:
# 安装 certbot(以 Nginx 为例)
apt-get install certbot
apt-get install python3-certbot-nginx
# 申请证书(会自动修改 Nginx 配置)
certbot --nginx -d example.com -d www.example.com
# 按提示输入邮箱、同意服务条款即可
# 设置自动续期(证书有效期 90 天,建议每月自动续一次)
crontab -e
# 添加以下行:每月 1 日 00:00 自动续期
0 0 1 * * /usr/bin/certbot renew --quietcertbot 会自动完成域名验证、下载证书、配置 Nginx 的全流程,几乎是零门槛。
六、进阶:TLS 1.3 的改进
TLS 1.3(RFC 8446,2018 年)相比 1.2 有几处重要的改进:
握手速度:从 2-RTT 降到 1-RTT
TLS 1.2 的握手需要2 个来回(2-RTT)才能完成,加上 TCP 握手的 1-RTT,一共要 3 个往返才能开始发数据。
TLS 1.3 把握手压缩到了 1-RTT:
TLS 1.3 的握手:
Client ——→ ClientHello(含密钥共享参数)
Server ——→ ServerHello + 证书 + Finished(合并为一次发送)
Client ——→ Finished
← 完成!比 1.2 少一个往返 →此外 TLS 1.3 还支持 0-RTT 恢复(Session Resumption):对于之前访问过的服务器,客户端可以在第一个握手包里直接附上应用层数据,以极低的延迟恢复会话(有一定的重放攻击风险,需谨慎使用)。
废除了不安全的加密套件
TLS 1.3 移除了大量老旧的加密算法,只保留少数被认为安全的:
- 废除 RSA 密钥交换(不支持前向保密);
- 废除 RC4、DES、3DES、MD5、SHA-1 等脆弱算法;
- 只允许使用支持前向保密(Forward Secrecy)的密钥交换算法(ECDHE 等)。
前向保密(Forward Secrecy):即使服务端的私钥未来泄露,攻击者也无法解密历史流量。因为每次 TLS 连接使用临时生成的密钥,私钥只用于身份认证,不参与密钥派生。
七、实战:用 Wireshark 抓出解密后的 HTTPS 包
默认情况下,抓包工具只能看到加密密文。通过以下配置,可以让 Chrome 和 Wireshark 配合,看到解密后的明文 HTTP 内容:
原理:Chrome 可以把每次连接的会话密钥记录到一个日志文件,Wireshark 读取这个文件后,就能用密钥解密对应的 TLS 流量。
步骤:
- 新建密钥日志文件,例如
C:\sslkey.log(内容为空即可); 设置环境变量
SSLKEYLOGFILE,指向该文件:# Windows(永久设置,需重启生效) setx SSLKEYLOGFILE "C:\sslkey.log" # Linux / macOS(临时,在同一终端启动 Chrome) export SSLKEYLOGFILE=~/sslkey.log- 重启 Chrome 浏览器(必须在设置环境变量之后启动的 Chrome 才会记录密钥);
- 配置 Wireshark:
编辑→首选项→Protocols→TLS→(Pre)-Master-Secret log filename,选中刚才的sslkey.log; - 开始抓包,访问任意 HTTPS 网站(如
https://v2ex.com); - 在 Wireshark 过滤框输入
http2,即可看到解密后的 HTTP/2 请求和响应明文。
这是学习 HTTP/2、调试 HTTPS 问题时极其有用的技巧。
总结
| 阶段 | 目的 | 关键动作 |
|---|---|---|
| ClientHello | 声明己方能力 | 发送随机数、TLS 版本、加密套件列表、SNI |
| ServerHello + 证书 | 服务端应答 + 身份证明 | 确认版本和套件、发送证书(公钥+签名) |
| 证书验证 + 密钥交换 | 认证服务端真实性 + 安全传递 PMS | 验证信任链 → 公钥加密 PMS → 双方派生 4 个会话密钥 |
| ChangeCipherSpec + Finished | 切换加密 + 验证握手完整性 | 双方均加密 Finished 消息,互相验证密钥和握手未被篡改 |
| 应用层数据传输 | 加密通信 | 所有数据用会话密钥加密,HMAC 保护完整性 |
TLS 的设计哲学和 TCP 一脉相承:用多出来的几次来回(握手的代价),换取整个通信过程的安全性。理解了 TLS,HTTPS 就不再是一个神秘的"小锁头",而是一套有清晰步骤、可以在 Wireshark 里逐帧验证的具体协议。
本系列前四篇:
· 第一篇:《TCP 协议格式详解》
· 第二篇:《TCP 三次握手与四次挥手》
· 第三篇:《TCP 可靠数据传输》
· 第四篇:《TCP 流量控制与拥塞控制》
参考资料:《计算机网络:自顶向下方法》