CMAC 消息认证码:基于分组密码的 MAC 构造与 NIST 标准

算法原理 · 2026-10-27

概述

CMAC(Cipher-based Message Authentication Code,基于分组密码的消息认证码)是一种使用分组密码构造消息认证码的标准方案。该方案由 NIST 在 SP 800-38B 中定义,是对早期 CBC-MAC 的安全增强版本。

CMAC 的核心价值在于:利用现有分组密码(如 AES、SM4)直接构造 MAC,无需额外哈希函数。这与 HMAC 形成对比——HMAC 需要密码哈希函数,而 CMAC 只需要分组密码。

历史演进:从 CBC-MAC 到 CMAC

CBC-MAC 的缺陷

CBC-MAC 是最直观的 MAC 构造方式:将消息分成固定长度块,使用分组密码的 CBC 模式加密,取最后一块密文作为 MAC。

CODE
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
这是因为 CBC-MAC 的输出直接可作为下一块的初始化向量。

ANSI X9.19:金融领域的早期方案

国际会计准则委员会(ANSI)在 X9.19 标准中提出了改进方案:使用双重密钥,最后一轮使用第二个密钥加密。这实际上是 DES-MAC 的基础。

NIST SP 800-38B:CMAC 标准化

2005 年,NIST 发布 SP 800-38B,正式定义 CMAC 为通用标准。CMAC 的关键创新:

  • 子密钥派生:从主密钥派生两个子密钥 K₁ 和 K₂
  • 最后一块处理:根据消息是否为完整块,使用不同的子密钥异或
  • 安全性证明:在理想密码模型下证明安全性

算法结构

子密钥派生

CMAC 首先需要从主密钥派生两个子密钥。这通过加密全零块实现:

有限域 GF(2ⁿ) 乘法解释:

子密钥派生本质上是有限域 GF(2¹²⁸) 上的乘法运算。Rb 是特定常数:

  • AES-CMAC:Rb = 0x87(左移后异或)
  • SM4-CMAC:同样使用 0x87
这种设计确保了 K₁ 和 K₂ 在统计上独立于主密钥。

CMAC 计算流程

CODE
输入:密钥 K,消息 M(可变长度)
输出:MAC 标签 T

步骤:
1. 派生子密钥 K₁, K₂
2. 确定消息分块方式
3. 如果消息是块大小的整数倍:
     最后一块与 K₁ 异或
   否则:
     最后一块补 1 后与 K₂ 异或
4. CBC 模式加密
5. 输出最后一块密文

伪代码:

AES-CMAC 实例

AES 块大小为 128 位(16 字节),使用 AES 作为分组密码:

安全性分析

安全性证明

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/IVCMAC 不需要 nonce,但需防止重放
重要:CMAC 不需要 nonce(随机数),这与 GCM 等 AEAD 模式不同。每次计算都是确定性的,相同的密钥和消息总是产生相同的 MAC。

CMAC vs HMAC 对比

维度CMACHMAC
基础原语分组密码(AES、SM4)哈希函数(SHA-256、SM3)
标准NIST SP 800-38BRFC 2104, FIPS 198-1
性能(硬件加速)快(AES-NI 硬件支持)中等
性能(纯软件)较慢较快
安全性假设分组密码是 PRP哈希函数是理想预言机
密钥长度与分组密码相同推荐 ≥ 块大小
输出长度等于块大小(128 bit)等于哈希输出长度

选型建议

选择 CMAC 的场景:

  • 已有分组密码硬件加速(如 AES-NI)
  • 需要与现有加密系统共享密钥材料
  • 国密场景:SM4-CMAC
选择 HMAC 的场景:
  • 需要不同长度的 MAC(如 256 位 SHA-256)
  • 已有哈希硬件加速
  • 需要与 TLS、JWT 等现有协议兼容

国密场景:SM4-CMAC

在国密体系中,可以使用 SM4 分组密码构造 CMAC。SM4 块大小为 128 位,与 AES 相同。

注意:目前 gmssl 库未直接提供 CMAC API,需要自行实现或使用其他密码库。

工程实现注意事项

陷阱 1:错误处理空消息

CMAC 应该能够处理空消息。根据 NIST 规范,空消息应视为一个全零块处理:

PYTHON
# 正确:处理空消息
if not message:
    message = b'\x00' * BLOCK_SIZE

陷阱 2:不正确的补位

最后一条消息的补位必须正确:

  • 如果消息长度是块大小的整数倍,最后一块与 K₁ 异或
  • 否则,补 0x80 和零填充至块大小,与 K₂ 异或
PYTHON
# 错误的补位方式
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:非恒定时间比较

必须使用恒定时间比较函数,防止时序攻击:

PYTHON
# 错误:提前退出比较
if computed_mac == received_mac:
    accept()

# 正确:恒定时间比较
import hmac
if hmac.compare_digest(computed_mac, received_mac):
    accept()

相关实践

标准引用

  • 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 分组密码算法