想象你开了一家网店,每天寄出几十个包裹。你担心两件事:第一,包裹在运输途中被人拆开调包;第二,有人冒充你给老客户寄假货。快递公司说:“我们帮你加个封条吧!”——但普通的塑料封条一撕就开,胶带一揭就能重新贴上。你需要一种特殊封条:它由你和收件人共同约定的秘密材料制成,任何人没有这份秘密材料,都无法伪造出一模一样的封条;而只要封条被碰过,收件人一眼就能看出破绽。 这就是消息认证码(MAC)的本质。在数字世界里,它给每条消息贴上一张“防伪封条”,让接收方既能确认消息没被改动(完整性),又能确认它确实来自声称的发送者(真实性)。而HMAC,是其中最著名的一种“封条制作工艺”——它用一种叫做“哈希”的机器,把秘密材料和消息搅在一起,产出这张独一无二的封条。本文会用快递包裹的比喻,带你从头理解MAC与HMAC的工作原理、工程实现,以及它们如何默默守护着每一次网络通信。

岗位关联

消息认证是密码学体系的安全基石,掌握MAC/HMAC的设计思想,是理解TLS、JWT、云安全签名等现代安全体系的第一步,也是安全工程师与密码工程开发的必备核心技能。

目录
  1. 可视化演示
  2. MAC基本概念
  3. 安全设计要求
  4. HMAC嵌套构造
  5. CBC-MAC与CMAC
  6. 长度扩展攻击
  7. AEAD认证加密
  8. 主流应用场景
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

消息认证码MAC保证消息完整性与真实性,HMAC是基于哈希的MAC标准。本文详解MAC的工作原理与安全要求、H... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

MAC基本概:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

MAC基本概念

你寄出的每个快递包裹上,都贴着一张由特殊材料制成的封条。这张封条不是随便贴上去的——它是由你和收件人事先约定好的一种“秘密配方”材料压制而成。没有这个配方的人,即使看到了你封条的样子,也做不出一模一样的。这正是消息认证码(Message Authentication Code, MAC)的核心思想:用共享密钥给消息生成一段固定长度的“指纹”标签。

在密码学中,MAC算法接收两个输入——秘密密钥K和任意长度的消息M——输出一个固定长度的标签T,记作 T = MAC(K, M)。发送方把“消息+标签”一起发给接收方,接收方用同一个密钥K重新计算MAC,如果结果一致,就能确认:消息没被改动过(完整性),而且确实来自持有密钥的发送方(真实性)。

MAC和哈希函数看似都在计算“摘要”,但关键区别在于:普通哈希(如SHA-256)不需要密钥,任何人都能计算;而MAC必须持有密钥才能计算和验证。这就像普通的封条任何人都能买到,但你的特殊封条只有你俩才有。与数字签名相比,MAC用的是对称密钥(双方共享同一个密钥),而数字签名用公私钥对,能提供“不可否认性”——这在后续的区块链和身份认证中尤为关键。

安全设计要求

什么样的封条才算好封条?首先,造假的成本要极高——如果别人看一眼你的封条就能仿制,那这封条毫无意义。密码学中这一性质叫计算不可伪造性:即使攻击者见过大量“消息-标签”对,也无法在不知道密钥K的情况下,为一条新消息计算出合法的MAC标签。

其次,密钥必须保密。封条材料的“秘密配方”一旦泄露,整个认证体系就崩塌了。这听起来理所当然,但在工程实践中,密钥管理往往是最薄弱的环节——硬编码密钥、日志中打印密钥、密钥长期不轮换,都是常见的致命错误。

另外,MAC的标签长度要足够(如HMAC-SHA256输出256位),防止暴力碰撞攻击;而且无论消息多长,输出长度应固定,方便协议使用。最后,MAC必须具备“可验证可复算”特性:接收方用同一算法、同一密钥、同一消息,必须能得出完全一致的标签,任何一位的差异都意味着消息被篡改。

HMAC嵌套构造

我们已经有了“封条材料”(密钥)和“封条样式”(MAC算法),但如何用现成的哈希机器(如SHA-256)来制作封条?这里有一段精妙的“双倍封装”工艺。想象一个生产线:先用外层材料(ipad)包裹密钥与消息搅拌一次,产出一个中间半成品,再换另一种外层材料(opad)包裹这个半成品再搅拌一次,最终固化成型的封条。这就是HMAC(Hash-based Message Authentication Code)的嵌套哈希结构。

HMAC的标准定义如下:

