HMAC 和数据完整性:用共享密钥检测篡改
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 13 minutes reading.
1. 为什么要关心这个问题?
你正在构建一个 API。客户端发送这样的请求:
{"action": "transfer", "amount": 1000, "to": "attacker"}你怎么知道这个请求在传输过程中没有被修改?你怎么知道它来自授权的客户端?
如果你已经和客户端共享了一个密钥(比如 API 密钥),你可以使用消息认证码(MAC)来验证真实性和完整性——比数字签名更快。
2. 定义
消息认证码 (MAC) 是一小段用于认证消息并验证其完整性的信息。
HMAC(基于哈希的 MAC) 使用密码学哈希函数和密钥来构造 MAC。
MAC 的特性:
- 任何知道密钥的人都可以生成
- 任何知道密钥的人都可以验证
- 没有密钥,无法伪造
数字签名 vs MAC:
- 签名:只有签名者能创建,任何人都能验证
- MAC:任何知道密钥的人都能创建和验证
- 签名:不可否认性
- MAC:没有不可否认性(双方都能创建)3. 为什么不能只用哈希?
朴素方法(有漏洞)
# 错误:简单哈希不是认证
import hashlib
def naive_integrity(message):
return hashlib.sha256(message).hexdigest()
# 攻击者可以计算任何消息的哈希!
# 这提供不了任何认证问题所在
单独的哈希证明:
✗ 谁创建了消息——什么都没有
✗ 授权——什么都没有
因为:
- 哈希函数是公开的
- 任何人都可以计算 SHA256(任何消息)
- 攻击者可以伪造:message' + SHA256(message')为什么 HMAC 有效
HMAC 包含密钥:
HMAC(key, message) = hash(key || hash(key || message))
只有知道密钥的人才能:
- 计算有效的 MAC
- 验证 MAC
没有密钥,攻击者无法:
- 为修改过的消息计算 MAC
- 找到具有相同 MAC 的不同消息4. HMAC 构造
HMAC 公式
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
其中:
K = 密钥
K' = 填充到块大小的密钥
H = 哈希函数(SHA-256 等)
⊕ = 异或
opad = 外层填充(0x5c 重复)
ipad = 内层填充(0x36 重复)
m = 消息为什么是这种结构?
简单方法有缺陷:
H(key || message): 长度扩展攻击
H(message || key): 碰撞问题
H(key || message || key):仍然有漏洞
HMAC 的嵌套结构:
- 防止长度扩展
- 有安全性证明
- RFC 2104(1997)以来的标准5. 在 Python 中使用 HMAC
基本 HMAC
import hmac
import hashlib
key = b"super-secret-api-key"
message = b"action=transfer&amount=1000&to=alice"
# 计算 HMAC
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f"HMAC: {mac}")
# 验证 HMAC(时序安全比较)
def verify_hmac(key, message, received_mac):
expected_mac = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected_mac, received_mac)
# 使用
is_valid = verify_hmac(key, message, mac)
print(f"有效: {is_valid}")常见错误:时序攻击
# 错误:容易受到时序攻击
def insecure_verify(expected, received):
return expected == received # 泄露长度信息!
# 正确:常数时间比较
import hmac
def secure_verify(expected, received):
return hmac.compare_digest(expected, received)
# 为什么时序很重要:
# == 运算符在第一个不匹配时提前返回
# 攻击者可以通过测量时间逐字节猜测 MAC
# compare_digest 总是花费相同时间6. 实际应用
API 请求签名
import hmac
import hashlib
import time
import base64
class APIClient:
def __init__(self, api_key: str, api_secret: str):
self.api_key = api_key
self.api_secret = api_secret.encode()
def sign_request(self, method: str, path: str, body: str = "") -> dict:
timestamp = str(int(time.time()))
# 创建待签名字符串
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
# 计算 HMAC
signature = hmac.new(
self.api_secret,
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return {
"X-API-Key": self.api_key,
"X-Timestamp": timestamp,
"X-Signature": signature
}
class APIServer:
def __init__(self, secrets: dict):
self.secrets = secrets # api_key -> api_secret
def verify_request(self, method: str, path: str, body: str,
api_key: str, timestamp: str, signature: str) -> bool:
# 检查时间戳(防止重放攻击)
if abs(time.time() - int(timestamp)) > 300: # 5 分钟窗口
return False
# 获取此密钥的密钥
api_secret = self.secrets.get(api_key)
if not api_secret:
return False
# 重新计算签名
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
expected = hmac.new(
api_secret.encode(),
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Cookie 认证
import hmac
import hashlib
import base64
import json
import time
class SecureCookie:
def __init__(self, secret_key: bytes):
self.secret_key = secret_key
def create(self, data: dict, max_age: int = 3600) -> str:
"""创建签名的 cookie 值"""
payload = {
"data": data,
"exp": int(time.time()) + max_age
}
payload_json = json.dumps(payload, sort_keys=True)
payload_b64 = base64.b64encode(payload_json.encode()).decode()
# 签名 payload
signature = hmac.new(
self.secret_key,
payload_b64.encode(),
hashlib.sha256
).hexdigest()
return f"{payload_b64}.{signature}"
def verify(self, cookie_value: str) -> dict | None:
"""验证并解码签名的 cookie"""
try:
payload_b64, signature = cookie_value.rsplit(".", 1)
# 验证签名
expected = hmac.new(
self.secret_key,
payload_b64.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return None
# 解码并检查过期
payload = json.loads(base64.b64decode(payload_b64))
if time.time() > payload["exp"]:
return None
return payload["data"]
except Exception:
return None
# 使用
cookie = SecureCookie(b"my-super-secret-key")
value = cookie.create({"user_id": 123, "role": "admin"})
print(f"Cookie: {value}")
data = cookie.verify(value)
print(f"数据: {data}")Webhook 验证
import hmac
import hashlib
def verify_github_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""验证 GitHub webhook 签名"""
# GitHub 发送:sha256=<hex_digest>
if not signature.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def verify_stripe_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""验证 Stripe webhook 签名"""
# Stripe 格式:t=timestamp,v1=signature
parts = dict(p.split("=") for p in signature.split(","))
# Stripe 签名:timestamp.payload
signed_payload = f"{parts['t']}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])7. HMAC vs 其他 MAC
对比
HMAC:
- 基于哈希函数(SHA-256 等)
- 研究充分,保守的选择
- 比某些替代品稍慢
Poly1305:
- 为速度设计
- 与 ChaCha20 一起使用(ChaCha20-Poly1305)
- 每条消息使用一次性密钥
CMAC/OMAC:
- 基于块密码(AES)
- 在某些标准中使用
- 安全性与 HMAC 类似
GMAC:
- GCM 模式的 MAC 部分
- 有 AES-NI 时非常快
- 需要唯一的 nonce何时使用什么
使用 HMAC-SHA256 当:
- 需要独立的 MAC
- 最大兼容性
- 保守的安全选择
使用 Poly1305 当:
- 使用 ChaCha20 加密
- 需要最大速度
- 作为认证加密的一部分
使用 GCM/GMAC 当:
- 使用 AES 加密
- 需要认证加密
- 有硬件 AES 支持8. 安全考虑
密钥管理
HMAC 密钥要求:
- 必须保密(显然)
- 应该是随机的,不是从密码派生的
- 最少 128 位,推荐 256 位
- 不同用途使用不同密钥
如果需要密钥派生:
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
def derive_hmac_key(master_key: bytes, purpose: str) -> bytes:
return HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=purpose.encode()
).derive(master_key)HMAC 不提供什么
HMAC 不提供:
✗ 机密性(消息是明文)
✗ 不可否认性(双方都能创建 MAC)
✗ 重放保护(需要时间戳/nonce)
要获得机密性 + 完整性:
→ 使用认证加密(AES-GCM、ChaCha20-Poly1305)
要获得不可否认性:
→ 使用数字签名
要获得重放保护:
→ 在消息中包含时间戳或序列号常见错误
# 错误:使用密码作为密钥
hmac.new(b"password123", message, hashlib.sha256)
# 正确:从密码派生密钥
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
kdf = Scrypt(salt=salt, length=32, n=2**20, r=8, p=1)
key = kdf.derive(b"password123")
hmac.new(key, message, hashlib.sha256)
# 错误:加密和 MAC 使用相同密钥
aes_key = os.urandom(32)
encrypted = aes_encrypt(aes_key, plaintext)
mac = hmac.new(aes_key, encrypted, hashlib.sha256)
# 正确:分开的密钥
aes_key = os.urandom(32)
mac_key = os.urandom(32)
encrypted = aes_encrypt(aes_key, plaintext)
mac = hmac.new(mac_key, encrypted, hashlib.sha256)
# 最佳:使用认证加密
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)9. HMAC 在协议中的应用
TLS
TLS 使用 HMAC(或 AEAD)进行记录认证:
TLS 记录:
┌────────────────────────────────────────────┐
│ 内容类型 │ 版本 │ 长度 │ 负载 │
├────────────────────────────────────────────┤
│ 加密 + MAC │
└────────────────────────────────────────────┘
TLS 1.2:MAC-then-encrypt 或 AEAD
TLS 1.3:仅 AEAD(GCM 或 ChaCha20-Poly1305)JWT
JWT 结构:
header.payload.signature
对于 HS256(HMAC-SHA256):
signature = HMAC-SHA256(
secret,
base64url(header) + "." + base64url(payload)
)
验证:
1. 将 token 分成部分
2. 重新计算 HMAC
3. 比较签名(常数时间!)AWS Signature V4
AWS 请求签名使用 HMAC 链:
DateKey = HMAC-SHA256("AWS4" + SecretKey, Date)
RegionKey = HMAC-SHA256(DateKey, Region)
ServiceKey = HMAC-SHA256(RegionKey, Service)
SigningKey = HMAC-SHA256(ServiceKey, "aws4_request")
Signature = HMAC-SHA256(SigningKey, StringToSign)10. 完整示例:签名消息
import hmac
import hashlib
import json
import time
import os
import base64
class SignedMessageProtocol:
"""认证消息的完整协议"""
def __init__(self, shared_secret: bytes):
self.key = shared_secret
def create_message(self, payload: dict) -> str:
"""创建认证消息"""
# 添加元数据
message = {
"payload": payload,
"timestamp": int(time.time()),
"nonce": base64.b64encode(os.urandom(16)).decode()
}
# 序列化
message_json = json.dumps(message, sort_keys=True)
message_b64 = base64.b64encode(message_json.encode()).decode()
# 创建 MAC
mac = hmac.new(
self.key,
message_b64.encode(),
hashlib.sha256
).hexdigest()
return f"{message_b64}.{mac}"
def verify_message(self, signed_message: str,
max_age: int = 300) -> dict | None:
"""验证并提取消息"""
try:
# 分割
message_b64, received_mac = signed_message.rsplit(".", 1)
# 验证 MAC
expected_mac = hmac.new(
self.key,
message_b64.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_mac, received_mac):
return None
# 解码
message_json = base64.b64decode(message_b64)
message = json.loads(message_json)
# 检查时间戳
age = time.time() - message["timestamp"]
if age < 0 or age > max_age:
return None
return message["payload"]
except Exception:
return None
# 使用
secret = os.urandom(32)
protocol = SignedMessageProtocol(secret)
# 发送方
msg = protocol.create_message({
"action": "transfer",
"amount": 100,
"to": "[email protected]"
})
print(f"签名消息: {msg[:50]}...")
# 接收方
payload = protocol.verify_message(msg)
if payload:
print(f"验证的负载: {payload}")
else:
print("验证失败!")11. 本章小结
三点要记住:
HMAC 提供认证和完整性。 与普通哈希不同,HMAC 需要知道密钥。没有密钥,攻击者无法伪造有效的 MAC。
始终使用常数时间比较。 使用
hmac.compare_digest()防止时序攻击。普通字符串比较通过时序泄露信息。HMAC 不能替代加密。 它验证完整性但不隐藏内容。要获得机密性 + 完整性,使用认证加密(AES-GCM)。
12. 下一步
我们现在已经介绍了基本的密码学原语:对称加密、非对称加密、数字签名、证书和 MAC。
在下一节中,我们将看到这些部分如何在真实协议中结合在一起:TLS 深入分析——HTTPS 实际上是如何工作的,从握手到安全数据传输。
