Skip to content

第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)的数学原理

你的任务

  1. 回答核心问题:HTTPS 比 HTTP 多了什么安全能力?(加密、认证、完整性)
  2. 理解 TLS 握手的核心流程(1.2 需要几次 RTT,1.3 怎么减少的)
  3. 掌握证书链验证机制——浏览器怎么知道证书可信,不需要手动实现
  4. 了解加密套件(Cipher Suite)的命名逻辑和一个典型选择
  5. 理解前向安全性的价值
  6. 梳理 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完整性校验 + PRFSHA-384

Python 实战:列出系统支持的套件

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 看到的那张)

验证顺序(自底向上):

  1. Leaf 证书的 issuer 字段 → 找 Intermediate 证书
  2. Intermediate 证书的 issuer 字段 → 找 Root CA 证书
  3. Root CA 证书必须在本机信任存储中——自签名所以叫 Root
  4. 每级用上一级的公钥验证签名
python
# 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

bash
# 查看证书链(两条命令等效)
openssl s_client -connect baidu.com:443 -showcerts
echo | openssl s_client -connect baidu.com:443 2>/dev/null | openssl x509 -text
Certificate 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 CA

s: 是 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 RTT

1.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:

python
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。

python
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 输出一致

通关挑战

基础级 🥉

  1. 启动 openssl s_client -connect yoursite.com:443 -showcerts,写出该站点的完整证书链(从 Leaf 到 Root)
  2. 识别服务器选择的 TLS 版本和加密套件

进阶级 🥈

  1. 用 Python 的 ssl.get_server_certificate() 下载一个站点的证书,然后用 cryptography 库解析其 Subject、Issuer、NotBefore、NotAfter、SAN
  2. 构建一个强制使用 TLS 1.2 的 Python HTTPS 客户端,确认加密套件是 ECDHE 前缀的

专家级 🥇

  1. 编写函数验证本地证书文件(PEM 格式)的链是否完整,指出缺失的中间证书
  2. 用 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」,你将从「请求-响应」走向「流」。

Built with VitePress | Software Systems Atlas