3. 对称加密与认证加密在TLS中的应用

TLS 1.3 的机密性几乎完全建立在两类 AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)算法之上:AES-GCM 与 ChaCha20-Poly1305。它们同时提供机密性与完整性,取代了 TLS 1.2 中"先加密再单独 MAC"的分离式结构。理解这两类算法的内部组成、密钥如何从握手秘密派生、以及 nonce 如何被唯一管理,是判断 TLS 配置是否安全的核心。

一、AEAD 的三要素:密钥、nonce、AAD

AEAD 的输入不是"明文 + 密钥"这么简单,而是四个部分:

  • 密钥(key):由 TLS 密钥调度从握手秘密派生,长度固定(AES-128-GCM 为 16 字节,AES-256-GCM 为 32 字节,ChaCha20-Poly1305 为 32 字节)。
  • nonce:一次性数值,同一密钥下绝不允许重复。TLS 1.3 中每条记录使用 64 位序列号构造 nonce。
  • 明文(plaintext):待保护的记录载荷。
  • AAD(Associated Data):需要认证但不加密的数据,TLS 1.3 中即记录头(含类型、版本、长度)。

输出为密文与认证标签(tag)。AES-GCM 的 tag 为 16 字节,ChaCha20-Poly1305 的 tag 同样为 16 字节。接收方用相同密钥、nonce、AAD 验证 tag,验证失败即丢弃记录并触发 bad_record_mac 告警。

二、AES-GCM 与 ChaCha20-Poly1305 的结构差异

AES-GCM 由两部分组成:CTR 模式加密 + GHASH 认证。它依赖 AES 轮函数与 GF(2^128) 上的乘法,在有 AES-NI 或 ARM Crypto 扩展的 CPU 上性能极佳,是现代服务器首选。其弱点是软件实现时 GHASH 的查表操作可能受缓存时序攻击影响,因此必须使用硬件加速或恒定时间实现。

ChaCha20-Poly1305 由 ChaCha20 流密码 + Poly1305 一次性 MAC 组成。它完全基于 ARX(加、旋转、异或)运算,天然恒定时间,在无 AES 硬件加速的移动设备、嵌入式平台上性能优于 AES-GCM。RFC 8439 定义了其标准形式,TLS 1.3 使用 IETF 变体(96 位 nonce)。

TLS 1.3 AEAD 加密记录:密钥派生 → nonce 构造 → 加密 → 认证 握手秘密 Handshake Secret HKDF-Expand-Label 客户端/服务端写密钥 client/server_write_key HKDF-Expand-Label 写 IV(静态部分) client/server_write_iv Nonce 构造(每条记录唯一) write_iv(4 字节固定) 序列号 seq(8 字节,逐条递增) → 96 位 nonce AES-GCM 路径 AES-CTR 加密 AES-NI 硬件加速 GHASH 认证 GF(2¹²⁸) 乘法 输入:key(16/32B) + nonce(12B) + AAD + 明文 输出:密文 + 16 字节 tag ChaCha20-Poly1305 路径 ChaCha20 流加密 ARX 恒定时间 Poly1305 MAC 一次性认证 输入:key(32B) + nonce(12B) + AAD + 明文 输出:密文 + 16 字节 tag 选择依据:有 AES-NI/ARM Crypto → AES-GCM;无硬件加速或需恒定时间 → ChaCha20-Poly1305。两者 tag 均为 16 字节,nonce 均为 96 位。

三、密钥派生:HKDF-Expand-Label 的实际计算

TLS 1.3 使用 HKDF(RFC 5869)从握手秘密派生各阶段密钥。核心函数是 HKDF-Expand-Label,其结构为:

HKDF-Expand-Label(Secret, Label, Context, Length) =
    HKDF-Expand(Secret, HkdfLabel, Length)

HkdfLabel = struct {
    uint16 length;
    opaque label<7..255> = "tls13 " + Label;
    opaque context<0..255>;
}

