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)。
三、密钥派生: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 告警。
五、密钥更新(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 中安全落地的底线。