2. 使用HMAC实现消息认证码

使用HMAC实现消息认证码 学习卡片

HMAC 的核心结构与验证流程

HMAC(Hash-based Message Authentication Code)不是简单地把密钥拼在消息后面再哈希,而是通过内外两层哈希结构把密钥混入摘要计算,从而抵抗长度扩展攻击。其标准形式为:

HMAC(K, M) = H( (K ⊕ opad) || H( (K ⊕ ipad) || M ) )

其中 ipad 为字节 0x36 重复到哈希块大小,opad 为字节 0x5C 重复到哈希块大小。以 SHA-256 为例,块大小为 64 字节。密钥若短于块大小则右侧补零,若长于块大小则先哈希压缩。

发送方(Alice) 共享密钥 K 消息 M K 填充至 64 字节(不足补 0x00,过长先 SHA-256) K ⊕ ipad (0x36) SHA-256 内层 内层摘要 H₁ K ⊕ opad (0x5C) SHA-256 外层 HMAC 认证码 (32 字节) 发送:M ‖ HMAC(K, M) 传输 接收方(Bob) 接收 M' 接收 HMAC' 共享密钥 K 重新计算 HMAC(K, M') 使用相同的 ipad/opad 结构 恒定时间比较 HMAC' == HMAC(K, M') ? ✓ 验证通过 消息完整且来源可信 ✗ 验证失败 消息被篡改或密钥错误 HMAC 同时提供完整性校验与来源认证;密钥不随消息传输

使用 Python 标准库实现 HMAC

Python 的 hmac 模块配合 hashlib 可直接调用 HMAC 接口。以下示例使用 SHA-256 作为底层哈希,密钥为 32 字节随机值,消息为 UTF-8 编码的文本。

import hmac
import hashlib
import os

# 1. 生成 32 字节共享密钥(实际场景中通过安全信道分发)
key = os.urandom(32)
print("密钥(hex):", key.hex())

# 2. 待认证消息
message = "转账: 从A账户到B账户, 金额=1000, 时间=2025-01-15T10:30:00Z"
msg_bytes = message.encode("utf-8")

# 3. 生成 HMAC-SHA256 认证码
mac = hmac.new(key, msg_bytes, hashlib.sha256)
tag = mac.digest()          # 32 字节二进制
tag_hex = mac.hexdigest()   # 64 字符十六进制
print("HMAC(hex):", tag_hex)

# 4. 接收方验证(模拟)
received_msg = msg_bytes
received_tag = tag

# 重新计算并比较
expected = hmac.new(key, received_msg, hashlib.sha256).digest()
is_valid = hmac.compare_digest(expected, received_tag)
print("验证结果:", is_valid)  # True

# 5. 篡改消息后验证
tampered = "转账: 从A账户到B账户, 金额=9999, 时间=2025-01-15T10:30:00Z".encode("utf-8")
expected2 = hmac.new(key, tampered, hashlib.sha256).digest()
print("篡改后验证:", hmac.compare_digest(expected2, received_tag))  # False
关键点:验证时必须使用 hmac.compare_digest() 而非 ==。普通字符串比较会在第一个不同字节处提前返回,攻击者可通过响应时间差异逐字节猜测认证码,这就是时序攻击(timing attack)。compare_digest 保证恒定时间比较。

参数与行为对照

参数/行为说明示例值
底层哈希算法决定输出长度与安全强度SHA-256 → 32 字节输出
密钥长度建议 ≥ 哈希输出长度;短于块大小自动补零32 字节(256 位)
块大小由哈希算法决定,SHA-256 为 64 字节64 字节
ipad / opad固定填充字节,用于内外层密钥混合0x36 / 0x5C
消息编码发送方与接收方必须一致UTF-8
比较方式必须恒定时间compare_digest

验证清单

  • 发送方与接收方使用相同的密钥和相同的哈希算法。
  • 消息在传输前完成编码(如 UTF-8),接收方按相同方式解码/编码后再计算。
  • 认证码与消息一起传输,但密钥绝不随消息传输。
  • 比较认证码时使用恒定时间函数,避免时序侧信道。
  • 若验证失败,需区分是消息被篡改还是密钥不匹配——两者产生相同的失败结果,需通过带外方式排查。