CMAC 消息认证码:基于分组密码的 MAC 构造与 NIST 标准
概述
CMAC(Cipher-based Message Authentication Code,基于分组密码的消息认证码)是一种使用分组密码构造消息认证码的标准方案。该方案由 NIST 在 SP 800-38B 中定义,是对早期 CBC-MAC 的安全增强版本。
CMAC 的核心价值在于:利用现有分组密码(如 AES、SM4)直接构造 MAC,无需额外哈希函数。这与 HMAC 形成对比——HMAC 需要密码哈希函数,而 CMAC 只需要分组密码。
┌─────────────────────────────────────────────────────────────┐
│ CMAC 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ 发送方: │
│ 1. 从主密钥派生子密钥 K1, K2 │
│ 2. 对消息分块进行 CBC 模式加密 │
│ 3. 最后一块使用特殊子密钥异或 │
│ 4. 输出最终密文块作为 MAC │
│ │
│ 接收方: │
│ 1. 同样派生子密钥 │
│ 2. 执行相同计算 │
│ 3. 恒定时间比较 MAC 值 │
└─────────────────────────────────────────────────────────────┘历史演进:从 CBC-MAC 到 CMAC
CBC-MAC 的缺陷
CBC-MAC 是最直观的 MAC 构造方式:将消息分成固定长度块,使用分组密码的 CBC 模式加密,取最后一块密文作为 MAC。
CBC-MAC 结构:
C₀ = IV(通常为 0)
C₁ = E(K, M₁ ⊕ C₀)
C₂ = E(K, M₂ ⊕ C₁)
...
Cₙ = E(K, Mₙ ⊕ Cₙ₋₁)
MAC = Cₙ致命问题:长度扩展攻击
对于固定长度消息,CBC-MAC 是安全的。但对于变长消息,攻击者可以:
- 已知有效消息 M 和 MAC = Cₙ
- 附加新块 M',构造 M || M'
- 计算新的 MAC = E(K, M' ⊕ Cₙ)
- 无需知道密钥即可伪造新消息的 MAC
ANSI X9.19:金融领域的早期方案
国际会计准则委员会(ANSI)在 X9.19 标准中提出了改进方案:使用双重密钥,最后一轮使用第二个密钥加密。这实际上是 DES-MAC 的基础。
NIST SP 800-38B:CMAC 标准化
2005 年,NIST 发布 SP 800-38B,正式定义 CMAC 为通用标准。CMAC 的关键创新:
- 子密钥派生:从主密钥派生两个子密钥 K₁ 和 K₂
- 最后一块处理:根据消息是否为完整块,使用不同的子密钥异或
- 安全性证明:在理想密码模型下证明安全性
算法结构
子密钥派生
CMAC 首先需要从主密钥派生两个子密钥。这通过加密全零块实现:
def generate_subkeys(key, block_encrypt):
"""
从主密钥派生子密钥 K1 和 K2
block_encrypt: 分组密码加密函数
"""
# 加密全零块得到 L
L = block_encrypt(key, b'\x00' * BLOCK_SIZE)
# 计算 K1
if L[0] & 0x80: # 最高位为 1,需要有限域约简
K1 = (L << 1) ^ Rb
else:
K1 = L << 1
K1 = K1 & (2**(BLOCK_SIZE*8) - 1) # 截断到块大小
# 计算 K2
if K1[0] & 0x80:
K2 = (K1 << 1) ^ Rb
else:
K2 = K1 << 1
K2 = K2 & (2**(BLOCK_SIZE*8) - 1)
return K1, K2有限域 GF(2ⁿ) 乘法解释:
子密钥派生本质上是有限域 GF(2¹²⁸) 上的乘法运算。Rb 是特定常数:
- AES-CMAC:Rb = 0x87(左移后异或)
- SM4-CMAC:同样使用 0x87
CMAC 计算流程
输入:密钥 K,消息 M(可变长度)
输出:MAC 标签 T
步骤:
1. 派生子密钥 K₁, K₂
2. 确定消息分块方式
3. 如果消息是块大小的整数倍:
最后一块与 K₁ 异或
否则:
最后一块补 1 后与 K₂ 异或
4. CBC 模式加密
5. 输出最后一块密文伪代码:
CMAC-Key generation: K ← {0,1}^k
Subkey generation: (K1, K2) ← KG(K)
CMAC processing:
if msg is empty:
B_n ← zeros(n) ⊕ K2
else:
pad = msg || 0x80 || zeros(8*n-1 - (|msg| mod n))
B_n ← last block of pad
B_0 ← zeros(n)
for i = 1 to n:
B_i ← E(K, B_{i-1} ⊕ B_i)
T ← B_nAES-CMAC 实例
AES 块大小为 128 位(16 字节),使用 AES 作为分组密码:
import aes_cipher # 假设的 AES 实现
def aes_cmac(key, message):
"""
AES-CMAC 实现(NIST SP 800-38B)
块大小:128 bit = 16 bytes
"""
BLOCK_SIZE = 16
Rb = 0x87 # AES-CMAC 的有限域常数
# 派生子密钥
L = aes_cipher.encrypt(key, b'\x00' * BLOCK_SIZE)
K1 = gf_mul(L, Rb) # 有限域乘法
K2 = gf_mul(K1, Rb)
# 分块处理
blocks = []
msg_len = len(message)
full_blocks = msg_len // BLOCK_SIZE
remainder = msg_len % BLOCK_SIZE
for i in range(full_blocks):
blocks.append(message[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE])
if remainder > 0:
# 最后一块补位并与 K2 异或
last_block = message[full_blocks*BLOCK_SIZE:] + b'\x80'
last_block += b'\x00' * (BLOCK_SIZE - remainder - 1)
last_block = xor(last_block, K2)
blocks.append(last_block)
else:
# 完整块与 K1 异或
last_block = xor(blocks[-1], K1)
blocks[-1] = last_block
# CBC 模式加密
prev = b'\x00' * BLOCK_SIZE
for block in blocks:
prev = aes_cipher.encrypt(key, xor(prev, block))
return prev安全性分析
安全性证明
CMAC 的安全性在理想密码模型下被证明。核心定理:
如果 E 是一个理想密码(理想置换),则 CMAC 是一个安全的伪随机函数(PRF),其优势界限为:$$\text{Adv}_{CMAC}^{prf}(A) \leq \frac{3q^2}{2^n}$$
其中 q 是攻击者的查询次数,n 是块大小(AES 为 128)。
这意味着:即使攻击者可以自适应地选择消息并获取 MAC,伪造新消息 MAC 的成功概率也仅为约 q²/2¹²⁸,可忽略不计。
与 CBC-MAC 的安全性对比
| 方案 | 变长消息安全性 | 安全性证明 | 实际部署 |
|---|---|---|---|
| CBC-MAC | ❌ 不安全 | 无 | 仅用于固定长度 |
| CMAC | ✅ 安全 | 已证明 | NIST 标准 |
| HMAC | ✅ 安全 | 已证明 | RFC 标准 |
已知攻击与限制
| 攻击类型 | 影响 | 防御 |
|---|---|---|
| 暴力破解密钥 | 尝试所有可能密钥 | 使用足够长的密钥(AES-128 及以上) |
| 侧信道攻击 | 通过功耗/时序泄露密钥 | 使用恒定时间实现 |
| 重合攻击 | 重用 nonce/IV | CMAC 不需要 nonce,但需防止重放 |
CMAC vs HMAC 对比
| 维度 | CMAC | HMAC |
|---|---|---|
| 基础原语 | 分组密码(AES、SM4) | 哈希函数(SHA-256、SM3) |
| 标准 | NIST SP 800-38B | RFC 2104, FIPS 198-1 |
| 性能(硬件加速) | 快(AES-NI 硬件支持) | 中等 |
| 性能(纯软件) | 较慢 | 较快 |
| 安全性假设 | 分组密码是 PRP | 哈希函数是理想预言机 |
| 密钥长度 | 与分组密码相同 | 推荐 ≥ 块大小 |
| 输出长度 | 等于块大小(128 bit) | 等于哈希输出长度 |
选型建议
选择 CMAC 的场景:
- 已有分组密码硬件加速(如 AES-NI)
- 需要与现有加密系统共享密钥材料
- 国密场景:SM4-CMAC
- 需要不同长度的 MAC(如 256 位 SHA-256)
- 已有哈希硬件加速
- 需要与 TLS、JWT 等现有协议兼容
国密场景:SM4-CMAC
在国密体系中,可以使用 SM4 分组密码构造 CMAC。SM4 块大小为 128 位,与 AES 相同。
from gmssl import sm4, func
def sm4_cmac(key, message):
"""
SM4-CMAC 实现
块大小:128 bit = 16 bytes
"""
BLOCK_SIZE = 16
# 派生子密钥
L = sm4.sm4_encrypt(key, b'\x00' * BLOCK_SIZE)
K1 = gf_mul_sm4(L)
K2 = gf_mul_sm4(K1)
# ... 后续处理同 AES-CMAC注意:目前 gmssl 库未直接提供 CMAC API,需要自行实现或使用其他密码库。
工程实现注意事项
陷阱 1:错误处理空消息
CMAC 应该能够处理空消息。根据 NIST 规范,空消息应视为一个全零块处理:
# 正确:处理空消息
if not message:
message = b'\x00' * BLOCK_SIZE陷阱 2:不正确的补位
最后一条消息的补位必须正确:
- 如果消息长度是块大小的整数倍,最后一块与 K₁ 异或
- 否则,补
0x80和零填充至块大小,与 K₂ 异或
# 错误的补位方式
last_block = message[-BLOCK_SIZE:] # 忽略补位规则
# 正确的补位方式
remainder = len(message) % BLOCK_SIZE
if remainder == 0:
last_block = xor(message[-BLOCK_SIZE:], K1)
else:
padding = b'\x80' + b'\x00' * (BLOCK_SIZE - remainder - 1)
last_block = xor(message[-remainder:] + padding, K2)陷阱 3:非恒定时间比较
必须使用恒定时间比较函数,防止时序攻击:
# 错误:提前退出比较
if computed_mac == received_mac:
accept()
# 正确:恒定时间比较
import hmac
if hmac.compare_digest(computed_mac, received_mac):
accept()相关实践
- HMAC-SM3 消息认证码实战 — HMAC 的国密实现
- 国密 API 签名验证 — HMAC 与 SM2 混合方案
- SM4 分组密码算法 — CMAC 所需的分组密码基础
标准引用
- NIST SP 800-38B: Recommendation for Block Cipher Modes of Operation: The CMAC Modes of Operation
- 官方文档: https://csrc.nist.gov/publications/detail/sp/800-38b/final
- FIPS 197: Advanced Encryption Standard(AES 标准,CMAC 的基础)
- 官方文档: https://csrc.nist.gov/publications/detail/fips/197/final
- GB/T 32907-2016: SM4 分组密码算法