HMAC 是什么
HMAC(Hash-based Message Authentication Code)是 RFC 2104 与 FIPS 198-1 定义的密钥化认证码,用带密钥的哈希同时提供消息完整性与来源认证:只有持有同一密钥的一方才能算出相同的值。它不加密内容,明文照常可见;作用是让对方确认消息没被改、且确实来自持有密钥的伙伴。
基本信息
| 标准 | RFC 2104 / FIPS 198-1 |
|---|---|
| 构造 | H((K xor opad) || H((K xor ipad) || M)) |
| 推荐哈希 | SHA-256 及以上 |
| 密钥长度 | 不小于哈希输出长度,如 32 字节随机值 |
| 输出长度 | 与底层哈希一致(SHA-256 为 32 字节) |
| 典型用途 | 接口签名、Webhook 校验、JWT HS256、TLS 1.2 的 PRF |
典型使用场景
- 开放平台接口签名:把参数按约定顺序拼接后计算 HMAC,服务端复算比对
- Webhook 回调验签:第三方推送事件时附带签名头,接收方用共享密钥校验来源与完整性
- JWT 的 HS256:用对称密钥签令牌,适合前后端或服务之间自用
- 会话与 Cookie 完整性:对状态值加签名,防止客户端篡改字段
- 文件传输校验:共享密钥场景下比裸哈希更能防主动篡改
- 消息队列与内部 RPC:为消息体附加签名,避免伪造与重放(重放还需时间戳与序号配合)
计算与校验
计算与校验
# 命令行计算 HMAC(-hmac 后面跟共享密钥)
printf '%s' 'payload' | openssl dgst -sha256 -hmac 'my-secret'
# 校验时把收到的签名与本地复算值做比较(生产代码用常量时间比较)
printf '%s' 'payload' | openssl dgst -sha256 -hmac 'my-secret' | awk '{print $2}'
# Base64 形式便于放进 Header,注意区分二进制签名与十六进制字符串
printf '%s' 'payload' | openssl dgst -sha256 -hmac 'my-secret' -binary | openssl base64
# 生成一把 32 字节随机密钥,禁止用口令或短字符串充当
openssl rand -base64 32服务端校验(Python)
服务端校验(Python)
import hmac, hashlib
# 常量时间比较:compare_digest 避免逐字节比较泄漏时序信息
signature = request.headers['X-Signature'] # 十六进制字符串
expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
return 401 # 统一错误响应,不提示差异细节
# 建议把时间戳一起纳入签名,并限制时间窗口,防重放攻击与相邻概念对照
| 机制 | 是否有密钥 | 保护目标 | 典型误用 |
|---|---|---|---|
| 裸哈希(SHA-256) | 无 | 仅完整性提示 | 当作签名做来源校验,攻击者可重算 |
| HMAC | 共享密钥 | 完整性与来源 | 密钥过短、比较非常量时间 |
| 数字签名 | 私钥/公钥 | 完整性与不可否认来源 | 私钥权限过宽、填充混用 |
| AEAD 加密 | 密钥 | 机密性与完整性 | nonce 重用、只看密文不看标签 |
校验 HMAC 必须使用常量时间比较(Python 的 hmac.compare_digest、Go 的 hmac.Equal 等)。逐字节提前返回的比较方式会泄漏「前多少字节匹配」,理论上可被逐字节恢复出合法签名。
HMAC 的密钥不能是口令或短字符串:密钥熵不足时,攻击者可用字典离线复算。用密码学随机数生成至少 32 字节,并像对待密码一样存放(配置中心、密钥管理服务),不写死在代码与镜像里。
HMAC 只解决「消息没被改、来源可信」,不解决重放。完整方案要叠加时间戳与随机数(nonce),服务端校验时间窗口并记录已用过的 nonce。签名内容里也要包含时间戳与请求路径,避免把同一签名挪用到其他接口。
签名串构造建议
| 要素 | 是否纳入 | 原因 |
|---|---|---|
| HTTP 方法 | 是 | 防止同一路径的读写请求互相顶替 |
| 请求路径与查询串 | 是 | 防止签名被挪用到其他接口 |
| 请求体摘要 | 是 | 大请求体可只签摘要,避免整体参与计算 |
| 时间戳 | 是 | 配合服务端时间窗口判断,降低重放可能 |
| 随机数 nonce | 建议 | 挡住窗口期内的重复提交,需服务端做去重 |
| 密钥标识 keyid | 建议 | 支持密钥轮换,避免只能靠时间推断用哪把密钥 |