第5章:HTTPS 与 TLS
元数据卡
| 属性 | 值 |
|---|---|
| 难度 | |
| 前置 | HTTP 协议基础(第4章)、对称/非对称加密常识 |
| 核心概念 | TLS 握手、证书链、密钥交换、前向安全性 |
| 实战能力 | 读懂 TLS 1.2/1.3 握手日志、配置证书链、排查常见 TLS 错误 |
你在哪
"但你马上发现一个问题——驿道上有劫匪。你在驿道上传输的法术信函是明文,任何野法师都能偷看。HTTPS 和 TLS 给这条驿道加了一道封印锁——只有发送者和接收者能打开。"
你从第4章的 HTTP 中明白:HTTP 是明文裸奔的传送咒——中间人嗅探、篡改、冒充,三刀全中。
你 → [明文"密令:123456"] → 🔍 谁都能看 → 信标塔你的任务只有一个字:锁。这就是 HTTPS——HTTP + TLS(传送法阵安全),而非 HTTP + SSL。SSL 早已废弃,但工具箱和生态里 SSL 这个名字阴魂不散。
核心事实:HTTPS 不是一种新协议。它是 HTTP over TLS。
TLS 本身不属于应用层——它在传送咒(TCP)和应用层(HTTP)之间插了一层「安全封印咒」:
应用层: HTTP
安全层: TLS
传送层: TCP
驿道层: IP此刻你站在 TCP 连接已经建立、但 TLS 法力握手还没开始的边界上。下一步——加密信道建立。
本章分层
- 必读:HTTPS 比 HTTP 多了什么安全能力(加密/认证/完整性),TLS 握手核心流程(1.2 和 1.3 的 RTT 差异),证书链验证的浏览器信任模型
- 选读:Cipher Suite 的命名规则拆解、ECHDE 和前向安全性的原理、TLS 1.3 的改进
- 深水区:TLS 1.2 完整握手消息顺序、手工验证证书链的编码实现
本章不会要求你掌握
- 手动实现证书链验证逻辑
- OCSP Stapling 的协议细节
- AEAD 加密(GCM/Poly1305)的数学原理
你的任务
- 回答核心问题:HTTPS 比 HTTP 多了什么安全能力?(加密、认证、完整性)
- 理解 TLS 握手的核心流程(1.2 需要几次 RTT,1.3 怎么减少的)
- 掌握证书链验证机制——浏览器怎么知道证书可信,不需要手动实现
- 了解加密套件(Cipher Suite)的命名逻辑和一个典型选择
- 理解前向安全性的价值
- 梳理 TLS 在整个安全蓝图中的完整角色——它解决什么、不解决什么
遭遇战 → 获得技能
技能 1:TLS 1.2 握手 · 六步
TLS 1.2 握手是理解所有现代加密通信的基线。不要跳过它直接学 TLS 1.3——你不知道它简化了什么。
客户端 服务器
│ │
│ 1. ClientHello ───────────────────→│
│ (TLS版本, 加密套件列表, 随机数) │
│ │
│ 2. ←── ServerHello │
│ (选中的版本/套件, 服务器随机数) │
│ │
│ 3. ←── Certificate │
│ (服务器证书链) │
│ │
│ 4. ←── ServerHelloDone │
│ │
│ 5. ClientKeyExchange ────────────→│
│ (用服务器公钥加密的 PreMaster) │
│ │
│ 6. ──── 双方各自计算 MasterSecret │
│ │
│ 7. ChangeCipherSpec ─────────────→│
│ "接下来用对称密钥通信" │
│ │
│ 8. Finished ─────────────────────→│
│ (加密的握手摘要, 验证完整性) │
│ │
│ 9. ←── ChangeCipherSpec │
│ 10. ←── Finished │
│ │
│ ═══════ TLS 隧道建立 ═══════ │高密度要点:
- ClientHello + ServerHello 协商版本和套件——双方各贡献一个随机数,防重放
- Certificate 携带的是证书链,而不是单张证书。浏览器必须验完全链
- PreMasterSecret 用服务器公钥加密——只有服务器能解密(RSA 密钥交换,现已不推荐)
- MasterSecret = PRF(PreMasterSecret + ClientRandom + ServerRandom),三要素缺一不可
- ChangeCipherSpec 不是 TLS 握手消息,是独立的记录层协议——双方各自切换加密上下文
- Finished 包含了之前所有握手消息的 HMAC——任何中间人篡改都会被发现
选读:以下 Cipher Suite / 前向安全性 / TLS 1.3 细节属于加深理解的内容。主线只需知道「Cipher Suite 是加密算法的套餐名」、「TLS 1.3 比 1.2 少了一次往返」即可。
技能 2:加密套件(Cipher Suite)拆解
TLS 1.2 的套件名长得像乱码,但每个字段都是精确的密码学选择:
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ PRF/校验用的哈希
│ │ │ │ │ └─────── 对称加密模式
│ │ │ │ └───────────── 对称加密算法+密钥长度
│ │ │ └───────────────── 证书签名算法
│ │ └───────────────────── 密钥交换算法
│ └─────────────────────────── 密钥交换+证书体系四个功能块,缺一不可:
| 组件 | 作用 | 典型选择 |
|---|---|---|
| 密钥交换 | 安全协商对称密钥 | ECDHE(前向安全) |
| 证书签名 | 服务器的身份验证 | RSA、ECDSA |
| 对称加密 | 实际数据的加解密 | AES-256-GCM |
| HMAC/Hash | 完整性校验 + PRF | SHA-384 |
Python 实战:列出系统支持的套件
import ssl
ctx = ssl.create_default_context()
ciphers = ctx.get_ciphers()
for c in ciphers[:8]:
print(f"{c['name']:45s} v={c['protocol']} bits={c['cipher_bits']}")输出示例:
ECDHE-ECDSA-AES256-GCM-SHA384 v=TLSv1.2 bits=256
ECDHE-RSA-AES256-GCM-SHA384 v=TLSv1.2 bits=256
TLS_AES_256_GCM_SHA384 v=TLSv1.3 bits=256 ← 1.3 格式巨变TLS 1.3 的套件名字短到只剩对称部分——因为密钥交换和签名在握手层独立协商了。
技能 3:证书链验证
一个 HTTP 站点可能出示 3 张证书。浏览器怎么敢信?
主线只要求理解浏览器信任模型:Root CA 内置在操作系统/浏览器中,Intermediate CA 由 Root CA 签名,Leaf 证书由 Intermediate CA 签名。浏览器自动验证整条链,你不需要手动实现。
Root CA(自签名,内置在 OS/浏览器)
│ "我验证了下一级"
▼
Intermediate CA
│ "我验证了站点"
▼
Leaf Certificate(服务器证书——你去 curl 看到的那张)验证顺序(自底向上):
- Leaf 证书的
issuer字段 → 找 Intermediate 证书 - Intermediate 证书的
issuer字段 → 找 Root CA 证书 - Root CA 证书必须在本机信任存储中——自签名所以叫 Root
- 每级用上一级的公钥验证签名
# Python 手动验证证书链的伪逻辑 🏊 **深水区**:以下为手动实现的示意代码,理解原理即可,不要求掌握。
def verify_chain(leaf_cert, intermediates, root_store):
# 从叶子往上验证签名链
cert = leaf_cert
while cert.issuer != cert.subject: # 不是 Root
issuer_cert = find_issuer(cert.issuer, intermediates, root_store)
# 用 issuer 的公钥验证 cert 的签名
verify_signature(cert.signature, issuer_cert.public_key)
cert = issuer_cert
# 最后一级必须是信任的 Root
assert cert in root_store常见陷阱: 中间证书缺失。很多服务器只发 Leaf 证书——浏览器无法构建完整链,显示 NET::ERR_CERT_AUTHORITY_INVALID。
# 查看证书链(两条命令等效)
openssl s_client -connect baidu.com:443 -showcerts
echo | openssl s_client -connect baidu.com:443 2>/dev/null | openssl x509 -textCertificate chain
0 s:CN = baidu.com
i:C = BE, O = GlobalSign nv-sa, CN = GlobalSign RSA OV SSL CA 2018
1 s:C = BE, O = GlobalSign nv-sa, CN = GlobalSign RSA OV SSL CA 2018
i:C = BE, O = GlobalSign nv-sa, CN = GlobalSign Root CAs: 是 Subject(这张证是谁的),i: 是 Issuer(谁签发了它)。第 1 级的 i: 指向 Root CA——它不在证书流里,因为它在系统信任存储里。
选读:理解前向安全性的概念很有价值(主线可以知道「ECDHE 让黑客事后拿到私钥也无法破解以前的通信」)。以下细节可跳过。
技能 4:前向安全性(Forward Secrecy)
传统的 RSA 密钥交换:客户端用服务器公钥加密 PreMasterSecret。攻击者若能事后拿到服务器的私钥,就能解密这个 PreMasterSecret——进而解密之前所有的流量。
ECDHE 解决这个问题:
1. 服务器生成临时密钥对 (dH, QH) — 用完就丢
- dH: 临时私钥(服务器侧)
- QH: 临时公钥(发给客户端)
2. 客户端生成临时密钥对 (dC, QC)
- dC: 临时私钥(客户端侧)
- QC: 临时公钥(发给服务器)
3. 双方计算:shared_secret = ECDH(dH, QC) = ECDH(dC, QH)
数学上等同,但中间人无法算出来
4. 证书只用来**签名**临时公钥——证明这个临时公钥确实来自声称的服务器关键洞察: 临时密钥对从不存储。如果攻击者在 2026 年拿到了服务器在 2024 年的私钥——2024 年的会话密钥是 ECDHE 临时协商的,无法恢复。这就是前向安全性。
不推荐 RSA 密钥交换的理由只有一个:它没有 FS。
技能 5:TLS 1.3 握手 · 一步顶六步
TLS 1.3 将 1.2 的 2-RTT 降至 1-RTT(甚至 0-RTT)。
TLS 1.2: ──C/S── ──S/C── ──KeyEx── ──C/S── ──Fin── ──Fin── = 2 RTT
TLS 1.3: ──C/S(含密钥交换参数)── ──S/C(含完成信号)── = 1 RTT1.3 做了什么手术:
| 改动 | 为什么 |
|---|---|
| 移除 RSA 密钥交换 | 不支持前向安全的算法不配存在 |
| 移除不安全的对称算法 | DES、RC4、3DES、CBC 模式全砍了 |
| 握手消息加密 | ServerHello 之后的所有消息都加密 |
| 0-RTT(会话恢复) | 上次握手的缓存 + PSK,无需额外往返 |
| 简化加密套件 | 密钥交换和签名独立协商,套件只写对称部分 |
1.3 的加密套件只剩 3 个字符变体:
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256现在不需要理解 CHACHA20 是什么。它是在缺少 AES-NI 硬件加速的 ARM/手机芯片上表现更好的流密码。存在即正义,记住它比 AES 在某些场景下更快就够了。
Python 客户端强制要求 TLS 1.3:
import ssl, socket
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.load_verify_locations("/etc/ssl/certs/ca-certificates.crt")
with socket.create_connection(("baidu.com", 443)) as sock:
with ctx.wrap_socket(sock, server_hostname="baidu.com") as tls:
print(f"TLS version: {tls.version()}")
print(f"Cipher: {tls.cipher()}")输出:TLS version: TLSv1.3
技能 6:TLS 在安全中的完整角色
TLS 解决网络安全的三个半问题:
┌──────────────────────────────────────────────┐
│ TLS 解决的三件事 │
│ │
│ ✅ 加密(Encryption)—— 中间人看不到内容 │
│ ✅ 认证(Authentication)—— 你在跟真服务器说话 │
│ ✅ 完整性(Integrity)—— 中途没人改过数据 │
│ │
│ ⚠️ 半个:防重放(TLS 1.3 通过随机数 + │
│ 序列号做了一定程度防护,但完整方案需 │
│ 应用层配合) │
└──────────────────────────────────────────────┘TLS 不解决的事:
- 端到端加密(E2EE)。TLS 只在客户端和服务器之间加密——服务器能看到明文。你需要 Signal/Matrix 那套,不是 TLS
- 应用层身份。证书证明的是域名,不是你面前的用户
- DDoS。恶意的 TLS 握手请求照样能打崩服务器
常见陷阱:用 Python 实现一个迷你 HTTPS 客户端
不依赖 requests/httpx,只用标准库 socket + ssl 完成一次完整的 HTTPS GET。
import ssl
import socket
def https_get(host: str, path: str = "/") -> tuple[int, bytes]:
"""
纯标准库 HTTPS GET,暴露 TLS 握手的每一个关键步骤。
返回: (HTTP 状态码, 响应体)
"""
# 1. 创建 SSL 上下文
ctx = ssl.create_default_context()
# 可选:强制 TLS 1.3
# ctx.minimum_version = ssl.TLSVersion.TLSv1_3
# 2. TCP 连接 → TLS 握手(一次完成)
with socket.create_connection((host, 443), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
# 3. 获取 TLS 握手参数
print(f"Cipher: {tls.cipher()}")
print(f"TLS v: {tls.version()}")
# 4. 获取服务器证书(供调试)
cert = tls.getpeercert()
print(f"Subject: {cert.get('subject', [])}")
print(f"Issuer: {cert.get('issuer', [])}")
# 5. 发送 HTTP 请求(现在通过加密隧道)
request = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"Connection: close\r\n"
f"User-Agent: AtlasDemo/1.0\r\n"
f"\r\n"
)
tls.sendall(request.encode())
# 6. 读取响应
response = b""
while chunk := tls.read(4096):
response += chunk
# 7. 粗解析
header_end = response.find(b"\r\n\r\n")
status_line = response[:response.find(b"\r\n")].decode()
status_code = int(status_line.split()[1])
body = response[header_end + 4:]
return status_code, body
# 执行
code, body = https_get("baidu.com", "/")
print(f"Status: {code}")
print(f"Body ({len(body)} bytes): {body[:200]}")Cipher: ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
TLS v: TLSv1.3
Subject: [[('commonName', 'baidu.com')]]
Issuer: [[('countryName', 'BE'), ('organizationName', 'GlobalSign nv-sa'), ...
Status: 200
Body (11104 bytes): <html>...这段代码暴露了什么:
ctx.wrap_socket()完成从裸 TCP 到加密信道的全套 TLS 握手- HTTP 请求通过
tls.sendall()发送——和普通 socket 写一样简单,但此刻数据已经加密 tls.cipher()返回的是一个三元组(套件名, 协议版本, 密钥位数)- 服务器证书的 Subject/Issuer 和前面
openssl输出一致
通关挑战
基础级 🥉
- 启动
openssl s_client -connect yoursite.com:443 -showcerts,写出该站点的完整证书链(从 Leaf 到 Root) - 识别服务器选择的 TLS 版本和加密套件
进阶级 🥈
- 用 Python 的
ssl.get_server_certificate()下载一个站点的证书,然后用cryptography库解析其 Subject、Issuer、NotBefore、NotAfter、SAN - 构建一个强制使用 TLS 1.2 的 Python HTTPS 客户端,确认加密套件是 ECDHE 前缀的
专家级 🥇
- 编写函数验证本地证书文件(PEM 格式)的链是否完整,指出缺失的中间证书
- 用 Wireshark 捕获一次 HTTPS 访问,找出 ClientHello 中的加密套件列表(至少 5 个)和服务器选择的套件
验收标准
完成本章后你应当能回答以下问题:
- [ ] TLS 1.2 握手需要几次往返(RTT)?TLS 1.3 呢?
- [ ] 说出加密套件
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384的每一段含义 - [ ] 证书链验证中,每张证书是怎么证明自己可信的?
- [ ] 什么是前向安全性?ECDHE 为什么能实现它?
- [ ] TLS 1.3 删掉了哪类加密套件?为什么?
- [ ] 为什么说 TLS 不解决端到端加密问题?
- [ ]
ChangeCipherSpec在 TLS 1.2 和 1.3 中有什么差异?
常见卡点
卡点:ClientHello 和 ServerHello 的随机数有什么用? 防止重放攻击。每次握手都生成不同的随机数 → 即使 PreMasterSecret 相同(概率几乎为 0),MasterSecret 也完全不同 → 攻击者无法重放之前的会话。
卡点:为什么服务器证书要手工拼成链? 早期设计错误——服务器只发 Leaf 证,浏览器需要自己补中间证书。现在很多 CDN 和云厂商自动拼全链了,但自建服务器经常漏。ACME/certbot 默认全链。
卡点:TLS 握手不验证客户端吗? 验证。这叫双向 TLS(mTLS),客户端也要出示证书。多见于微服务间通信、网银 U 盾。openssl s_client ... -cert client.crt -key client.key
卡点:HTTP/2 和 TLS 的关系? HTTP/2 规范本身不强制 TLS,但浏览器实现只支持 HTTP/2 over TLS(h2 协议标识)。老版本的 h2c(明文 HTTP/2)已经被废弃。
现在不需要理解
- OCSP Stapling(在线证书状态检查的优化)——知道有就行,出问题时再查
- HPKE(TLS 1.3 中用的混合公钥加密)——未来可能会重要,现在不必深究
- QUIC/HTTP/3 用的是 TLS 1.3 传输——下一章会讲,此刻别分心
- AEAD 加密的 GCM 和 Poly1305 具体数学实现——你不需要手搓它
- 自签名证书的完整流程——跳过信任链的典型反模式,开发调试才用
旅人笔记
- 第一次 TLS 握手是最慢的——后续复用会话 ID 或 Session Ticket 可以省掉一次完整握手
- Chrome DevTools 的 Security 面板直接显示 TLS 版本和证书信息——开发时先看这里再打开 Wireshark
curl -v https://example.com是最快的 TLS 调试工具——它打印完整握手流程- 自签名证书在开发中好用,生产环境不可绕过 CA——
--no-check-certificate是坏习惯
→ 下一站预告: WebSocket 将 HTTP 变为全双工长连接,gRPC 用 Protocol Buffers 取代 JSON 重新定义远程调用。下一章:「现代协议:WebSocket 与 gRPC」,你将从「请求-响应」走向「流」。