这种“双重哈希”设计不是多余的——它让攻击者即便利用哈希函数的某种数学特性(如长度扩展),也无法在不掌握密钥的情况下伪造MAC。HMAC已被RFC 2104标准化,是TLS、SSH、IPsec等协议中最广泛使用的MAC方案。

\displaystyleHMAC(K, M) = H\Big((K' \oplus opad) \,\|\, H\big((K' \oplus ipad) \,\|\, M\big)\Big)
ipad和opad如同两种不同的“胶水配方”,让同一个哈希函数通过内外两次不同方式的混合,达到“无密钥无法预测”的安全效果。
  1. 密钥预处理如果密钥K比哈希块长,先对K做一次哈希压缩,得到标准长度密钥K';如果K较短,右侧补零至块长度(如SHA-256的块长为64字节)。
    \displaystyleK' = \begin{cases} H(K) & \text{if } |K| > b \\ K \,\|\, 0^{b-|K|} & \text{otherwise} \end{cases}
  2. 内层异或将K'与固定的内层填充常量ipad(块长字节的0x36重复)逐字节异或,得到内层密钥流。
    \displaystyleS_{inner} = K' \oplus ipad
  3. 内层哈希把内层密钥流拼接在消息M前面,送入哈希函数H,得到中间摘要。
    \displaystyleH_{inner} = H(S_{inner} \,\|\, M)
  4. 外层异或将K'与另一个固定常量opad(块长字节的0x5C重复)异或,得到外层密钥流。
    \displaystyleS_{outer} = K' \oplus opad
  5. 外层哈希把外层密钥流拼接在中间摘要前面,再次送入哈希函数,得到最终HMAC标签。
    \displaystyleHMAC(K, M) = H(S_{outer} \,\|\, H_{inner})

CBC-MAC与CMAC

如果手头没有哈希函数,只有AES这样的分组加密机器,能不能做封条?可以,方法就像把多个箱子串联起来:每个箱子的锁扣由上一个箱子传下来的链条绑定,最终把所有箱子串成一条不可断开的链。这就是CBC-MAC(Cipher Block Chaining Message Authentication Code)的思路——用分组密码的链式模式计算MAC。

具体而言,把消息分成固定长度的分组,第一个分组与初始向量(通常为0)异或后加密,得到密文块;第二个分组与这个密文块异或后再加密……如此串联,最后一个密文块就是MAC标签。然而,这种朴素方案存在著名的安全缺陷:如果消息长度可变,攻击者可以通过“拼接两个已知消息的标签块”构造出第三条消息的合法MAC。这就像两个合法封条的碎片被拼在一起,竟然组成了一个新的有效封条。

CMAC(Cipher-based MAC)修复了这个漏洞:它引入两个由密钥派生的子密钥K1和K2,根据最后一个分组是否需要填充,选择异或K1或K2后再加密。这个“分支区别”机制让攻击者的拼接攻击彻底失效。CMAC已成为NIST推荐标准,特别适合硬件加速环境或资源受限的IoT设备——因为它只需复用已有的AES硬件,不必额外实现哈希模块。

长度扩展攻击

设想一种危险情形:你寄了一份合同,用旧式“哈希拼接”方案做封条——即标签 = H(密钥‖消息)。攻击者虽然不知道密钥,但他知道哈希函数的内部状态,可以像“在已写完的合同后用同一支笔继续续写内容”一样,在消息末尾追加任意数据,并计算出对应的新标签。这就是长度扩展攻击(Length Extension Attack),它让SHA-256这类Merkle-Damgård结构的哈希函数在“密钥拼接”场景下彻底失守。

HMAC之所以免疫这种攻击,关键在于那双层结构:内层哈希的输出被外层哈希“重新包裹”了一遍,攻击者拿到的只是外层哈希的最终结果,无法从中间状态推断出密钥参与运算的细节。这就好比封条不仅盖了章,还在印章上面覆了一层透明保护膜,任何“续写”尝试都会破坏保护膜。

防御策略有三条:使用HMAC替代朴素拼接;切换到不受长度扩展影响的哈希算法(如SHA-3/Keccak的 sponge 结构天然免疫);或者用带密钥的专用构造(如KMAC)。在实际工程中,HMAC+SHA-256仍是黄金组合,而SHA-3系算法正在逐步进入新一代安全协议。

AEAD认证加密