以派生客户端写密钥为例,假设握手秘密为 HS(32 字节),标签为 "c hs traffic",上下文为握手哈希 TH(32 字节),目标长度 32 字节。实际计算时,先构造 HkdfLabel 字节串:长度字段 0x0020,标签字段为 "tls13 c hs traffic"(长度 19),上下文字段为 TH。然后以 HS 为 PRK,对该字节串做 HMAC-SHA256 扩展,得到 32 字节的 client_handshake_traffic_secret。再对该秘密做一次 HKDF-Expand-Label,标签为 "key",即得到 client_write_key;标签为 "iv",即得到 client_write_iv(12 字节,但实际只取前 4 字节作为静态部分参与 nonce 构造)。

这一过程的关键在于:每条连接、每个方向、每个加密阶段都使用不同的秘密。握手阶段与应用程序数据阶段使用不同的 traffic secret,即使某一阶段的密钥泄露,也不会直接危及另一阶段。

四、nonce 管理:TLS 1.3 的序列号方案

AEAD 的安全性对 nonce 重复极为敏感。AES-GCM 在同一密钥下重复 nonce 会导致认证密钥泄露,攻击者可伪造任意消息。TLS 1.3 采用确定性构造:

nonce = write_iv XOR (0x00000000 || seq_num)

其中:
  write_iv  = 12 字节,由 HKDF 派生,前 4 字节有效
  seq_num   = 64 位无符号整数,从 0 开始,每条记录递增 1
  实际参与异或的是 seq_num 的 8 字节大端表示

由于 seq_num 在每条记录后递增且不会回绕(TLS 1.3 规定达到 2^64-1 前必须重新密钥更新),同一密钥下 nonce 不会重复。接收方维护相同的序列号计数器,解密时用相同方式重建 nonce。若序列号不匹配或 tag 验证失败,记录被丢弃并发送 bad_record_mac 告警。

常见误区:不要将 write_iv 与 nonce 混为一谈。write_iv 是每条连接固定的 12 字节值,nonce 是 write_iv 与序列号异或后的结果,每条记录不同。若实现中错误地直接使用 write_iv 作为 nonce,将导致同一密钥下 nonce 重复,彻底破坏 AEAD 安全性。

五、密钥更新(KeyUpdate)与序列号耗尽

TLS 1.3 定义了 KeyUpdate 消息,允许任一方在不重新握手的情况下更新应用程序流量密钥。更新时,以当前 application_traffic_secret 为输入,通过 HKDF-Expand-Label 标签 "traffic upd" 派生新的秘密,并重置序列号计数器。触发条件通常有两个:一是序列号接近 2^64-1,二是策略要求定期轮换。更新后,旧密钥应尽快从内存中清除。

实际部署中,建议在以下场景主动触发 KeyUpdate:单条连接传输超过 2^30 条记录、或连接持续时间超过预设阈值(如 1 小时)。这可以限制单密钥加密的数据量,降低 GCM 在多密钥场景下的安全边界压力。

六、配置与验证要点

在 OpenSSL 中,可通过以下命令验证服务端是否优先协商 AES-GCM 或 ChaCha20-Poly1305:

openssl s_client -connect example.com:443 -tls1_3 -ciphersuites \
  TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

# 观察输出中的 "Cipher is TLS_AES_256_GCM_SHA384" 或
# "Cipher is TLS_CHACHA20_POLY1305_SHA256"

若服务端同时支持两者,客户端会依据自身硬件能力选择。在无 AES 加速的客户端上,ChaCha20-Poly1305 通常被优先选中。管理员应确保服务端 cipher suite 列表中至少包含一个 AEAD 算法,且不启用任何非 AEAD 的 TLS 1.3 套件(TLS 1.3 本身已移除所有非 AEAD 选项)。

对于自研 TLS 实现或使用密码库的场景,必须检查三点:nonce 是否由 write_iv 与序列号异或生成;序列号是否在每条记录后严格递增;KeyUpdate 后是否重置序列号并清除旧密钥。这三点是 AEAD 在 TLS 中安全落地的底线。