在本系列前面的文章中,我们分别讨论了对称加密(保证机密性)和消息认证码(保证完整性与认证性)。一个自然而关键的问题随之浮现:在实际系统中,我们几乎总是同时需要机密性和完整性——仅仅加密一条消息,并不能防止攻击者篡改密文;而仅仅认证一条消息,又不能防止攻击者窃取明文内容。那么,如何正确地将加密与认证组合在一起?这个看似简单的问题,在密码学史上曾引发过无数安全灾难,直到认证加密(Authenticated Encryption with Associated Data,简称 AEAD)这一统一原语的出现,才从根本上给出了令人满意的答案。
本文将从”为什么需要 AEAD”这一根本动机出发,给出 AEAD 的形式化安全定义,然后深入剖析三种最重要的 AEAD 方案——AES-GCM、ChaCha20-Poly1305 和 OCB 的内部构造。我们还将讨论 nonce 管理的陷阱、nonce-misuse resistant 设计(GCM-SIV)、NIST 轻量级密码学标准 Ascon,以及在实际工程中选择 AEAD 方案时需要考虑的各种因素。
一、为什么需要 AEAD
仅加密不提供完整性
许多初学者有一种直觉上的误解:如果消息被加密了,攻击者看不懂密文,自然也就无法有意义地篡改它。这种直觉是完全错误的。
考虑 CTR 模式加密。CTR 模式的密文满足 C = P ⊕ KS,其中 KS 是密钥流。攻击者虽然不知道 P 和 KS 的值,但如果他将密文的第 i 个字节异或上一个值 Δ,得到 C’ = C ⊕ Δ,则解密后的明文变为 P’ = P ⊕ Δ——攻击者精确地翻转了明文中对应比特的值。这就是密文可塑性(Ciphertext Malleability)。如果攻击者知道明文的部分结构(例如,JSON 格式中金额字段的位置),他完全可以在不知道密钥的情况下,将 “amount”:100 篡改为 “amount”:900。
CBC 模式同样存在类似问题。2002 年 Serge Vaudenay 发现的 CBC Padding Oracle 攻击表明,如果系统在解密时泄露了”填充是否合法”这一比特信息,攻击者可以通过反复篡改密文并观察系统的响应,逐字节恢复完整的明文。这一攻击在 TLS、ASP.NET、Java Server Faces 等众多实际系统中被成功利用,造成了严重的安全后果。
这些例子清楚地表明:加密只保证机密性,不保证完整性。攻击者不需要理解密文的含义,就可以对其进行有目的的篡改。
Encrypt-then-MAC:正确但笨拙
既然加密和认证是两种独立的安全属性,最直接的解决方案是同时使用加密算法和 MAC 算法。但组合的顺序至关重要,历史上出现过三种组合方式:
Encrypt-and-MAC(加密并 MAC):对明文 P 同时进行加密得到 C = Enc(K₁, P) 和计算 MAC 得到 T = MAC(K₂, P),然后发送 (C, T)。这种方式被 SSH 协议采用。问题在于 MAC 是对明文计算的,而 MAC 并不保证保密性——理论上 T 可能泄露关于 P 的信息。
MAC-then-Encrypt(先 MAC 后加密):先计算 T = MAC(K₂, P),然后加密 C = Enc(K₁, P || T)。这种方式被 TLS 1.0–1.2 的 CBC 套件采用。问题在于接收方必须先解密才能验证 MAC,这为 Padding Oracle 攻击打开了大门——解密过程中产生的错误信息(填充无效 vs. MAC 无效)成为了侧信道。
Encrypt-then-MAC(先加密后 MAC):先加密 C = Enc(K₁, P),然后计算 T = MAC(K₂, C)。接收方首先验证 T,只有 MAC 验证通过后才解密 C。这种方式被 IPsec 采用。Bellare 和 Namprempre 在 2000 年证明,在合理的安全假设下,Encrypt-then-MAC 是唯一一种在通用组合(Generic Composition)下总能提供认证加密安全性的方式。
然而,即使采用 Encrypt-then-MAC 这一正确的组合顺序,工程实践中仍然容易出错:需要使用两个独立的密钥(否则可能引入安全隐患)、需要确保 MAC 覆盖了密文的所有部分(包括 IV/nonce)、需要正确处理两个算法的参数协商。每一个环节都可能成为安全漏洞的来源。
AEAD:统一的解决方案
AEAD 的核心思想是将加密和认证封装为一个不可分割的原子操作。一个 AEAD 方案提供两个接口:
Encrypt(K, N, A, P) → (C, T)
Decrypt(K, N, A, C, T) → P 或 ⊥
其中 K 是密钥,N 是 nonce(一次性数值),A 是关联数据(Associated Data,被认证但不加密的数据),P 是明文,C 是密文,T 是认证标签。解密函数要么返回正确的明文,要么返回一个特殊的拒绝符号 ⊥,表示密文或关联数据被篡改。
关联数据(Associated Data)这一概念是 AEAD 相对于单纯认证加密的重要扩展。在实际协议中,许多元数据需要被认证(以防篡改)但不需要加密(因为它们本身就是公开的)。例如,在网络协议中,数据包的头部信息(如版本号、序列号、目的地址)需要明文传输以便路由,但必须被认证以防止攻击者篡改路由信息。AEAD 允许将这些头部信息作为 A 传入,既保证了它们的完整性,又避免了不必要的加密开销。
AEAD 的出现从根本上简化了密码学工程。开发者不再需要自己选择加密算法和 MAC 算法、决定组合顺序、管理多个密钥——他们只需要调用一个 AEAD 接口,就能同时获得机密性和完整性。这种”难以被误用”(Misuse-resistant)的设计哲学,是现代密码学 API 设计的核心原则。
二、AEAD 的形式化定义
IND-CPA + INT-CTXT = IND-CCA
要严格定义 AEAD 的安全性,我们需要回顾三个关键的安全概念。
IND-CPA(Indistinguishability under Chosen Plaintext Attack,选择明文攻击下的不可区分性):攻击者可以请求任意明文的加密,但仍然无法区分两段等长明文的密文。这是对机密性的基本要求。
INT-CTXT(Integrity of Ciphertext,密文完整性):攻击者可以请求任意明文的加密,但仍然无法伪造一个能通过验证的新的(密文,标签)对。这是对完整性的基本要求。
Bellare 和 Namprempre 在其开创性论文中证明了一个优美的等价关系:一个加密方案同时满足 IND-CPA 和 INT-CTXT,当且仅当它满足 IND-CCA(Indistinguishability under Chosen Ciphertext Attack,选择密文攻击下的不可区分性)——这是对称加密最强的标准安全概念。IND-CCA 要求即使攻击者拥有解密预言机(Oracle),他仍然无法区分两段等长明文的密文。
直觉上理解:INT-CTXT 保证攻击者无法伪造有效密文,因此解密预言机实际上对攻击者”无用”(除了解密由加密预言机产生的密文之外,任何其他查询都会返回 ⊥),从而将 IND-CCA 的安全性归约到了 IND-CPA。
基于 nonce 的 AEAD(nAEAD)
在形式化框架中,现代 AEAD 方案通常被建模为基于 nonce 的 AEAD(nonce-based AEAD,简称 nAEAD)。核心要求是:对于同一密钥 K,每个 nonce N 最多只能使用一次。在这个约束下,方案需要满足:
- 机密性:对于不同的 (N, A, P) 输入,加密输出与随机字符串在计算上不可区分。
- 完整性:攻击者在获得多个加密查询的结果后,无法伪造任何新的有效 (N, A, C, T) 元组。
nonce 的引入使得安全性定义更加精确。与概率化加密不同,nAEAD 方案是确定性的——对于同一组 (K, N, A, P),加密结果总是相同的。安全性完全依赖于 nonce 的唯一性。这意味着 nonce 管理是 AEAD 使用中最关键也最危险的环节——我们将在第四节专门讨论这一问题。
安全性的量化界
在具体实例化中,安全性通常以优势(Advantage)的上界来量化。对于一个 AEAD 方案 Π,攻击者的优势通常形如:
Adv(A) ≤ f(q, l, σ) / 2^n
其中 q 是查询次数,l 是单条消息的最大长度,σ 是所有查询的总块数,n 是底层原语的状态大小(例如 AES 的 128 位)。这个界告诉我们,随着加密数据量的增长,安全性会逐渐降低——这就是所谓的生日界(Birthday Bound)限制。对于 128 位的 AES-GCM,当加密的数据总量接近 2^64 个块时,安全性就开始显著下降,这是实际部署中必须注意的限制。
三、AES-GCM 深入
AES-GCM(Galois/Counter Mode)由 David McGrew 和 John Viega 于 2004 年提出,是目前使用最广泛的 AEAD 方案。它被 TLS 1.2/1.3、IPsec、SSH、WireGuard 等几乎所有主流安全协议采用。GCM 的设计理念是将 CTR 模式加密与基于有限域乘法的 GHASH 认证器组合在一起,在充分利用硬件加速(AES-NI 和 CLMUL 指令)的情况下实现极高的吞吐量。
CTR 模式加密
GCM 的加密部分使用 CTR(Counter)模式。给定一个 96 位 nonce N 和一个 128 位 AES 密钥 K,GCM 构造计数器块:
J₀ = N || 0x00000001 (128 位,nonce 后接 32 位计数器,初始值为 1)
Jᵢ = N || (i + 1) (计数器递增)
加密过程为:
Cᵢ = Pᵢ ⊕ AES_K(Jᵢ) (i = 1, 2, ..., n)
注意 J₀ 不用于加密明文,而是保留用于最后生成认证标签。这是一个精巧的设计:J₀ 的 AES 加密结果被用作 GHASH 输出的掩码,确保认证标签的不可预测性。
GHASH:有限域上的多项式求值
GHASH 是 GCM 认证部分的核心,本质上是在有限域 GF(2^128) 上的多项式求值。理解 GHASH 需要先理解有限域 GF(2^128) 上的运算。
GF(2^128) 的基本结构:GF(2^128) 是一个包含 2^128 个元素的有限域。每个元素可以表示为一个次数不超过 127 的二进制多项式。域上的加法就是按位异或(XOR),乘法则是多项式乘法后对一个不可约多项式取模。GCM 使用的不可约多项式为:
p(x) = x^128 + x^7 + x^2 + x + 1
GHASH 的计算过程:设 H = AES_K(0^128) 是哈希密钥(用全零块加密得到),输入数据被分成 128 位的块 X₁, X₂, …, Xₘ(最后一块可能需要零填充)。GHASH 的计算是一个迭代过程:
Y₀ = 0^128
Yᵢ = (Yᵢ₋₁ ⊕ Xᵢ) · H (在 GF(2^128) 中的乘法)
GHASH_H(X) = Yₘ
从代数角度看,这等价于计算多项式 f(H) = X₁·H^m ⊕ X₂·H^(m-1) ⊕ … ⊕ Xₘ·H。也就是说,GHASH 将输入块作为多项式系数,在秘密点 H 处求值。这种 Horner 形式的迭代计算恰好对应了上面的递推公式。
GF(2^128) 乘法的实现:GF(2^128) 乘法是 GHASH 的核心运算,也是 GCM 性能的瓶颈。在没有硬件加速的情况下,一种经典的实现方式是”移位-异或”(Shift-XOR)方法,类似于小学乘法,但所有加法替换为 XOR:
/*
* GF(2^128) multiply-and-accumulate for AES-GCM GHASH.
*
* X, Y: 128-bit operands stored as two 64-bit halves (big-endian bit order).
* The irreducible polynomial is p(x) = x^128 + x^7 + x^2 + x + 1,
* whose reduced constant is 0xE1 placed in the most-significant byte
* after a shift, giving the reduction mask 0xE100000000000000.
*
* After multiplication, the result is XORed into the accumulator (acc)
* to support the iterative GHASH computation:
* Y_i = (Y_{i-1} ^ X_i) * H
*
* WARNING: This is a reference implementation for clarity.
* It is NOT constant-time and must not be used where timing
* side-channels are a concern. Production code should use
* the PCLMULQDQ / CLMUL instruction or a table-driven method
* with constant-time table lookup.
*/
#include <stdint.h>
typedef struct {
uint64_t hi; /* bits 127..64 */
uint64_t lo; /* bits 63..0 */
} block128;
static void ghash_mul_acc(block128 *acc, const block128 *X, const block128 *H)
{
/*
* Reduction constant: when bit 128 overflows we XOR the
* representation of x^7 + x^2 + x + 1 into the high word.
*/
const uint64_t R = UINT64_C(0xE100000000000000);
/* Z accumulates the product; V is the shifting copy of H */
uint64_t Zhi = 0, Zlo = 0;
uint64_t Vhi = H->hi, Vlo = H->lo;
/* Iterate over each bit of X, MSB first (big-endian bit order) */
/* Process high 64 bits of X */
for (int i = 0; i < 64; i++) {
/* If the current bit of X.hi is set, XOR V into Z */
if ((X->hi >> (63 - i)) & 1) {
Zhi ^= Vhi;
Zlo ^= Vlo;
}
/*
* Shift V right by one bit in the polynomial basis.
* If the LSB was 1, a reduction is needed: XOR R into
* the high word (this accounts for x^128 mod p(x)).
*/
uint64_t carry = Vlo & 1;
Vlo = (Vlo >> 1) | (Vhi << 63);
Vhi = Vhi >> 1;
if (carry)
Vhi ^= R;
}
/* Process low 64 bits of X */
for (int i = 0; i < 64; i++) {
if ((X->lo >> (63 - i)) & 1) {
Zhi ^= Vhi;
Zlo ^= Vlo;
}
uint64_t carry = Vlo & 1;
Vlo = (Vlo >> 1) | (Vhi << 63);
Vhi = Vhi >> 1;
if (carry)
Vhi ^= R;
}
/* XOR the product into the accumulator (multiply-and-accumulate) */
acc->hi ^= Zhi;
acc->lo ^= Zlo;
}上述代码实现了 GF(2^128) 中的乘法并将结果累加到累加器中,这正是 GHASH 迭代中 Yᵢ = (Yᵢ₋₁ ⊕ Xᵢ) · H 这一步的核心。代码遵循大端比特序(MSB first),逐位扫描 X 的每一位:如果当前位为 1,则将 V(H 的移位副本)异或到结果 Z 中;然后将 V 右移一位,如果移出的最低位为 1,则异或约化常数 R = 0xE1 << 56,对应不可约多项式 x^128 + x^7 + x^2 + x + 1 的低阶部分。
需要强调的是,这个逐位实现不是常数时间的——分支条件取决于秘密数据 X 和 H,可能导致时序侧信道泄露。生产环境中应当使用 Intel 的 PCLMULQDQ(无进位乘法)指令或预计算表方法来实现 GF(2^128) 乘法,以消除时序侧信道。现代 x86 处理器上的 PCLMULQDQ 指令可以在几个时钟周期内完成一次 128 位无进位乘法,配合 CLMUL 加速的 GCM 实现可以达到亚周期每字节(sub-cycle per byte)的吞吐量。
GCM 的整体流程
将加密和认证结合起来,GCM 的完整加密流程如下:
- 计算哈希密钥:H = AES_K(0^128)。
- 构造初始计数器:J₀ = N || 0x00000001(对于 96 位 nonce)。
- CTR 加密:对每个明文块 Pᵢ,计算 Cᵢ = Pᵢ ⊕ AES_K(inc(Jᵢ₋₁))。
- 构造 GHASH 输入:将关联数据 A(零填充至 128 位块边界)、密文 C(零填充至 128 位块边界)、以及一个 128 位的长度块 len(A) || len(C)(各 64 位,表示比特长度)依次拼接。
- 计算 GHASH:对上述拼接数据计算 GHASH_H。
- 生成标签:T = GHASH_H(A, C, len) ⊕ AES_K(J₀)。
最后一步中 AES_K(J₀) 的异或是至关重要的——它确保了认证标签的不可预测性。没有这一步,GHASH 的输出直接作为标签会泄露关于 H 的信息(因为 GHASH 是一个线性函数),攻击者可能利用这一点伪造标签。
解密时,接收方首先重新计算 GHASH 和预期的标签值,然后在常数时间内比较计算出的标签与收到的标签。只有标签匹配时,才对密文进行 CTR 解密并返回明文。标签验证必须在解密之前完成——绝不能在标签验证失败时释放任何部分明文,否则可能导致类似 Padding Oracle 的攻击。
四、GCM 的 nonce 管理
Nonce 重用的灾难性后果
GCM 对 nonce 的唯一性要求是绝对的——在同一密钥下重用 nonce 不仅仅是”降低安全性”,而是完全摧毁安全性。
机密性的丧失:如果两条消息 P₁ 和 P₂ 使用了相同的 (K, N),由于 CTR 模式产生了相同的密钥流 KS,攻击者可以计算 C₁ ⊕ C₂ = P₁ ⊕ P₂,得到两条明文的异或。结合频率分析或已知明文,可以恢复两条明文的内容。这与 OTP(一次一密)的重用问题完全相同。
认证性的丧失(GHASH 密钥恢复):这是 GCM nonce 重用最严重的后果。回顾 GHASH 的计算:标签 T = GHASH_H(data) ⊕ AES_K(J₀)。如果两条不同的消息使用了相同的 (K, N),则 AES_K(J₀) 相同。攻击者可以计算:
T₁ ⊕ T₂ = GHASH_H(data₁) ⊕ GHASH_H(data₂)
由于 GHASH 是一个多项式,T₁ ⊕ T₂ 等于一个关于 H 的已知多项式,其系数由 data₁ 和 data₂ 决定。攻击者可以对这个多项式求根(在 GF(2^128) 上求根是高效的),从而恢复哈希密钥 H。一旦 H 被恢复,攻击者可以对任意消息计算有效的 GHASH 值,结合已知的 AES_K(J₀)(从 T₁ 和 GHASH_H(data₁) 中恢复),他可以伪造任意消息的认证标签——认证性被完全打破。
Joux 在 2006 年的论文中给出了这一攻击的完整描述。这一攻击是实际可行的,不需要任何超出多项式算术的计算资源。教训非常明确:在同一密钥下,绝不允许 nonce 重复。
一个值得深思的现象是:AEAD 作为一个概念被提出,本是为了终结”加密与认证分离”所导致的长期安全灾难——先加密后 MAC、先 MAC 后加密、加密同时 MAC,这三种组合方式中只有 Encrypt-then-MAC 是可证明安全的,而实践中选错组合的事故屡见不鲜。AEAD 将加密与认证封装为一个原子操作,理论上消除了这一类错误。然而,它在解决旧问题的同时,却悄然引入了一个全新的脆弱性表面:nonce 管理。旧的 MAC-then-encrypt 漏洞至少会在解密时产生可观测的异常(填充错误、认证失败),而 nonce 重用却是彻底”静默”的——密文看起来完全正常,系统运行毫无报错,但安全性已经归零。从某种意义上说,AEAD 把一类”显性 bug”替换成了一类”隐性 bug”,后者在工程实践中反而更加危险。
笔者认为,nonce 误用抵抗(nonce-misuse resistance)本应从一开始就是 AEAD 的默认设计目标,而不是作为事后补丁出现的”高级特性”。GCM 被标准化的 2007 年,密码学界已经完全清楚 nonce 重用的灾难性后果——Joux 的攻击论文发表于前一年。但 GCM 的设计者选择了性能最优的路径:单遍在线处理,代价是对 nonce 唯一性的绝对依赖。SIV 构造(同年由 Rogaway 和 Shrimpton 提出)本可以成为默认推荐,但它的两遍扫描和不支持在线处理被视为不可接受的工程代价。这个决策在当时或许合理,但回头看,它让整个行业在此后十五年中反复为 nonce 管理付出代价——从 TLS 实现中的计数器溢出,到云存储系统中的虚拟机快照回退,再到无数开发者在分布式环境中对”如何保证 nonce 唯一”的苦苦挣扎。
96 位 nonce 的标准实践
GCM 规范允许任意长度的 nonce,但推荐使用 96 位(12 字节)的 nonce。当 nonce 为 96 位时,J₀ = N || 0x00000001,计算简洁高效。当 nonce 长度不是 96 位时,GCM 需要对 nonce 做一次 GHASH 计算以将其压缩到 128 位,这不仅增加了计算开销,还引入了额外的安全考量(不同长度的 nonce 可能经过 GHASH 后碰撞)。因此,实际部署中几乎总是使用 96 位 nonce。
确定性 nonce vs. 随机 nonce
96 位的 nonce 空间为 2^96,看似很大。但如果使用随机生成的 nonce,根据生日悖论,大约在 2^48 条消息后就有显著的碰撞概率——对于高流量系统,这可能是一个真实的威胁。因此,NIST SP 800-38D 推荐了两种 nonce 生成策略:
确定性构造:使用一个单调递增的计数器作为 nonce(或 nonce 的一部分)。例如,TLS 1.3 将一个 64 位的序列号(Sequence Number)与从密钥派生的 IV 异或来生成 96 位 nonce。计数器保证了唯一性,前提是不发生回绕。这是最安全的方法,适用于单一发送方的场景。
随机 nonce(RBG-based):当无法维护状态(例如在分布式系统中)时,可以使用密码学安全随机数生成器生成 nonce。NIST 建议在这种情况下限制同一密钥下的加密次数不超过 2^32 条消息,以将碰撞概率控制在可接受范围内。对于更高的消息量,应当进行密钥轮换。
五、ChaCha20-Poly1305
设计背景与动机
ChaCha20-Poly1305 由 Daniel J. Bernstein 设计(ChaCha20 流密码和 Poly1305 消息认证码分别独立设计,后由 Adam Langley 组合为 AEAD 方案),并在 RFC 8439 中标准化。它的出现有明确的工程动机:
无需 AES 硬件加速:AES-GCM 的高性能严重依赖 AES-NI 指令集。在没有 AES-NI 的平台上(如早期的 ARM 处理器、嵌入式设备、某些移动设备),纯软件实现的 AES 不仅速度慢,而且很难做到常数时间执行(查表实现容易受到缓存时序攻击)。ChaCha20 基于 ARX(Add-Rotate-XOR)运算,在任何平台上都能高效且安全地用纯软件实现。
天然抗时序攻击:ChaCha20 和 Poly1305 的所有运算——32 位加法、32 位异或、固定距离循环移位——在所有现代 CPU 上都是常数时间的。不需要查表,不需要 S-box,消除了缓存时序侧信道的风险。
更大的安全余量:ChaCha20 的 20 轮设计被广泛认为具有充足的安全余量。相比之下,AES-128 的 10 轮设计虽然目前尚无实际攻击,但已有针对 7 轮的有效攻击,安全余量相对较小。
RFC 8439 的逐步构造
RFC 8439 详细规定了 ChaCha20-Poly1305 AEAD 的构造步骤。
第一步:生成 Poly1305 密钥。使用 ChaCha20 的第一个块(计数器值为 0)生成一个一次性的 256 位 Poly1305 密钥 r || s。具体地,以 AEAD 密钥 K(256 位)和 nonce N(96 位)作为 ChaCha20 的输入,计数器设为 0,执行 ChaCha20 块函数,取输出的前 32 字节作为 Poly1305 的密钥。
第二步:加密明文。使用 ChaCha20 从计数器值 1 开始生成密钥流,对明文进行异或加密。注意计数器从 1 而非 0 开始,因为计数器 0 的输出已经被用来生成 Poly1305 密钥。
第三步:构造 Poly1305 输入。将关联数据 A(零填充至 16 字节对齐)、密文 C(零填充至 16 字节对齐)、以及 A 的长度和 C 的长度(各 8 字节,小端序)依次拼接,构造 Poly1305 的输入消息。
第四步:计算 Poly1305 标签。使用第一步生成的 Poly1305 密钥对第三步的输入计算 Poly1305 MAC,得到 128 位的认证标签 T。
Poly1305 的数学原理
Poly1305 是一个基于素数域上多项式求值的一次性 MAC。给定密钥 (r, s),其中 r 是一个”夹紧”(Clamped)的 128 位整数(某些位被强制置零以保证安全性和实现效率),s 是一个 128 位整数,消息被分成 16 字节的块 c₁, c₂, …, cₘ。Poly1305 的计算为:
Tag = ((c₁ · r^m + c₂ · r^(m-1) + ... + cₘ · r) mod p) + s (mod 2^128)
其中 p = 2^130 - 5 是一个梅森素数附近的素数。选择这个特殊的素数使得模约化可以非常高效地实现——2^130 mod p = 5,这意味着溢出的高位只需乘以 5 再加回低位即可完成约化。
Poly1305 与 GHASH 有本质的相似性——两者都是多项式求值。但 Poly1305 工作在素数域(近 2130)而非二元扩展域(GF(2128)),这使得它在纯软件实现中更高效——普通的整数乘法指令就足够了,不需要无进位乘法。同时,+s 的最后一步加法(而非 XOR)是一个信息论安全的掩码——即使攻击者知道了多项式部分的值,不知道 s 就无法伪造标签。关键的安全要求是:(r, s) 对每条消息必须是唯一的,绝不能重用。在 ChaCha20-Poly1305 中,这通过从 ChaCha20 的输出中派生一次性 (r, s) 来自动保证。
性能与部署优势
ChaCha20-Poly1305 在 Google Chrome、Android、OpenSSH、WireGuard 等系统中得到了广泛部署。在没有 AES-NI 的平台上,ChaCha20-Poly1305 的性能通常是 AES-GCM 软件实现的 3 倍以上。即使在有 AES-NI 的平台上,两者的性能也是可比的。更重要的是,ChaCha20-Poly1305 的安全性不依赖于任何硬件特性——它在所有平台上都提供一致的时序安全性保证。
GCM 与 ChaCha20-Poly1305 数据流对照
至此我们已经深入讨论了两大主流 AEAD 方案,以下将它们的数据流与核心设计要素并列对照:
AES-GCM 数据流 ChaCha20-Poly1305 数据流
======================== ========================
密钥: AES-128/256 密钥 K 密钥: 256 位密钥 K
Nonce: 96 位 N Nonce: 96 位 N
| |
v v
┌─────────────────┐ ┌──────────────────────┐
│ AES-CTR 加密 │ │ ChaCha20(K, N, ctr=0)│
│ J₀ = N‖0x01 │ │ → 生成 Poly1305 │
│ Jᵢ = N‖(i+1) │ │ 一次性密钥 (r, s) │
│ Cᵢ = Pᵢ ⊕ AES(Jᵢ)│ │ ChaCha20(K, N, ctr=1) │
└────────┬────────┘ │ → 密钥流 ⊕ 明文 = 密文│
| └──────────┬───────────┘
v v
┌─────────────────┐ ┌──────────────────────┐
│ GHASH 认证 │ │ Poly1305 认证 │
│ 域: GF(2^128) │ │ 域: Z_p, p=2^130 − 5 │
│ 运算: 无进位乘法 │ │ 运算: 整数乘法 + 加法 │
│ H = AES_K(0^128)│ │ Tag = poly(r) + s │
│ 多项式求值 over H│ │ 多项式求值 over r │
└────────┬────────┘ └──────────┬───────────┘
| |
v v
128 位认证标签 T 128 位认证标签 T
维度 AES-GCM ChaCha20-Poly1305
───────────── ──────────────────────── ────────────────────────
流密码 AES-CTR(分组密码计数模式) ChaCha20(ARX 流密码)
认证器 GHASH Poly1305
认证域 二元扩展域 GF(2^128) 素数域 Z_(2^130 − 5)
核心运算 无进位乘法(CLMUL) 普通整数乘法
硬件加速依赖 AES-NI + CLMUL 无(纯 ALU 运算)
最优平台 Intel/AMD(x86-64) ARM、MIPS、通用 CPU
认证密钥来源 AES_K(0^128) 生成 H ChaCha20 第 0 块生成 (r,s)
掩码方式 GHASH 输出 ⊕ AES_K(J₀) 多项式值 + s(mod 2^128)
时序安全性 依赖 CLMUL 指令 天然常量时间
回顾这张对照图,我个人最深刻的体会是:GCM 和 ChaCha20-Poly1305 的分野,本质上映射了 Intel 与 ARM 两大架构阵营在密码学硬件加速策略上的根本分歧。Intel 选择在指令集中嵌入 AES-NI 和 CLMUL——为 GCM 量身打造硬件加速通道;而 ARM 长期以来将晶体管预算更多地分配给通用计算单元,使得不依赖专用指令的 ChaCha20 和 Poly1305 在其上反而如鱼得水。这不是”谁更安全”的问题,而是”在哪块硅片上跑”的问题。正因如此,现代 TLS 库几乎都同时实现两套方案,并在握手协商时根据对端的硬件能力自动选择——这种工程上的务实,恰恰是密码学从象牙塔走向真实部署的标志。
六、OCB 模式
最高效的 AEAD
OCB(Offset Codebook Mode)由 Phillip Rogaway 于 2001 年首次提出(OCB1),后经修改发展为 OCB2(2004 年)和 OCB3(2011 年,RFC 7253)。OCB 的独特之处在于它只需要每个明文块一次分组密码调用即可同时完成加密和认证——相比之下,GCM 对每个块需要一次 AES 调用(用于 CTR 加密)加上一次 GF(2^128) 乘法(用于 GHASH),CTR+HMAC 则需要更多。
这种极高的效率来源于 OCB 的设计理念:将认证信息”免费地”嵌入到加密过程中,而不是在加密之后另外追加一个独立的认证计算。
偏移量计算
OCB 的核心思想是为每个明文块计算一个偏移量(Offset),然后用”加偏移-加密-再加偏移”的结构处理每个块:
Offset_i = Offset_{i-1} ⊕ L_{ntz(i)}
C_i = Offset_i ⊕ AES_K(P_i ⊕ Offset_i)
其中 ntz(i) 表示 i 的二进制表示中末尾零的个数,L_j 是从密钥预计算的一系列值(L_* = AES_K(0^128),L_0 = double(L_*),L_j = double(L_{j-1}),其中 double 是 GF(2^128) 中的倍乘运算)。
这种设计的精妙之处在于:偏移量的计算几乎不需要额外开销(只需一次 XOR),而偏移量的存在确保了每个块的加密输入都是唯一的(即使明文块相同),同时使得认证信息可以通过对所有明文块的校验和(Checksum = P₁ ⊕ P₂ ⊕ … ⊕ Pₙ)进行一次额外的 AES 加密来获得标签:
Tag = AES_K(Checksum ⊕ Offset_n ⊕ L_$) ⊕ HASH_K(A)
其中 HASH_K(A) 是对关联数据的处理。整个方案只需要 n + 1 次 AES 调用(n 个明文块 + 1 次标签计算),加上对关联数据的少量 AES 调用。
专利历史与自由许可
OCB 的广泛采用受到了严重的专利阻碍。Rogaway 为 OCB 申请了多项专利,这在密码学社区引发了长期的争议。虽然 Rogaway 后来为开源软件和非军事用途提供了免费许可,但专利的存在仍然使得许多组织和标准化机构望而却步。
2021 年,Rogaway 的主要 OCB 专利到期,OCB 现在可以自由使用。然而,专利期间造成的”冷落效应”已经难以逆转——GCM 和 ChaCha20-Poly1305 已经占据了几乎所有的部署空间,OCB 错过了进入主流标准的窗口期。
为什么 OCB 未能广泛采用
除了专利问题,OCB 未能广泛采用还有其他技术和生态原因:
- GCM 的先发优势:GCM 在 OCB 的专利问题解决之前就已经被 NIST 标准化(SP 800-38D),并进入了 TLS、IPsec 等关键协议。生态系统的惯性极难克服。
- OCB2 的安全缺陷:2018 年,Inoue 和 Minematsu 发现了 OCB2 的严重安全漏洞,允许通过精心构造的查询伪造认证标签。虽然 OCB3 不受此攻击影响,但这一事件损害了整个 OCB 家族的声誉。
- 硬件加速的发展:随着 AES-NI 和 CLMUL 指令集的普及,GCM 在有硬件加速的平台上达到了极高的性能,使得 OCB 的效率优势变得不那么显著。
七、AES-GCM-SIV
Nonce 误用抵抗设计
正如第四节所讨论的,GCM 在 nonce 重用时的安全性崩溃是灾难性的。在某些实际场景中——例如虚拟机快照恢复导致计数器回退、分布式系统中的时钟不同步、或简单的编程错误——nonce 重用可能是不可避免的。这催生了抗 nonce 误用(Nonce-misuse Resistant)AEAD 设计的研究。
AES-GCM-SIV 由 Shay Gueron 和 Yehuda Lindell 提出,是 GCM 的 nonce-misuse resistant 变体。其核心思想是合成 IV(Synthetic IV,SIV)构造。
SIV 构造的核心思想
SIV 构造最早由 Rogaway 和 Shrimpton 在 2006 年提出。核心思想是:不是把 nonce 直接用作 CTR 模式的 IV,而是从明文和关联数据中派生出 IV:
Tag = PRF_K₁(N, A, P)
C = CTR_K₂(Tag, P) (使用 Tag 作为 CTR 的 IV)
这种设计的安全性保证是:即使 nonce 重复,只要明文不同,派生出的 Tag(进而 CTR 的 IV)也几乎不会相同,因此 CTR 模式的密钥流仍然不同,机密性得以保持。只有当 (N, A, P) 完全相同时,加密结果才会相同——但这只泄露了”两条消息相同”这一信息,而不会像 GCM 那样导致密钥恢复或认证伪造。
AES-GCM-SIV 的两遍构造
AES-GCM-SIV 对 SIV 概念进行了优化以适应实际部署:
第一遍(认证):使用 POLYVAL(GHASH 的变体,采用小端比特序以更好地适配现代处理器)对关联数据和明文计算认证哈希。然后将 POLYVAL 的输出与 nonce 异或,再用 AES 加密,得到合成 Tag。
第二遍(加密):使用 Tag 作为 CTR 模式的初始计数器值,用 AES-CTR 对明文进行加密。Tag 的最高位被清零以确保 CTR 计数器空间充足。
注意到 AES-GCM-SIV 需要两遍扫描明文——第一遍计算 POLYVAL 用于认证,第二遍进行 CTR 加密。这意味着它不是一个在线(Online)方案——必须在加密开始前知道完整的明文。这是 SIV 构造为获得 nonce-misuse resistance 所付出的固有代价。
安全性保证
AES-GCM-SIV 的安全性保证可以概括为:
- nonce 不重复时:提供与标准 GCM 相同的 IND-CPA + INT-CTXT 安全性。
- nonce 重复时:不会泄露密钥或 GHASH 密钥,不会导致认证伪造。唯一的信息泄露是攻击者可以检测到两次使用相同 (N, A, P) 的加密是否产生了相同的密文——即泄露了消息的相等性。
这种”优雅降级”(Graceful Degradation)的特性使得 AES-GCM-SIV 成为对 nonce 管理要求较低的应用场景的理想选择。Google 在其 Tink 密码学库和内部系统中广泛部署了 AES-GCM-SIV。
从更深层的角度看,AES-GCM-SIV 的”优雅降级”理念揭示了密码学设计中一个被长期忽视的原则:安全方案的真正考验不在于一切正常时的表现,而在于出错时的行为。传统密码学证明关注的是”在正确使用条件下,方案能抵御什么攻击”,但工程现实是:条件从来不会被完全正确地满足。一个在前提条件被违反时安全性归零的方案(如 GCM),和一个在前提条件被违反时安全性优雅退化的方案(如 GCM-SIV),在数学证明中可能具有相同的”可证明安全性”,但在工程价值上却有天壤之别。这正是为什么 Google 的 Tink 库将 AES-GCM-SIV 而非 AES-GCM 作为默认推荐——他们深知,在管理着数十亿设备的生态系统中,“nonce 绝不重复”是一个无法在架构层面保证的假设。
八、Ascon
NIST 轻量级密码学标准
2023 年,NIST 宣布 Ascon 为其轻量级密码学标准化竞赛的最终获胜者。Ascon 由 Christoph Dobraunig、Maria Eichlseder、Florian Mendel 和 Martin Schläffer 于 2014 年设计,经历了长达数年的公开密码分析和严格评估。
轻量级密码学(Lightweight Cryptography)针对的是资源受限的设备——微控制器、传感器节点、RFID 标签、物联网(IoT)设备等。这些设备的计算能力、内存、能耗预算远低于通用计算机,传统的 AES-GCM 或 ChaCha20-Poly1305 在这些平台上可能过于”重量级”。Ascon 旨在提供一个在极低资源开销下仍然安全的 AEAD(和哈希)方案。
海绵构造与置换设计
Ascon 基于海绵构造(Sponge Construction)——与 SHA-3(Keccak)相同的设计范式。海绵构造的核心思想是:维护一个固定大小的内部状态,通过反复应用一个置换(Permutation)来处理输入并产生输出。状态被分为两部分:速率(Rate)部分 r 和容量(Capacity)部分 c。外部数据只与速率部分交互,容量部分则作为”安全余量”保持隐藏。
Ascon 的内部状态为 320 位(40 字节),分为 64 位速率和 256 位容量(用于 Ascon-128)。置换 p 由以下三个步骤组成:
- 常数加(Constant Addition):向状态异或一个轮常数,打破轮之间的对称性。
- 替代层(Substitution Layer):对状态的每 5 位列应用一个 5 位 S-box。这个 S-box 具有良好的差分和线性性质。
- 线性扩散层(Linear Diffusion Layer):对状态的每个 64 位字应用循环移位和异或操作,确保每一位的变化能快速扩散到整个状态。
Ascon-128 使用两种轮数的置换:初始化和终结化使用 12 轮(p^a),中间的数据处理阶段使用 6 轮(p^b)。较少的中间轮数是 Ascon 在资源受限平台上实现高性能的关键——它在安全性和效率之间取得了精心的平衡。
AEAD 流程
Ascon AEAD 的工作流程如下:
- 初始化:将密钥 K(128 位)、nonce N(128 位)和一个 IV 常数装入 320 位状态,然后应用 12 轮置换 p^a。之后将密钥再次异或到状态的最低 128 位。
- 处理关联数据:将关联数据分成 64 位的块,每块异或到状态的速率部分,然后应用 6 轮置换 p^b。处理完毕后对状态异或一个分隔常数。
- 加密明文:将明文分成 64 位的块,每块异或到状态的速率部分以产生密文块,然后应用 6 轮置换 p^b。
- 终结化:将密钥异或到状态中,应用 12 轮置换 p^a,然后再次异或密钥并提取 128 位认证标签。
应用场景
Ascon 的目标应用场景包括:
- IoT 设备:传感器网络、智能家居设备等资源极其受限的平台。
- 嵌入式系统:汽车电子、工业控制系统中的安全通信。
- RFID 和 NFC:短消息的认证加密。
- 硬件实现:Ascon 的设计特别考虑了硬件实现的效率,在 ASIC 和 FPGA 上可以用极少的逻辑门实现。
值得注意的是,Ascon 并非要取代 AES-GCM 或 ChaCha20-Poly1305——在通用计算平台上,后者仍然是更好的选择。Ascon 填补的是轻量级设备上缺乏标准化 AEAD 方案的空白。
九、AEAD 选型与大消息处理
方案对比
在选择 AEAD 方案时,需要综合考虑多个维度。下表总结了主要 AEAD 方案的关键特征:
| 方案 | 底层原语 | 密钥/nonce 长度 | 每块开销 | 硬件加速依赖 | 抗 nonce 误用 | 标准化状态 |
|---|---|---|---|---|---|---|
| AES-GCM | AES + GHASH | 128/256 位 / 96 位 | 1 AES + 1 GF mul | AES-NI + CLMUL | 否 | NIST SP 800-38D |
| ChaCha20-Poly1305 | ChaCha20 + Poly1305 | 256 位 / 96 位 | 1 ChaCha20 round + 1 int mul | 无 | 否 | RFC 8439 |
| OCB3 | AES | 128/256 位 / ≤120 位 | 1 AES | AES-NI | 否 | RFC 7253 |
| AES-GCM-SIV | AES + POLYVAL | 128/256 位 / 96 位 | ≈GCM 的 2 倍 | AES-NI + CLMUL | 是 | RFC 8452 |
| Ascon-128 | Ascon 置换 | 128 位 / 128 位 | 1 Ascon p^b | 无(轻量级优化) | 否 | NIST LWC |
STREAM 构造:流式 AEAD
标准的 AEAD 方案要求在开始认证计算之前知道完整的消息。对于大消息(如多 GB 的文件加密)或流式数据(如实时视频加密),这是一个实际问题——不可能将整个消息缓存在内存中。
STREAM 构造(由 Hoang、Reyhanitabar 和 Rogaway 提出)解决了这一问题。其核心思想是将大消息分成固定大小的片段(Segment),对每个片段使用标准 AEAD 独立加密,但在每个片段的关联数据中编码片段的序号和”是否为最后一个片段”的标志:
C_i = AEAD.Encrypt(K, N_base || i, (segment_index || is_last), P_i)
这种构造保证了:
- 片段的完整性:每个片段独立认证,篡改任一片段都会被检测到。
- 片段的顺序:序号被纳入关联数据,攻击者无法重排片段。
- 流的完整性:最后一个片段的标志防止了截断攻击(攻击者丢弃末尾片段)。
TLS 1.3 的记录层协议实质上就是 STREAM 构造的一个实例——每条 TLS 记录是一个独立的 AEAD 加密单元,序列号作为 nonce 的一部分保证了顺序和唯一性。
密钥承诺与”隐形蝾螈”问题
2020 年,Len、Grubbs 和 Ristenpart 发现了一个关于 AEAD 的微妙而重要的问题:标准 AEAD 方案(包括 AES-GCM 和 ChaCha20-Poly1305)不提供密钥承诺(Key Commitment)。具体地,存在这样的可能性:同一条密文在两个不同的密钥下解密为两个不同的合法明文——两个密钥各自的认证标签都能通过验证。
这被称为”隐形蝾螈”(Invisible Salamanders)问题,因为攻击者可以发送一条密文,使得不同的接收者(持有不同密钥)看到完全不同的明文内容。在端到端加密的群组消息场景中,这可能被利用来制造混乱——Alice 看到的消息内容与 Bob 看到的完全不同,但双方都认为消息是真实的。
这一问题的根本原因是:AEAD 的安全定义只涉及单一密钥下的安全性,没有对跨密钥行为做任何保证。解决方案包括:
- CTX 构造:在密文上追加密钥的哈希值作为承诺。
- 使用具有密钥承诺的 AEAD:某些 AEAD 方案(如基于海绵构造的 Ascon)天然提供密钥承诺。
- 在应用层验证:在解密后检查明文是否符合预期格式。
实践建议
综合以上讨论,以下是 AEAD 选型的实践建议:
首选 AES-GCM(在有 AES-NI 的平台上)。它是最广泛部署和审计的方案,性能卓越,所有主流 TLS 库和硬件都支持。但必须严格管理 nonce——使用确定性计数器,不要使用随机 nonce 除非消息量有严格的上限。
在无 AES-NI 的平台上选择 ChaCha20-Poly1305。它在纯软件实现中性能优越且天然抗时序攻击,是移动设备和嵌入式平台的理想选择。
对 nonce 管理缺乏信心时选择 AES-GCM-SIV。虽然性能略低于 GCM,但 nonce-misuse resistant 特性提供了宝贵的安全余量。特别适合数据库加密(静态数据)等 nonce 管理困难的场景。
在资源极度受限的设备上选择 Ascon。它是 NIST 标准化的轻量级 AEAD,专为 IoT 和嵌入式设备设计。
避免自行组合加密和 MAC。除非有非常特殊的需求和充分的密码学专业知识,否则应当使用现成的 AEAD 方案,而不是自行实现 Encrypt-then-MAC 构造。AEAD 的全部意义就在于消除这种组合中的出错可能性。
对于大消息和流式数据,使用 STREAM 构造或等效的分段加密方案,不要试图用单次 AEAD 调用处理 GB 级别的数据。
注意生日界限制。AES-GCM 在同一密钥下加密的数据总量不应超过约 2^64 个块(约 256 EB),且消息总数不应超过 2^32 条(使用随机 nonce 时)。对于长期密钥,应当建立密钥轮换机制。
总结
AEAD 是现代密码学工程的基石。它将加密和认证统一为不可分割的原子操作,从根本上消除了手动组合加密和 MAC 时的种种陷阱。AES-GCM 以其卓越的硬件加速性能统治着服务器端部署;ChaCha20-Poly1305 以其天然的侧信道安全性和纯软件高性能在移动端和嵌入式领域占据一席之地;OCB 虽然拥有最优的理论效率却因专利问题错失了历史机遇;GCM-SIV 为 nonce 管理困难的场景提供了优雅的降级保障;Ascon 则将 AEAD 的保护延伸到了最受资源约束的设备。
理解这些方案不仅需要掌握它们的算法细节——GF(2^128) 乘法、Poly1305 的素数域算术、海绵构造的安全性分析——更需要深刻理解它们各自的设计哲学、安全假设和适用边界。密码学工程的核心挑战从来不是”选择最安全的算法”,而是”在特定约束条件下,选择最不可能被误用的方案”。AEAD 的出现和普及,正是这一工程哲学的最佳体现。
密码学百科系列 · 第 12 篇
← 上一篇:MAC 与 HMAC | 系列目录 | 下一篇:密钥派生函数 →