前面我们把加密(保密性)和MAC(完整性/真实性)分开讨论,但在真实通信中,两者必须同时使用。问题随之而来:先用哪个?先加密再认证(Encrypt-then-MAC)是推荐方案——先给货物上锁(加密),再在锁好的箱子上贴封条(MAC),收件人先验封条再开锁,能避免很多配合不当引入的漏洞。

AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)将加密和认证融为一体,一次性完成机密性、完整性、真实性三重保护。其中最著名的是GCM模式(Galois/Counter Mode),它用计数器模式加密数据,同时用GHASH(一种高效的通用哈希)计算认证标签。GCM只需一次遍历即可同时输出密文和标签,性能极高,因此在TLS 1.3中,AEAD已成为唯一的加密套件选择——明文不支持,任何不提供认证的加密模式都被禁止。

工程选型建议:新系统优先选择AEAD算法(AES-GCM或ChaCha20-Poly1305);不要自行拼凑“加密+MAC”组合;如果必须组合,使用经过验证的Encrypt-then-MAC顺序,并确保使用独立的密钥。

主流应用场景

理论讲完了,我们看看“封条”在真实世界中的广泛应用。首先是TLS协议:你在浏览器地址栏看到的那把小锁,就是TLS握手时用HMAC或AEAD保护数据传输的体现。微信、支付宝、网银的每一次通信,背后都有MAC在默默验证每条消息的完整性。

其次是JWT令牌:当你在网站登录后拿到一串长长的token,其中签名部分用的就是HMAC-SHA256——服务端用密钥验证token没被篡改,你无法伪造一个“管理员”身份。这就像快递封条上印的专属编号,收货人一看就知道真假。

还有云存储完整性:对象存储服务(如AWS S3)对上传的文件计算MAC标签,下载时验证标签,确保文件在云端未被篡改。而在支付与IoT设备中,每笔交易指令、每个传感器上报的数据包都带MAC标签,防止中间人注入伪造指令。可以说,凡是有数据传递的地方,就有MAC在守护。

AI 应用场景

场景一:大模型API请求的完整性保护

阶段:模型服务安全与调用链保护

解决的问题:企业调用大模型API时,请求参数(提示词、温度等)和响应结果在网络传输中可能被篡改。传统方式只做HTTPS加密,无法防止合法用户修改请求后重放。采用HMAC对请求体签名,服务端验签后执行,可确保请求未被第三方篡改。

具体案例:某AI中台为内部多个业务线提供大模型接口,每个业务线分配独立API Key。请求头携带X-HMAC-Signature: HMAC-SHA256(secret, timestamp + body),服务端验签并校验时间戳防止重放攻击,两周内拦截了数百次异常调用。

场景二:模型训练数据集的完整性校验

阶段:数据准备与管理

解决的问题:训练数据集在传输或存储过程中被污染(如被注入错误标签)会严重影响模型效果。传统校验只有哈希值,无法验证数据来源。用HMAC为每个数据分片生成认证标签,可在发现数据异常时快速定位并提供来源认证。

具体案例:某自动驾驶团队从多个数据供应商获取路测数据,用HMAC-SHA256对每个数据包签名,接收端验签不通过的数据自动隔离告警。上线一个月,成功拦截了3起供应商数据篡改事件,保障了训练数据可信度。

Python 演示

python44 行
import hashlibimport hmacimport os  def simple_hmac_sha256(key: bytes, message: bytes) -> bytes:    """使用 hashlib 从零实现 HMAC-SHA256(教学用)"""    block_size = 64  # SHA-256 的块大小(字节)     # 1. 密钥预处理:过长则哈希压缩,过短则右侧补零    if len(key) > block_size:        key = hashlib.sha256(key).digest()    key = key.ljust(block_size, b'\x00')     # 2. 生成 ipad 与 opad 密钥流    ipad = bytes(b ^ 0x36 for b in key)    opad = bytes(b ^ 0x5C for b in key)     # 3. 内层哈希:H(K' XOR ipad || message)    inner = hashlib.sha256(ipad + message).digest()     # 4. 外层哈希:H(K' XOR opad || inner)    return hashlib.sha256(opad + inner).digest()  def verify_mac(key: bytes, message: bytes, received_tag: bytes) -> bool:    """验证消息认证码:重算比对,使用 hmac.compare_digest 防时序攻击"""    expected_tag = simple_hmac_sha256(key, message)    return hmac.compare_digest(expected_tag, received_tag)  if __name__ == '__main__':    secret = os.urandom(32)  # 生成真实随机密钥    msg = b'Hello, MAC & HMAC!'     tag = simple_hmac_sha256(secret, msg)    print('MAC tag:', tag.hex())     # 正常验证通过    print('Verify OK:', verify_mac(secret, msg, tag))     # 篡改消息后验证失败    forged_msg = b'Hello, MAC & HMAC?'    print('Verify forged:', verify_mac(secret, forged_msg, tag))
目的:用Python标准库从零实现HMAC-SHA256,演示嵌套哈希构造和消息认证的核心流程。
  1. simple_hmac_sha256(key, message)定义HMAC计算函数,完整复现ipad/opad双层哈希结构
  2. key = key.ljust(block_size, b'\x00')密钥补零至块长,这是HMAC规范的关键预处理步骤
  3. ipad = bytes(b ^ 0x36 for b in key)逐字节与ipad常量异或,生成内层密钥流
  4. inner = hashlib.sha256(ipad + message).digest()内层哈希,先把密钥流拼在消息前
  5. hmac.compare_digest(expected_tag, received_tag)恒定时间比较函数,防止时序侧信道攻击

