消息认证码(Message Authentication Code,简称 MAC)是密码学中用于验证消息完整性(Integrity)和真实性(Authenticity)的关键技术。它确保消息在传输过程中未被篡改,并且确实来自持有共享密钥的可信发送方。MAC 是现代通信协议(TLS、SSH、IPsec)、支付系统、API 签名认证等领域的基础安全构件。
本文将系统性地介绍 MAC 的核心原理、主要算法族(HMAC、CMAC、GMAC)、安全性分析和实际工程应用,每种算法都配有具体的数值计算示例和代码实现。
一个 MAC 方案由三个算法组成:
- 密钥生成 Gen(1n)→k:输入安全参数 n,输出密钥 k
- 标签生成 Mac(k,m)→t:输入密钥 k 和消息 m,输出固定长度的认证标签 t
- 验证算法 Vrfy(k,m,t)→{接受,拒绝}:验证标签 t 是否与消息 m 和密钥 k 匹配
核心安全要求是计算不可伪造性(Existential Unforgeability under Chosen Message Attack,EUF-CMA):即使攻击者能够获取任意多条消息的合法 MAC 标签,也无法为新消息(未查询过的)伪造一个有效标签。
例如,Alice 要向 Bob 发送一条消息:"Transfer $1000 to Alice",她使用共享密钥 k 计算标签 t=Mac(k,"Transfer $1000 to Alice"),并将 (m,t) 一起发送给 Bob。Bob 收到后重新计算并验证 Vrfy(k,m,t) 是否通过。如果攻击者在途中将消息篡改为 "Transfer $10000 to Mallory",那么新计算的标签将不匹配,Bob 就能立刻发现。
| 特性 |
哈希函数 |
消息认证码 |
| 密钥 |
无密钥 |
需要共享密钥 |
| 安全性依赖 |
碰撞阻力 |
密钥保密性 + 伪随机性 |
| 攻击者知识 |
可公开计算 |
无密钥无法计算 |
| 能防止篡改? |
否(可同时篡改消息和哈希值) |
是 |
| 典型输出长度 |
128-512 bit |
128-256 bit |
哈希函数提供完整性,但不提供真实性——攻击者可以在篡改消息后重新计算新的哈希值。MAC 通过引入密钥解决了这一问题。
HMAC(Hash-based MAC)是应用最广泛的 MAC 构造,由 Mihir Bellare、Ran Canetti 和 Hugo Krawczyk 于 1996 年提出,被纳入 RFC 2104、FIPS PUB 198 和 NIST 标准。
HMAC 的核心思想是利用哈希函数的抗碰撞特性,通过两次哈希运算和一个密钥调度来构造安全的 MAC。其形式化定义为:
HMAC(k,m)=H((k′⊕opad)∥H((k′⊕ipad)∥m))
其中:
- H 是底层哈希函数(如 SHA-256)
- k′ 是经过处理的密钥:如果 k 长度大于块大小 B,则 k′=H(k);否则 k′=k 并在右侧补零至 B 字节
- ipad=0x36 重复 B 次(内填充)
- opad=0x5c 重复 B 次(外填充)
- ⊕ 表示按位异或
以 HMAC-SHA256 为例,块大小 B=64 字节(512 bit),输出长度 L=32 字节(256 bit)。
计算过程:
-
密钥处理:设密钥 k="my-secret-key-123"(18 字节),由于 18<64,在右侧补 46 个零字节得到 k′(64 字节)
-
内层哈希:
- k′⊕ipad:将 k′ 的每个字节与 0x36 异或
- 拼接消息:(k′⊕ipad)∥m
- 计算:H((k′⊕ipad)∥m)
-
外层哈希:
- k′⊕opad:将 k′ 的每个字节与 0x5c 异或
- 拼接内层结果:(k′⊕opad)∥inner_hash
- 计算:H((k′⊕opad)∥inner_hash)
Python 实现验证:
import hmac
import hashlib
key = b"my-secret-key-123"
message = b"Transfer $1000 to Alice"
tag = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f"HMAC-SHA256: {tag}")
# 输出示例(因密钥和消息固定):6a4b3c8d...
HMAC 的安全证明依赖于底层哈希函数的伪随机函数(PRF)属性。具体来说:
- 即使底层哈希函数在无密钥情况下存在碰撞攻击(如 MD5、SHA-1 已被攻破),HMAC 构造本身仍能保持一定的安全性。例如,虽然 SHA-1 已被 Google 的 SHAttered 攻击打破碰撞阻力(2007 年,Google 展示了 9.2×1018 次哈希运算的碰撞攻击),但 HMAC-SHA1 至今未被有效攻破
- HMAC 的安全性还与密钥长度直接相关。下表展示了不同密钥长度下的安全强度:
| 底层哈希 |
输出长度 |
密钥建议长度 |
安全强度 |
| HMAC-SHA1 |
160 bit |
≥128 bit |
80 bit |
| HMAC-SHA256 |
256 bit |
≥128 bit |
128 bit |
| HMAC-SHA384 |
384 bit |
≥192 bit |
192 bit |
| HMAC-SHA512 |
512 bit |
≥256 bit |
256 bit |
安全强度 = min(密钥熵,输出长度/2)。对于 HMAC-SHA256,最佳攻击方式是暴力搜索 128 位密钥(2128 次操作),这在当前算力下不可行。
CMAC(Cipher-based MAC)是基于分组密码(如 AES)的 MAC 构造。它取代了旧标准 CBC-MAC,解决了 CBC-MAC 在处理变长消息时的安全缺陷。CMAC 由 NIST SP 800-38B 和 RFC 4493 标准化。
CBC-MAC 使用分组密码的 CBC 模式直接计算 MAC:将消息分组后,逐组使用前一组密文作为 IV,最后一组密文作为 MAC 标签。
安全缺陷:对于变长消息存在伪造攻击。
攻击示例:
假设分组大小为 128 bit(AES),密钥 k。CBC-MAC 定义为 M(m)=CBC-MAC(k,m)。
- 攻击者获取 t1=M(m1),其中 m1 是一个分组(128 bit)
- 攻击者构造 m2=m1∥(m1⊕t1)
- 可以证明 M(m2)=t1 —— 攻击者伪造了一对新消息的有效标签!
更具体地:
- 设 m1 为单分组消息,CBC-MAC 计算过程为:c1=Ek(m1),标签为 t1=c1
- 对于 m2=m1∥(m1⊕t1):
- 第一组:c1=Ek(m1)=t1
- 第二组:c2=Ek((m1⊕t1)⊕c1)=Ek(m1⊕t1⊕t1)=Ek(m1)=t1
- 标签 = c2=t1
因此攻击者无需知道密钥即可为 m2 伪造标签。这便是 CBC-MAC 的致命缺陷。
CMAC 通过引入两个子密钥 K1 和 K2 解决了 CBC-MAC 的问题。其核心思想是在处理最后一个分组时,根据消息长度是否对齐,使用不同的子密钥进行异或操作,从而消除上述变长攻击。
子密钥生成(AES-128 为例):
- 计算 L=Ek(0128) —— 加密全零块
- 定义 R=012010000111(0x87,对应 AES 的不可约多项式 x128+x7+x2+x+1)
- K1=dbl(L):将 L 左移 1 位,如果最高位为 1 则异或 R
- K2=dbl(K1):同上
其中 dbl(x) 操作定义为:
def dbl(x):
"""Multiply by 2 in GF(2^128)"""
x = x << 1
if x & (1 << 128): # 最高位溢出
x ^= 0x100000000000000000000000000000087
return x & ((1 << 128) - 1)
MAC 计算:
- 如果最后一个分组完整(消息长度 ∣m∣ 是 16 的倍数),使用 K1 异或后加密
- 如果最后一个分组不完整,在末尾补
10...0(1 字节 0x80 + 若干 0x00),使用 K2 异或后加密
这种设计使得长度对齐和未对齐的消息产生完全不同的 MAC 值,从根本上阻止了变长伪造攻击。
参数:
- 算法:AES-CMAC(AES-128)
- 密钥:k=0x2b7e151628aed2a6abf7158809cf4f3c
- 消息:m=0x6bc1bee22e409f96e93d7e117393172a
步骤 1:计算 L
L=Ek(0128)=0x7df76b0c1ab899b33e42f047b91b546f
步骤 2:计算 K1 和 K2
K1=dbl(L)=0xfbeed618354c33667ca85e08f7236a9e(左移后最高位未溢出,不需异或 R)
K2=dbl(K1)=0xf7ddac306a9866ccf950bc11ee46d53c
步骤 3:计算最后一个分组
消息恰好 16 字节(完整),因此:
last_block=m⊕K1=0x6bc1bee22e409f96⊕0xfbeed618354c3366
计算结果:0x902f68fa1b0ccaf0
步骤 4:加密得到 MAC
t=Ek(last_block)=0x070a16b46b4d4144f79bdd9dd04a287c
最终 HMAC-SHA256 与 AES-CMAC 对比:
算法HMAC-SHA256AES-CMAC输出长度32 字节16 字节底层基元SHA-256 哈希AES-128 分组密码
GMAC(Galois MAC)是 AES-GCM(Galois/Counter Mode)认证加密方案中的认证组件。它利用 GF(2128) 上的有限域乘法和计数器模式实现对密文的认证。
GMAC 将认证视为一个多项式求值问题。设 GHASH 函数为:
GHASH(H,A,C)=Xm+n+1
其中:
- H=Ek(0128) 是哈希子密钥
- A 是附加认证数据(Additional Authenticated Data, AAD)
- C 是密文
- 计算过程为在 GF(2128) 域上对消息块进行多项式求值
具体而言,将 A 和 C 分别补齐为 16 字节的倍数后拼接,每块作为多项式系数,在 H 点处求值:
Xi=(Xi−1⊕blocki)⋅H
这里 ⋅ 是在 GF(2128) 上的乘法,使用不可约多项式 x128+x7+x2+x+1(即前面提到的 0x87,注意此处与 AES-CMAC 使用的是同一个域)。
最终 GMAC 标签为:
T=GHASH(H,A,C)⊕Ek(nonce∥1)
假设 H=0x66e94bd4ef8a2c3b884cfa59ca342b2e(AES-128 加密全零块的典型输出),认证数据 A 为空,密文 C 只有一个 128-bit 块:
C=0x0388dace60b6a392b3b4f30e340e1bb6
则 GHASH 计算:
X1=0x0388dace60b6a392b3b4f30e340e1bb6⋅0x66e94bd4ef8a2c3b884cfa59ca342b2e
这个乘法在 GF(2128) 上展开为 128 轮移位和异或操作(类似于二进制乘法但使用多项式约简)。最终结果与计数器模式加密输出异或得到最终标签。
发送方:
┌─────────────┐
nonce ──────────→│ CTR 模式 │──→ 密文块序列
│ 加密 │
plaintext ──────→│ │──→ 密文 C
└──────┬──────┘
│
┌──────▼──────┐
AAD ────────────→│ GHASH │──→ auth tag T
C ──────────────→│ (GF 乘法) │
└─────────────┘
输出:C || T (密文 + 认证标签)
AES-GCM 是当前最高效的认证加密方案之一,支持流水线执行(认证和加密可并行处理),在 Intel 处理器上有 AES-NI + PCLMULQDQ 硬件指令加速。
MAC 的常见用途是构造认证加密(Authenticated Encryption with Associated Data, AEAD)方案。AEAD 同时保证保密性和真实性。
| 方案 |
构造方式 |
性能 |
特点 |
| AES-GCM |
CTR + GMAC |
极高(硬件加速) |
最常用,但 nonce 重用的后果是灾难性的 |
| ChaCha20-Poly1305 |
Stream + Poly1305 |
高(无硬件加速也快) |
Google 在移动设备上推广,无专利限制 |
| AES-CCM |
CTR + CBC-MAC |
较低 |
旧标准,不支持流水线 |
| AES-OCB |
专用 OCB 模式 |
极高 |
有专利限制 |
Nonce 重用攻击实例(GCM):
如果使用相同的 nonce 和密钥加密两条不同消息 m1 和 m2,攻击者可以恢复出哈希子密钥 H:
- 得到两条消息的密文 (C1,T1) 和 (C2,T2)
- 由于 T1⊕T2=GHASH(H,A,C1)⊕GHASH(H,A,C2)
- 通过代数方法求解 H(一组 AEAD 方程可通过 GCM-nonce-reuse 攻击工具自动化完成,耗时只需几秒)
- 一旦 H 泄露,攻击者可以伪造任意消息的有效标签
因此,使用 GCM 时必须确保每个 nonce 在相同密钥下只使用一次。 实践中通常使用 96-bit 随机数作为 nonce,碰撞概率在 248 条消息后才超过 10−9。
ChaCha20-Poly1305 是 RFC 8439 标准化的 AEAD 方案,结构如下:
- ChaCha20 流密码:生成密钥流 KS=ChaCha20(k,nonce,counter)
- 加密:ciphertext=plaintext⊕KS
- Poly1305 MAC:在 GF(2130−5) 域上计算多项式,认证密文和 AAD
Poly1305 被称为"地球上最快的 MAC"之一,因为它使用大素数域 p=2130−5 上的多项式求值,避免了二进制域乘法的位操作开销:
T=(i=0∑n(blocki⋅ri+1)+s)mod2128
其中 r 和 s 是子密钥,r 的低 4 位被清零以保证安全性。
现代 API 广泛使用 HMAC 进行请求签名。亚马逊 AWS Signature V4 是典型例子:
import hmac
import hashlib
import datetime
def sign_aws_request(secret_key, region, service, date_stamp):
"""简化版 AWS Signature V4 签名"""
# 1. 创建签名密钥(分阶段 HKDF 派生)
k_date = hmac.new(f"AWS4{secret_key}".encode(),
date_stamp.encode(), hashlib.sha256).digest()
k_region = hmac.new(k_date, region.encode(), hashlib.sha256).digest()
k_service = hmac.new(k_region, service.encode(), hashlib.sha256).digest()
k_signing = hmac.new(k_service, b"aws4_request", hashlib.sha256).digest()
# 2. 对待签名字符串进行 HMAC
string_to_sign = "AWS4-HMAC-SHA256\n..." # 规范请求
signature = hmac.new(k_signing, string_to_sign.encode(),
hashlib.sha256).hexdigest()
return signature
TLS 1.3 使用 HMAC 构建密钥派生函数 HKDF(RFC 5869),其核心结构如下:
IKM (初始密钥材料)
│
▼
HKDF-Extract(salt, IKM) = HMAC(salt, IKM)
│
▼
PRK (伪随机密钥)
│
▼
HKDF-Expand(PRK, info, L) = HMAC(PRK, info || 0x01) ||
HMAC(PRK, info || 0x02) || ...
│
▼
派生密钥
具体来说,TLS 1.3 的握手过程需要 5 次 HKDF 派生,每次使用不同的 info 标签:
| 阶段 |
输入 |
输出 |
| PSK 派生 |
PSK + (EC)DHE 共享密钥 |
主密钥 |
| 握手流量密钥 |
主密钥 + 握手上下文 |
握手加密密钥 |
| 应用流量密钥 |
主密钥 + finished 消息 |
应用层加密密钥 |
| 恢复密钥 |
主密钥 + 会话信息 |
0-RTT 恢复密钥 |
在支付系统中,MAC 用于保护交易数据的完整性:
- ISO 8583/20022 消息认证:金融报文使用 MAC 确认交易字段未被篡改,通常使用 DES MAC(ANSI X9.9)或 AES MAC
- 卡数据保护:3DS 协议中,目录服务器使用 MAC 保护验证响应(VERes/PAReq 签名)
- 退款防伪造:退款请求使用 MAC 签名,防止用户伪造退款指令
- API 请求认证:支付 API 的 Webhook 回调使用 HMAC 验证来源真实性
MAC 的安全性最终取决于密钥管理的质量:
| 最佳实践 |
说明 |
| 密钥长度 |
至少 128 bit,推荐 256 bit |
| 密钥派生 |
使用 HKDF 从主密钥派生每个会话的子密钥,避免直接使用原始密钥 |
| 定期轮换 |
80-bit 安全密钥每 2 年轮换一次;128-bit 密钥每 5 年轮换一次 |
| 密钥分类 |
不同用途使用不同密钥(加密 MAC 密钥分离,即 separate encryption and MAC keys) |
| 侧信道防护 |
使用常数时间比较函数验证 MAC(如 Python 的 hmac.compare_digest),避免时序攻击 |
# ❌ 不安全的比较方式(可能被时序攻击利用)
if computed_tag == received_tag: # 逐字节比较,不匹配即返回
pass
# ✅ 安全的常数时间比较
import hmac
if hmac.compare_digest(computed_tag, received_tag):
pass # 固定时间,不泄露不匹配位置
MAC 本身不提供时序保护。一个有效的 MAC 标签可以被攻击者截获后重复使用:
场景: Alice 发送 "Transfer $1000 to Alice" 的签名消息,攻击者 Bob 截获后立即重新发送同一份消息。银行收到两次相同消息(均带有有效 MAC),会执行两次转账。
防御方案:
| 防御手段 |
实现方式 |
| Nonce/计数器 |
消息中包含递增序列号,验证方记录已使用的序列号 |
| 时间戳 |
消息中包含 UTC 时间戳,验证方拒绝超出时间窗口的消息(如 ±5 分钟) |
| 挑战-响应 |
验证方先发送随机挑战 nonce,签名方将 nonce 包含在 MAC 计算中 |
例如,支付系统可以在消息体中加入 timestamp=1680000000 和 nonce=a1b2c3d4,MAC 计算覆盖这些字段:
message = {
"amount": 1000,
"currency": "USD",
"to_account": "alice@bank.com",
"timestamp": 1680000000,
"nonce": "a1b2c3d4"
}
# MAC 计算覆盖整个消息(包括 timestamp 和 nonce)
tag = hmac.new(key, json.dumps(message, sort_keys=True).encode(), hashlib.sha256).hexdigest()
适用对象: 使用 MD5、SHA1、SHA256 等 Merkle-Damgård 构造的哈希函数构建的未经过安全处理的 MAC,如直接拼接 H(k∥m)。
攻击原理:
对于 Merkle-Damgård 哈希,攻击者知道 H(k∥m) 后,可以在无需知道 k 的情况下计算出 H(k∥m∥pad∥m′),从而伪造新消息的"MAC"。
示例:
- 原始签名消息:H(k∥"Transfer $1000 to Alice")
- 攻击者构造:H(k∥"Transfer $1000 to Alice" || pad || " now Transfer $10000 to Bob")
- 无需知道 k,攻击者即可得到新消息的 MAC
HMAC 的防御:
HMAC 使用 H((k′⊕opad)∥H((k′⊕ipad)∥m)) 的双重哈希结构,彻底避免了长度扩展攻击。
| MAC 方案 |
抗长度扩展? |
备注 |
| H(k∥m) |
❌ 易攻击 |
永远不要使用 |
| H(m∥k) |
❌ 易攻击(有特定条件) |
不推荐 |
| HMAC |
✅ 安全 |
标准方案 |
| CMAC |
✅ 安全(不基于哈希) |
分组密码方案 |
如果 MAC 验证使用不安全的比较方式(逐字节提前返回),攻击者可以通过精确测量响应时间逐字节恢复出正确标签:
单字节猜对概率: 1/256
攻击复杂度: 对于 32 字节的 HMAC-SHA256,平均需要 32×128=4096 次尝试
使用 HMAC 安全的常数时间比较(如 Python 的 hmac.compare_digest,Go 的 hmac.Equal,Rust 的 constant_time_eq)可以完全防御此攻击。
| 场景 |
推荐方案 |
原因 |
| 通用数据完整性 |
HMAC-SHA256 |
最广泛支持,无专利问题 |
| 高速硬件环境 |
AES-CMAC 或 AES-GCM |
Intel AES-NI 指令加速 |
| 移动/嵌入式设备 |
HMAC-SHA256 或 ChaCha20-Poly1305 |
无硬件加速也高性能 |
| 金融/支付系统 |
AES-CMAC(DES MAC) |
行业标准(ISO 8583) |
| 流媒体/大文件 |
HMAC-SHA256(分块更新) |
支持增量计算 |
| 网络协议(TLS/SSH) |
AEAD(GCM 或 Poly1305) |
认证加密一体化 |
# HMAC 增量计算示例(适用于大文件)
import hashlib, hmac
def hmac_file(filepath, key, chunk_size=8192):
h = hmac.new(key, digestmod=hashlib.sha256)
with open(filepath, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk) # 增量更新
return h.hexdigest()
消息认证码是密码学工具箱中验证数据完整性和真实性的核心工具。以下是核心知识点总结:
- MAC 解决哈希的不足:哈希提供完整性但缺乏真实性;MAC 通过共享密钥弥补这一缺陷
- HMAC 是首选方案:安全证明完善,标准广泛支持,推荐 HMAC-SHA256
- CMAC/GMAC 用于高速场景:底部基于分组密码,适合硬件加速环境
- AEAD 是更高层抽象:GCM 和 ChaCha20-Poly1305 将加密和认证合二为一,是 TLS 1.3 的默认选择
- Nonce 管理关乎生死:GCM 等方案中 nonce 重用会导致灾难性安全崩溃
- 密钥管理不可忽视:使用 HKDF 派生密钥、定期轮换、分类使用不同密钥
- 常数时间比较是底线:验证 MAC 时必须使用安全比较函数
- RFC 2104 — HMAC: Keyed-Hashing for Message Authentication
- NIST SP 800-38B — Recommendation for Block Cipher Modes of Operation: The CMAC Mode for Authentication
- NIST SP 800-38D — Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC
- RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols
- RFC 5869 — HKDF: HMAC-based Key-Derivation Function
- Bellare et al. — "Keying Hash Functions for Message Authentication" (1996, Crypto)
- 参见:对称加密 | 哈希函数 | 数字签名