常见误区

误区MAC标签看起来是乱码,是不是就能当加密用?我把消息的MAC标签直接当作密文传输,应该也能保密吧?

正解这是混淆了“认证”与“加密”两个概念。MAC的目的是让接收方验证消息是否被篡改,它不提供机密性——攻击者即使没有密钥,也可能从MAC算法中推断出消息的部分信息(尤其是短消息)。可以把MAC理解为封条而不是保险柜:封条证明没人动过箱子,但箱子里的东西是透明的。要保密必须额外使用加密算法。

为何常见:因为MAC的输入输出看起来都是“乱码”,初学者容易把两者混淆,以为“不可读”就等于“已加密”。

误区既然SHA-256能生成固定长度的摘要,我直接发送“消息+哈希值”不就能验证完整性了吗?何必还搞个密钥?

正解无密钥的哈希只能检测“意外损坏”(如传输错误),无法防御恶意篡改。攻击者可以改消息后重新计算哈希,接收方照样验证通过。这就好比封条任何人都能买到——贴了等于没贴。必须引入密钥(如HMAC),让攻击者因不知道密钥而无法伪造标签。

为何常见:人们直觉上认为“哈希值一致”就代表没被改过,忽略了攻击者也能进行同样的哈希计算。

误区我用SHA-256(secret + message)做MAC,哈希本身很安全,这方案应该没问题吧?

正解这是一个经典的错误构造。SHA-256属于Merkle-Damgård结构,存在长度扩展攻击:攻击者不知道secret,但知道H(secret‖message)的输出,就能推导出H(secret‖message‖padding‖extra)的值,从而伪造新消息的合法MAC。这就是为什么必须用HMAC的双层结构来切断这种递推关系。

为何常见:人们只看到哈希的单向性,忽略了其迭代结构带来的可扩展性。

误区我把HMAC密钥在TLS、JWT、数据库加密里都用了同一把,方便管理,应该没问题吧?

正解密钥复用是密码学大忌。不同场景的安全边界不同:一个场景泄露可能导致所有场景沦陷。而且某些组合(如加密与MAC使用同一密钥)会引入结构性漏洞。正确做法是每类用途派生独立密钥(如用HKDF),正如不同门锁要用不同钥匙,而不是一把钥匙开所有锁。

为何常见:密钥管理是工程中最“麻烦”的部分,人们倾向于图省事而忽视隔离原则。

误区我收到的MAC验证失败了,那一定是数据被黑客篡改了,赶紧报警!

正解验证失败的原因很多:密钥不匹配(两端配置不一致)、消息编码差异(如字符串与字节串不一致)、算法参数不同(哈希算法用错)、时间戳过期(重放保护机制拒绝)等。工程上需要区分“安全事件”和“配置事故”,否则容易误报甚至漏报真正攻击。

为何常见:教学场景中MAC验证失败总伴随着“篡改”的示例,导致人们形成单一归因的思维定式。

面试题

Q1. MAC的主要安全目标是什么?

单选题 · easy

  • A.提供数据机密性
  • B.提供消息完整性和真实性认证← 正确答案
  • C.提供不可否认性
  • D.提供密钥交换功能

答案:B。MAC的核心目标是完整性与真实性——接收方验证消息未被篡改且来源可信。机密性由加密算法提供,不可否认性由数字签名提供,密钥交换由DH等协议负责。

Q2. 哈希函数与MAC的关键区别是什么?

单选题 · easy

  • A.哈希输出更长
  • B.MAC需要密钥参与计算← 正确答案
  • C.哈希速度更快
  • D.MAC可以加密数据

答案:B。MAC = 带密钥的哈希,密钥的引入让攻击者无法预计算标签。哈希函数不需要密钥,任何人都能计算,因此只能检测无意的损坏,不能防御恶意篡改。

Q3. HMAC中ipad和opad的作用是什么?

单选题 · mid

  • A.加密消息本身
  • B.生成两种不同的密钥派生流,抵抗长度扩展攻击← 正确答案
  • C.压缩消息长度
  • D.增加密钥长度

答案:B。ipad(0x36)和opad(0x5C)分别与密钥异或生成内、外层密钥流,通过双重哈希构造使攻击者无法利用哈希的内部结构推断新MAC,从而免疫长度扩展攻击。

Q4. 以下哪个场景最适合使用CMAC而非HMAC?

单选题 · mid

  • A.需要快速部署的Web应用
  • B.资源受限的IoT设备,已有AES硬件加速← 正确答案
  • C.大数据量文件校验
  • D.需要跨平台兼容的Token签名

答案:B。CMAC基于分组密码(如AES),在已有AES硬件的设备上可以直接复用加密模块,无需额外实现哈希。而IoT设备通常资源受限,CMAC更高效。

Q5. 长度扩展攻击针对的是哪种MAC构造?

单选题 · mid

  • A.HMAC双层哈希
  • B.H(secret || message) 的朴素拼接← 正确答案
  • C.CMAC子密钥机制
  • D.GCM的GHASH

答案:B。朴素拼接 H(secret‖message) 暴露了哈希内部状态,攻击者可借此在消息后追加数据并计算新标签。HMAC的双层结构正是为防御此攻击而设计。

Q6. 以下哪些措施能有效防御长度扩展攻击?

多选题 · mid

  • A.使用HMAC代替朴素哈希拼接← 正确答案
  • B.改用SHA-3(Keccak)算法← 正确答案
  • C.增加密钥长度到512位
  • D.使用CMAC← 正确答案

答案:ABD。HMAC的双层结构天然免疫长度扩展;SHA-3的sponge结构不暴露内部状态。CMAC基于分组密码也不受哈希长度扩展影响。单纯加长密钥不改变构造本身,无法防御。

Q7. 关于MAC的安全要求,以下说法正确的是?

多选题 · mid

  • A.MAC必须具备计算不可伪造性← 正确答案
  • B.密钥可以存储在客户端代码中
  • C.标签长度应足够长以防止碰撞攻击← 正确答案
  • D.同一密钥可用于加密和认证

答案:AC。不可伪造性是MAC的核心安全要求,标签长度直接影响暴力破解难度。密钥必须保密且不同场景应使用独立密钥,加密与认证共用密钥会引入结构漏洞。

Q8. TLS 1.3中要求使用哪种类型的加密方案?

单选题 · easy

  • A.仅加密不认证
  • B.AEAD认证加密← 正确答案
  • C.只使用数字签名
  • D.仅使用HMAC不加密

答案:B。TLS 1.3将AEAD设为唯一选择(如AES-GCM),一次性提供机密性、完整性和真实性保护,避免了过去因“加密与认证分离”引发的组合漏洞。

Q9. 关于CBC-MAC的安全局限,以下哪些说法正确?

多选题 · hard

  • A.固定长度消息是安全的← 正确答案
  • B.变长消息存在拼接伪造攻击← 正确答案
  • C.最后一个分组的填充方式会影响安全性← 正确答案
  • D.CMAC通过子密钥机制修复了CBC-MAC的缺陷← 正确答案

答案:ABCD。CBC-MAC对固定长度消息是安全的,但消息长度可变时,攻击者可拼接已知标签构造新MAC。CMAC引入子密钥K1/K2,根据末块是否完整选择不同处理,并强制填充,修复了该问题。

Q10. 在AEAD的Encrypt-then-MAC组合中,错误的做法是什么?

单选题 · hard

  • A.先加密明文,再对密文计算MAC
  • B.先对明文计算MAC,再一起加密← 正确答案
  • C.加密和MAC使用独立的密钥
  • D.验证MAC通过后才解密

答案:B。Encrypt-and-MAC(先MAC后加密)虽然也能工作,但可能泄露明文的MAC信息,且验证时机不佳。推荐Encrypt-then-MAC:先加密再对密文认证,接收端先验标签再解密,避免对伪造密文解密引发漏洞。