HMAC - 基于哈希的消息认证码
·31 分钟阅读·6091 字··作者:xinglei.wang
什么是 HMAC
HMAC (Hash-based Message Authentication Code,基于哈希的消息认证码) 是一种使用密钥和哈希函数来验证消息完整性和真实性的算法。它由 Mihir Bellare、Ran Canetti 和 Hugo Krawczyk 于 1996 年提出,定义在 RFC 2104 中。
核心功能
| 功能 | 说明 |
|---|---|
| 消息完整性 | 确保消息在传输过程中未被篡改 |
| 消息认证 | 验证消息确实来自声称的发送者 |
| 防重放攻击 | 结合时间戳或序列号防止重放 |
| 密钥验证 | 只有持有正确密钥的人才能生成有效的 HMAC |
与普通哈希的区别
普通哈希 (不安全):
消息 "Hello" → SHA256 → "185f8db32271fe25f561a6fc938b2e26..."
问题: 任何人都能计算哈希值,无法验证来源
HMAC (安全):
消息 "Hello" + 密钥 "secret" → HMAC-SHA256 → "f7bc83f430538424b13298..."
优势: 只有知道密钥的人才能生成正确的 HMAC
工作原理
数学定义
HMAC 的计算公式为:
HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))
其中:
- K = 原始密钥
- K' = 处理后的密钥 (长度等于哈希函数的块大小)
- M = 消息
- H = 哈希函数 (如 SHA-256)
- ⊕ = 异或操作
- || = 连接操作
- opad = 外部填充 (0x5c 重复)
- ipad = 内部填充 (0x36 重复)
计算流程
flowchart TD
classDef input fill:#2b2d42,stroke:#8d99ae,stroke-width:2px,color:#edf2f4
classDef process fill:#ffffff,stroke:#2b2d42,stroke-width:2px,color:#2b2d42
classDef padding fill:#fca311,stroke:#e85d04,stroke-width:2px,color:#ffffff
classDef hash fill:#7209b7,stroke:#3a0ca3,stroke-width:2px,color:#ffffff
classDef output fill:#2ec4b6,stroke:#011627,stroke-width:2px,color:#ffffff
K["密钥 K"]:::input
M["消息 M"]:::input
subgraph KeyProcess ["密钥预处理"]
direction TB
Check{"密钥长度检查"}:::process
Pad["填充到块大小"]:::process
Hash1["哈希压缩"]:::hash
end
K --> Check
Check -- "长度 < 块大小" --> Pad
Check -- "长度 > 块大小" --> Hash1
Hash1 --> Pad
Pad --> K2["K' (标准化密钥)"]:::padding
subgraph InnerHash ["内层哈希"]
direction TB
XOR1["K' XOR ipad"]:::padding
Concat1["拼接消息"]:::process
H1["H(K' XOR ipad || M)"]:::hash
end
K2 --> XOR1
XOR1 --> Concat1
M --> Concat1
Concat1 --> H1
subgraph OuterHash ["外层哈希"]
direction TB
XOR2["K' XOR opad"]:::padding
Concat2["拼接内层结果"]:::process
H2["H(K' XOR opad || 内层结果)"]:::hash
end
K2 --> XOR2
H1 --> Concat2
XOR2 --> Concat2
Concat2 --> H2
H2 --> Result["HMAC 值"]:::output
linkStyle default stroke:#8d99ae,stroke-width:2px,fill:none
详细步骤解析
步骤 1: 密钥预处理
def prepare_key(key: bytes, block_size: int, hash_func) -> bytes:
"""
预处理密钥,确保长度等于哈希函数的块大小
- SHA-256 块大小: 64 bytes
- SHA-512 块大小: 128 bytes
"""
if len(key) > block_size:
# 密钥太长,先哈希压缩
key = hash_func(key).digest()
if len(key) < block_size:
# 密钥太短,右填充零
key = key + b'\x00' * (block_size - len(key))
return key
步骤 2: 生成填充后的密钥
def generate_padded_keys(key: bytes, block_size: int) -> tuple:
"""
生成内部填充和外部填充后的密钥
ipad: 0x36 重复 block_size 次
opad: 0x5c 重复 block_size 次
"""
ipad = bytes([0x36] * block_size)
opad = bytes([0x5c] * block_size)
# 异或操作
inner_key = bytes(k ^ p for k, p in zip(key, ipad))
outer_key = bytes(k ^ p for k, p in zip(key, opad))
return inner_key, outer_key
步骤 3: 双层哈希计算
import hashlib
def hmac_sha256(key: bytes, message: bytes) -> bytes:
"""
完整的 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 = bytes([0x36] * block_size)
opad = bytes([0x5c] * block_size)
inner_key = bytes(k ^ p for k, p in zip(key, ipad))
outer_key = bytes(k ^ p for k, p in zip(key, opad))
# 步骤 3: 内层哈希
inner_hash = hashlib.sha256(inner_key + message).digest()
# 步骤 4: 外层哈希
outer_hash = hashlib.sha256(outer_key + inner_hash).digest()
return outer_hash
为什么使用双层哈希?
双层哈希结构是 HMAC 安全性的关键:
单层方案 (不安全):
H(K || M) - 容易受到长度扩展攻击
H(M || K) - 容易受到碰撞攻击
双层方案 (安全):
H((K ⊕ opad) || H((K ⊕ ipad) || M))
安全性证明:
1. 内层哈希保护消息完整性
2. 外层哈希保护内层结果
3. 不同的填充 (ipad/opad) 确保两层独立
4. 即使哈希函数存在弱点,HMAC 仍然安全
安全特性
1. 抵抗长度扩展攻击
长度扩展攻击是针对 Merkle-Damgard 结构哈希函数 (如 MD5, SHA-1, SHA-256) 的一种攻击方式。
长度扩展攻击原理:
已知: H(secret || message) 的值
攻击者可以计算: H(secret || message || padding || attacker_data)
示例场景 (不安全的 API 签名):
原始请求: GET /api/balance?user=alice
签名: SHA256(secret + "/api/balance?user=alice")
攻击者可以构造:
请求: GET /api/balance?user=alice[padding]&admin=true
签名: 通过长度扩展计算得出
HMAC 如何防御:
- 外层哈希隔离了内层结果
- 攻击者无法利用中间状态
- 每次计算都从初始状态开始
2. 密钥安全性
# HMAC 密钥安全性分析
"""
1. 密钥长度要求:
- 最小: 等于哈希输出长度 (如 SHA-256 需要 32 bytes)
- 推荐: 等于或大于哈希输出长度
- 超长密钥会先被哈希,不会提升安全性
2. 密钥熵要求:
- 使用密码学安全的随机数生成器
- 避免使用可预测的密钥
"""
import secrets
def generate_secure_key(length: int = 32) -> bytes:
"""生成密码学安全的随机密钥"""
return secrets.token_bytes(length)
# 不要这样做:
bad_key = b"password123" # 可预测
bad_key = b"1234567890" * 3 # 低熵
# 应该这样做:
good_key = generate_secure_key(32) # 256 位随机密钥
3. 时序攻击防护
import hmac
def secure_compare(a: bytes, b: bytes) -> bool:
"""
恒定时间比较,防止时序攻击
时序攻击原理:
普通比较在发现第一个不同字节时立即返回
攻击者可以通过测量响应时间逐字节猜测 HMAC 值
"""
return hmac.compare_digest(a, b)
# 不安全的比较 (不要使用):
def insecure_compare(a: bytes, b: bytes) -> bool:
return a == b # 发现不同立即返回,泄露时序信息
# 使用示例:
expected_hmac = compute_hmac(key, message)
received_hmac = request.headers.get('X-Signature')
if secure_compare(expected_hmac, bytes.fromhex(received_hmac)):
# 验证通过
pass
安全性对比
| 攻击类型 | 简单哈希 | Hash(K||M) | Hash(M||K) | HMAC |
|---|---|---|---|---|
| 长度扩展攻击 | 易受攻击 | 易受攻击 | 部分抵抗 | 安全 |
| 碰撞攻击 | 易受攻击 | 部分抵抗 | 易受攻击 | 安全 |
| 密钥恢复 | 不适用 | 困难 | 困难 | 困难 |
| 伪造消息 | 简单 | 中等 | 中等 | 困难 |
常见应用场景
1. API 签名验证
API 签名是 HMAC 最常见的应用场景,用于验证 API 请求的真实性和完整性。
import hmac
import hashlib
import time
import json
from urllib.parse import urlencode
class APIClient:
"""API 客户端 - 请求签名"""
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, params: dict) -> dict:
"""
生成请求签名
签名格式: HMAC-SHA256(secret, method + path + timestamp + params)
"""
timestamp = str(int(time.time() * 1000))
# 构建待签名字符串
sorted_params = urlencode(sorted(params.items()))
message = f"{method}{path}{timestamp}{sorted_params}"
# 计算签名
signature = hmac.new(
self.api_secret,
message.encode(),
hashlib.sha256
).hexdigest()
return {
'X-API-Key': self.api_key,
'X-Timestamp': timestamp,
'X-Signature': signature
}
class APIServer:
"""API 服务端 - 签名验证"""
def __init__(self, secrets_store: dict):
self.secrets_store = secrets_store # api_key -> api_secret
self.timestamp_tolerance = 300000 # 5分钟容忍度 (毫秒)
def verify_request(self, request) -> bool:
"""验证请求签名"""
api_key = request.headers.get('X-API-Key')
timestamp = request.headers.get('X-Timestamp')
signature = request.headers.get('X-Signature')
# 1. 验证时间戳 (防重放攻击)
current_time = int(time.time() * 1000)
if abs(current_time - int(timestamp)) > self.timestamp_tolerance:
return False
# 2. 获取密钥
api_secret = self.secrets_store.get(api_key)
if not api_secret:
return False
# 3. 重新计算签名
sorted_params = urlencode(sorted(request.params.items()))
message = f"{request.method}{request.path}{timestamp}{sorted_params}"
expected_signature = hmac.new(
api_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# 4. 恒定时间比较
return hmac.compare_digest(signature, expected_signature)
Rust 版本:
use hmac::{Hmac, Mac};
use sha2::Sha256;
use hex;
use std::time::{SystemTime, UNIX_EPOCH};
use std::collections::BTreeMap;
type HmacSha256 = Hmac<Sha256>;
/// API 客户端
pub struct ApiClient {
api_key: String,
api_secret: Vec<u8>,
}
impl ApiClient {
pub fn new(api_key: &str, api_secret: &str) -> Self {
ApiClient {
api_key: api_key.to_string(),
api_secret: api_secret.as_bytes().to_vec(),
}
}
/// 生成请求签名
pub fn sign_request(
&self,
method: &str,
path: &str,
params: &BTreeMap<String, String>,
) -> RequestHeaders {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string();
// 构建待签名字符串
let sorted_params: String = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let message = format!("{}{}{}{}", method, path, timestamp, sorted_params);
// 计算 HMAC-SHA256
let mut mac = HmacSha256::new_from_slice(&self.api_secret)
.expect("HMAC can take key of any size");
mac.update(message.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
RequestHeaders {
api_key: self.api_key.clone(),
timestamp,
signature,
}
}
}
/// API 服务端
pub struct ApiServer {
secrets_store: std::collections::HashMap<String, String>,
timestamp_tolerance_ms: u128,
}
impl ApiServer {
pub fn new() -> Self {
ApiServer {
secrets_store: std::collections::HashMap::new(),
timestamp_tolerance_ms: 300_000,
}
}
pub fn add_client(&mut self, api_key: &str, api_secret: &str) {
self.secrets_store.insert(api_key.to_string(), api_secret.to_string());
}
/// 验证请求签名
pub fn verify_request(&self, request: &Request) -> bool {
// 1. 验证时间戳
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let request_time: u128 = match request.timestamp.parse() {
Ok(t) => t,
Err(_) => return false,
};
if current_time.abs_diff(request_time) > self.timestamp_tolerance_ms {
return false;
}
// 2. 获取密钥
let api_secret = match self.secrets_store.get(&request.api_key) {
Some(s) => s,
None => return false,
};
// 3. 重新计算签名
let sorted_params: String = request.params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let message = format!(
"{}{}{}{}",
request.method, request.path, request.timestamp, sorted_params
);
let mut mac = HmacSha256::new_from_slice(api_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(message.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
// 4. 恒定时间比较
constant_time_compare(&request.signature, &expected)
}
}
/// 恒定时间字符串比较
fn constant_time_compare(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.bytes().zip(b.bytes()) {
result |= x ^ y;
}
result == 0
}
pub struct RequestHeaders {
pub api_key: String,
pub timestamp: String,
pub signature: String,
}
pub struct Request {
pub api_key: String,
pub timestamp: String,
pub signature: String,
pub method: String,
pub path: String,
pub params: BTreeMap<String, String>,
}
2. JWT (JSON Web Token) 签名
JWT 使用 HMAC 作为对称签名算法 (HS256/HS384/HS512)。
import json
import base64
import hmac
import hashlib
from typing import Optional
class JWTHandler:
"""简化的 JWT 处理器 (HS256)"""
def __init__(self, secret: str):
self.secret = secret.encode()
def _base64url_encode(self, data: bytes) -> str:
"""URL 安全的 Base64 编码"""
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def _base64url_decode(self, data: str) -> bytes:
"""URL 安全的 Base64 解码"""
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
return base64.urlsafe_b64decode(data)
def create_token(self, payload: dict) -> str:
"""
创建 JWT Token
格式: header.payload.signature
"""
# Header
header = {'alg': 'HS256', 'typ': 'JWT'}
header_b64 = self._base64url_encode(json.dumps(header).encode())
# Payload
payload_b64 = self._base64url_encode(json.dumps(payload).encode())
# Signature
message = f"{header_b64}.{payload_b64}"
signature = hmac.new(
self.secret,
message.encode(),
hashlib.sha256
).digest()
signature_b64 = self._base64url_encode(signature)
return f"{header_b64}.{payload_b64}.{signature_b64}"
def verify_token(self, token: str) -> Optional[dict]:
"""
验证并解析 JWT Token
返回 payload 或 None (验证失败)
"""
try:
parts = token.split('.')
if len(parts) != 3:
return None
header_b64, payload_b64, signature_b64 = parts
# 验证签名
message = f"{header_b64}.{payload_b64}"
expected_signature = hmac.new(
self.secret,
message.encode(),
hashlib.sha256
).digest()
received_signature = self._base64url_decode(signature_b64)
if not hmac.compare_digest(expected_signature, received_signature):
return None
# 解析 payload
payload = json.loads(self._base64url_decode(payload_b64))
return payload
except Exception:
return None
# 使用示例
jwt_handler = JWTHandler("your-256-bit-secret")
# 创建 Token
token = jwt_handler.create_token({
"sub": "1234567890",
"name": "Alice",
"iat": 1516239022,
"exp": 1516242622
})
print(f"Token: {token}")
# 验证 Token
payload = jwt_handler.verify_token(token)
if payload:
print(f"Verified! User: {payload['name']}")
3. Webhook 签名验证
Webhook 服务通常使用 HMAC 来验证回调请求的真实性。
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
class WebhookVerifier:
"""Webhook 签名验证器"""
# 常见的 Webhook 签名格式
SIGNATURE_FORMATS = {
'github': 'sha256={signature}', # GitHub
'stripe': 't={timestamp},v1={signature}', # Stripe
'shopify': '{signature}', # Shopify
}
def __init__(self, secret: str, provider: str = 'github'):
self.secret = secret.encode()
self.provider = provider
def verify_github(self, payload: bytes, signature_header: str) -> bool:
"""验证 GitHub Webhook 签名"""
if not signature_header.startswith('sha256='):
return False
received_signature = signature_header[7:] # 移除 'sha256=' 前缀
expected_signature = hmac.new(
self.secret,
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received_signature, expected_signature)
def verify_stripe(self, payload: bytes, signature_header: str, tolerance: int = 300) -> bool:
"""
验证 Stripe Webhook 签名
Stripe 签名格式: t={timestamp},v1={signature}
"""
import time
# 解析签名头
elements = dict(item.split('=') for item in signature_header.split(','))
timestamp = elements.get('t')
signature = elements.get('v1')
if not timestamp or not signature:
return False
# 验证时间戳 (防重放)
if abs(time.time() - int(timestamp)) > tolerance:
return False
# 构建待签名消息
signed_payload = f"{timestamp}.{payload.decode()}"
expected_signature = hmac.new(
self.secret,
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask 路由示例
WEBHOOK_SECRET = "whsec_your_secret_here"
verifier = WebhookVerifier(WEBHOOK_SECRET)
@app.route('/webhook/github', methods=['POST'])
def github_webhook():
signature = request.headers.get('X-Hub-Signature-256', '')
if not verifier.verify_github(request.data, signature):
abort(401, 'Invalid signature')
# 处理 webhook 事件
event = request.json
event_type = request.headers.get('X-GitHub-Event')
print(f"Received {event_type} event")
return {'status': 'ok'}
@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
signature = request.headers.get('Stripe-Signature', '')
if not verifier.verify_stripe(request.data, signature):
abort(401, 'Invalid signature')
# 处理 Stripe 事件
event = request.json
print(f"Received {event['type']} event")
return {'status': 'ok'}
4. 密码存储 (PBKDF2)
PBKDF2 (Password-Based Key Derivation Function 2) 内部使用 HMAC 进行密钥派生。
import hashlib
import os
def hash_password(password: str, iterations: int = 600000) -> str:
"""
使用 PBKDF2-HMAC-SHA256 哈希密码
NIST 推荐 2023 年使用至少 600,000 次迭代
"""
salt = os.urandom(32) # 256 位随机盐
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
iterations,
dklen=32 # 输出 256 位
)
# 存储格式: algorithm$iterations$salt$hash
return f"pbkdf2_sha256${iterations}${salt.hex()}${key.hex()}"
def verify_password(password: str, stored_hash: str) -> bool:
"""验证密码"""
try:
algorithm, iterations, salt_hex, hash_hex = stored_hash.split('$')
if algorithm != 'pbkdf2_sha256':
return False
salt = bytes.fromhex(salt_hex)
stored_key = bytes.fromhex(hash_hex)
computed_key = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
int(iterations),
dklen=len(stored_key)
)
return hmac.compare_digest(computed_key, stored_key)
except Exception:
return False
# 使用示例
hashed = hash_password("my_secure_password")
print(f"Stored: {hashed}")
# 验证
is_valid = verify_password("my_secure_password", hashed)
print(f"Valid: {is_valid}")
5. 消息队列签名
import json
import hmac
import hashlib
import time
from dataclasses import dataclass, asdict
@dataclass
class SignedMessage:
"""带签名的消息"""
payload: dict
timestamp: int
signature: str
class MessageSigner:
"""消息队列签名器"""
def __init__(self, secret: str):
self.secret = secret.encode()
def sign_message(self, payload: dict) -> SignedMessage:
"""签名消息"""
timestamp = int(time.time())
# 规范化 JSON (确保字段顺序一致)
canonical_payload = json.dumps(payload, sort_keys=True, separators=(',', ':'))
message = f"{timestamp}.{canonical_payload}"
signature = hmac.new(
self.secret,
message.encode(),
hashlib.sha256
).hexdigest()
return SignedMessage(
payload=payload,
timestamp=timestamp,
signature=signature
)
def verify_message(self, message: SignedMessage, max_age: int = 300) -> bool:
"""
验证消息签名
max_age: 消息最大有效期 (秒)
"""
# 检查时间戳
current_time = int(time.time())
if current_time - message.timestamp > max_age:
return False
# 重新计算签名
canonical_payload = json.dumps(message.payload, sort_keys=True, separators=(',', ':'))
expected_message = f"{message.timestamp}.{canonical_payload}"
expected_signature = hmac.new(
self.secret,
expected_message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(message.signature, expected_signature)
# 使用示例
signer = MessageSigner("queue_secret_key")
# 生产者: 签名消息
original_payload = {"event": "user.created", "user_id": 12345}
signed_msg = signer.sign_message(original_payload)
print(f"Signed message: {asdict(signed_msg)}")
# 消费者: 验证消息
is_valid = signer.verify_message(signed_msg)
print(f"Message valid: {is_valid}")
多语言实现
Python
import hmac
import hashlib
# 标准库实现
def hmac_sha256_stdlib(key: bytes, message: bytes) -> bytes:
return hmac.new(key, message, hashlib.sha256).digest()
# 使用示例
key = b"secret_key"
message = b"Hello, World!"
result = hmac_sha256_stdlib(key, message)
print(f"HMAC-SHA256: {result.hex()}")
# 支持的哈希算法
algorithms = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
for algo in algorithms:
h = hmac.new(key, message, algo)
print(f"HMAC-{algo.upper()}: {h.hexdigest()}")
JavaScript / Node.js
const crypto = require('crypto');
// Node.js 实现
function hmacSha256(key, message) {
return crypto.createHmac('sha256', key)
.update(message)
.digest('hex');
}
// 使用示例
const key = 'secret_key';
const message = 'Hello, World!';
const result = hmacSha256(key, message);
console.log(`HMAC-SHA256: ${result}`);
// 浏览器环境 (Web Crypto API)
async function hmacSha256Browser(key, message) {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const messageData = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Promise 使用
hmacSha256Browser('secret_key', 'Hello, World!')
.then(result => console.log(`Browser HMAC: ${result}`));
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func hmacSha256(key, message []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
// 安全比较
func secureCompare(a, b []byte) bool {
return hmac.Equal(a, b)
}
func main() {
key := []byte("secret_key")
message := []byte("Hello, World!")
result := hmacSha256(key, message)
fmt.Printf("HMAC-SHA256: %s\n", hex.EncodeToString(result))
// 验证示例
expected := hmacSha256(key, message)
received := hmacSha256(key, message)
if secureCompare(expected, received) {
fmt.Println("Signature verified!")
}
}
Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;
use hex;
type HmacSha256 = Hmac<Sha256>;
fn hmac_sha256(key: &[u8], message: &[u8]) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key)
.expect("HMAC can take key of any size");
mac.update(message);
mac.finalize().into_bytes().to_vec()
}
fn verify_hmac(key: &[u8], message: &[u8], signature: &[u8]) -> bool {
let mut mac = HmacSha256::new_from_slice(key)
.expect("HMAC can take key of any size");
mac.update(message);
mac.verify_slice(signature).is_ok()
}
fn main() {
let key = b"secret_key";
let message = b"Hello, World!";
let result = hmac_sha256(key, message);
println!("HMAC-SHA256: {}", hex::encode(&result));
// 验证
let is_valid = verify_hmac(key, message, &result);
println!("Verified: {}", is_valid);
}
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class HmacExample {
public static byte[] hmacSha256(byte[] key, byte[] message) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(keySpec);
return mac.doFinal(message);
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// 恒定时间比较
public static boolean secureCompare(byte[] a, byte[] b) {
return MessageDigest.isEqual(a, b);
}
public static void main(String[] args) throws Exception {
byte[] key = "secret_key".getBytes(StandardCharsets.UTF_8);
byte[] message = "Hello, World!".getBytes(StandardCharsets.UTF_8);
byte[] result = hmacSha256(key, message);
System.out.println("HMAC-SHA256: " + bytesToHex(result));
}
}
PHP
<?php
// 计算 HMAC
$key = 'secret_key';
$message = 'Hello, World!';
$hmac = hash_hmac('sha256', $message, $key);
echo "HMAC-SHA256: " . $hmac . "\n";
// 原始二进制输出
$hmacBinary = hash_hmac('sha256', $message, $key, true);
echo "HMAC (binary length): " . strlen($hmacBinary) . "\n";
// 安全比较
function secureCompare($a, $b) {
return hash_equals($a, $b);
}
// 验证示例
$received = hash_hmac('sha256', $message, $key);
if (secureCompare($hmac, $received)) {
echo "Signature verified!\n";
}
// 支持的算法
$algorithms = ['md5', 'sha1', 'sha256', 'sha384', 'sha512'];
foreach ($algorithms as $algo) {
echo "HMAC-" . strtoupper($algo) . ": " . hash_hmac($algo, $message, $key) . "\n";
}
?>
与其他算法对比
HMAC vs CMAC
| 特性 | HMAC | CMAC |
|---|---|---|
| 基础算法 | 哈希函数 (SHA-256等) | 分组密码 (AES等) |
| 输出长度 | 等于哈希输出 | 等于分组大小 |
| 性能 | 软件实现快 | 硬件加速时快 |
| 标准化 | RFC 2104 | NIST SP 800-38B |
| 适用场景 | 通用 | 需要 AES 硬件加速时 |
# CMAC 示例 (使用 pycryptodome)
from Crypto.Hash import CMAC
from Crypto.Cipher import AES
def cmac_aes(key: bytes, message: bytes) -> bytes:
"""计算 CMAC-AES"""
cobj = CMAC.new(key, ciphermod=AES)
cobj.update(message)
return cobj.digest()
# 需要 16/24/32 字节的 AES 密钥
key = b'sixteen_byte_key' # 128-bit key
message = b'Hello, World!'
result = cmac_aes(key, message)
print(f"CMAC-AES: {result.hex()}")
HMAC vs Poly1305
| 特性 | HMAC | Poly1305 |
|---|---|---|
| 类型 | 基于哈希 | 基于多项式 |
| 密钥使用 | 可重复使用 | 一次性密钥 |
| 性能 | 中等 | 非常快 |
| 安全性 | 成熟稳定 | 现代设计 |
| 典型用途 | API签名、JWT | TLS 1.3、ChaCha20-Poly1305 |
# Poly1305 示例 (通常与 ChaCha20 配合使用)
from Crypto.Cipher import ChaCha20_Poly1305
import os
def chacha20_poly1305_encrypt(key: bytes, plaintext: bytes, aad: bytes = b'') -> tuple:
"""
ChaCha20-Poly1305 认证加密
返回 (nonce, ciphertext, tag)
"""
nonce = os.urandom(12) # 96-bit nonce
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
cipher.update(aad) # 附加认证数据
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return nonce, ciphertext, tag
def chacha20_poly1305_decrypt(key: bytes, nonce: bytes, ciphertext: bytes, tag: bytes, aad: bytes = b'') -> bytes:
"""解密并验证"""
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
cipher.update(aad)
return cipher.decrypt_and_verify(ciphertext, tag)
# 使用示例
key = os.urandom(32) # 256-bit key
plaintext = b"Secret message"
aad = b"header data"
nonce, ciphertext, tag = chacha20_poly1305_encrypt(key, plaintext, aad)
decrypted = chacha20_poly1305_decrypt(key, nonce, ciphertext, tag, aad)
print(f"Decrypted: {decrypted}")
HMAC vs 数字签名
| 特性 | HMAC | 数字签名 (RSA/ECDSA) |
|---|---|---|
| 密钥类型 | 对称 (共享密钥) | 非对称 (公私钥对) |
| 验证者 | 需要知道密钥 | 只需公钥 |
| 性能 | 快 | 慢 |
| 密钥管理 | 困难 (需安全分发) | 简单 (公钥可公开) |
| 不可否认性 | 无 | 有 |
| 适用场景 | 内部系统、API | 公开验证、法律效力 |
# 对比示例
# HMAC - 对称认证
import hmac
import hashlib
hmac_key = b"shared_secret"
message = b"important data"
hmac_tag = hmac.new(hmac_key, message, hashlib.sha256).digest()
# 验证者也需要 hmac_key
# 数字签名 - 非对称认证
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
# 生成密钥对
private_key = RSA.generate(2048)
public_key = private_key.publickey()
# 签名 (使用私钥)
h = SHA256.new(message)
signature = pkcs1_15.new(private_key).sign(h)
# 验证 (只需公钥)
try:
pkcs1_15.new(public_key).verify(h, signature)
print("Signature is valid")
except Exception:
print("Invalid signature")
算法选择指南
flowchart TD
classDef decision fill:#ff9f1c,stroke:#d00000,stroke-width:2px,color:#ffffff
classDef result fill:#2ec4b6,stroke:#011627,stroke-width:2px,color:#ffffff
classDef process fill:#ffffff,stroke:#2b2d42,stroke-width:2px,color:#2b2d42
Start["需要消息认证"]:::process
Q1{"需要不可否认性?"}:::decision
Q2{"通信双方<br/>能共享密钥?"}:::decision
Q3{"需要加密?"}:::decision
Q4{"有AES硬件加速?"}:::decision
DS["数字签名<br/>(RSA/ECDSA/EdDSA)"]:::result
HMAC["HMAC-SHA256"]:::result
AEAD["AEAD<br/>(AES-GCM/ChaCha20-Poly1305)"]:::result
CMAC["CMAC-AES"]:::result
Start --> Q1
Q1 -- "是" --> DS
Q1 -- "否" --> Q2
Q2 -- "否" --> DS
Q2 -- "是" --> Q3
Q3 -- "是" --> AEAD
Q3 -- "否" --> Q4
Q4 -- "是" --> CMAC
Q4 -- "否" --> HMAC
linkStyle default stroke:#8d99ae,stroke-width:2px,fill:none
最佳实践
1. 密钥管理
import secrets
import os
class KeyManager:
"""密钥管理最佳实践"""
@staticmethod
def generate_key(length: int = 32) -> bytes:
"""
生成密码学安全的随机密钥
推荐长度:
- HMAC-SHA256: 32 bytes (256 bits)
- HMAC-SHA384: 48 bytes (384 bits)
- HMAC-SHA512: 64 bytes (512 bits)
"""
return secrets.token_bytes(length)
@staticmethod
def key_from_password(password: str, salt: bytes, iterations: int = 600000) -> bytes:
"""从密码派生密钥 (不推荐用于 HMAC,仅作参考)"""
import hashlib
return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations)
@staticmethod
def rotate_keys(old_key: bytes, new_key: bytes, data: list) -> list:
"""
密钥轮换示例
1. 用旧密钥验证
2. 用新密钥重新签名
"""
rotated = []
for item in data:
# 验证旧签名
if verify_signature(old_key, item['data'], item['signature']):
# 生成新签名
new_signature = create_signature(new_key, item['data'])
item['signature'] = new_signature
rotated.append(item)
return rotated
# 密钥存储建议
"""
1. 环境变量 (开发/测试):
HMAC_SECRET_KEY=your_base64_encoded_key
2. 密钥管理服务 (生产):
- AWS KMS
- HashiCorp Vault
- Azure Key Vault
- GCP Secret Manager
3. 硬件安全模块 (高安全):
- HSM
- TPM
"""
2. 错误处理
import hmac
import hashlib
from typing import Optional
class HMACError(Exception):
"""HMAC 相关错误基类"""
pass
class InvalidSignatureError(HMACError):
"""签名无效"""
pass
class ExpiredSignatureError(HMACError):
"""签名已过期"""
pass
class InvalidKeyError(HMACError):
"""密钥无效"""
pass
def safe_hmac_verify(key: bytes, message: bytes, signature: bytes, max_age: Optional[int] = None) -> bool:
"""
安全的 HMAC 验证
- 捕获所有异常
- 不泄露错误细节
- 防止时序攻击
"""
try:
# 验证密钥长度
if len(key) < 16:
raise InvalidKeyError("Key too short")
# 计算期望签名
expected = hmac.new(key, message, hashlib.sha256).digest()
# 恒定时间比较
if not hmac.compare_digest(expected, signature):
raise InvalidSignatureError("Signature mismatch")
return True
except (InvalidSignatureError, InvalidKeyError):
# 记录日志但不暴露细节
# logger.warning("HMAC verification failed")
return False
except Exception:
# 捕获所有异常,防止信息泄露
return False
3. 日志和审计
import logging
import time
from functools import wraps
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('hmac_audit')
def audit_hmac_operation(operation: str):
"""HMAC 操作审计装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
success = False
error_msg = None
try:
result = func(*args, **kwargs)
success = True
return result
except Exception as e:
error_msg = str(e)
raise
finally:
elapsed = time.time() - start_time
logger.info(
f"HMAC Operation: {operation}, "
f"Success: {success}, "
f"Duration: {elapsed:.4f}s, "
f"Error: {error_msg}"
)
return wrapper
return decorator
@audit_hmac_operation("verify_api_signature")
def verify_api_request(api_key: str, signature: str, payload: bytes) -> bool:
"""带审计的 API 签名验证"""
# 实现验证逻辑
pass
常见问题
1. HMAC 和加密有什么区别?
HMAC (认证):
- 验证消息完整性
- 验证消息来源
- 不隐藏消息内容
- 输入: 密钥 + 消息 → 输出: 固定长度标签
加密 (保密):
- 隐藏消息内容
- 不保证完整性 (需要认证加密)
- 输入: 密钥 + 明文 → 输出: 可变长度密文
最佳实践: 使用 AEAD (如 AES-GCM) 同时提供加密和认证
2. 为什么不直接用 Hash(key + message)?
# 不安全的方案
def insecure_mac(key: bytes, message: bytes) -> bytes:
return hashlib.sha256(key + message).digest()
"""
问题 1: 长度扩展攻击
已知: H(key || message) = hash_value
攻击者可以计算: H(key || message || padding || extra_data)
而不需要知道 key!
问题 2: 密钥材料泄露
如果哈希函数的内部状态被泄露,可能暴露密钥信息
HMAC 的双层结构避免了这些问题
"""
3. HMAC 输出可以截断吗?
"""
可以截断,但会降低安全性:
HMAC-SHA256 输出: 256 bits (32 bytes)
截断到 128 bits: 仍然安全 (128 bits 抗碰撞)
截断到 64 bits: 可能被暴力破解
RFC 2104 建议:
- 最少保留一半长度
- 不少于 80 bits
"""
def hmac_sha256_truncated(key: bytes, message: bytes, length: int = 16) -> bytes:
"""截断的 HMAC"""
full_hmac = hmac.new(key, message, hashlib.sha256).digest()
return full_hmac[:length] # 取前 length 字节
4. 如何处理密钥轮换?
"""
密钥轮换策略:
1. 双密钥验证期
- 保留旧密钥一段时间
- 新消息用新密钥签名
- 验证时同时尝试新旧密钥
2. 版本化密钥
- 签名中包含密钥版本号
- 根据版本选择对应密钥
3. 渐进式迁移
- 设置轮换开始时间
- 逐步替换旧签名
"""
class KeyVersionedHMAC:
def __init__(self, keys: dict):
self.keys = keys # {version: key}
self.current_version = max(keys.keys())
def sign(self, message: bytes) -> tuple:
"""签名并返回 (version, signature)"""
key = self.keys[self.current_version]
signature = hmac.new(key, message, hashlib.sha256).digest()
return self.current_version, signature
def verify(self, message: bytes, version: int, signature: bytes) -> bool:
"""根据版本验证签名"""
if version not in self.keys:
return False
key = self.keys[version]
expected = hmac.new(key, message, hashlib.sha256).digest()
return hmac.compare_digest(expected, signature)
总结
HMAC 是现代密码学中最重要的消息认证方案之一:
- 安全性: 双层哈希结构提供强安全保证
- 通用性: 可与任何密码学哈希函数组合
- 效率: 计算速度快,适合大量数据
- 标准化: RFC 2104 定义,广泛支持
关键要点
| 方面 | 建议 |
|---|---|
| 哈希算法 | 优先使用 SHA-256 或 SHA-512 |
| 密钥长度 | 至少等于哈希输出长度 |
| 密钥生成 | 使用密码学安全随机数 |
| 比较方式 | 使用恒定时间比较函数 |
| 时间戳 | 包含时间戳防止重放攻击 |
应用场景总结
- API 签名: 验证请求真实性
- JWT: 对称签名算法
- Webhook: 回调验证
- 消息队列: 消息完整性
- 密钥派生: PBKDF2、HKDF 等
参考资料:
- RFC 2104: HMAC: Keyed-Hashing for Message Authentication
- NIST FIPS 198-1: The Keyed-Hash Message Authentication Code (HMAC)
- RFC 4868: Using HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512 with IPsec
系列:加密算法入门
第 1 篇,共 3 篇
相关推荐
RSA 算法 - 非对称加密的基石
深入解析 RSA 算法的数学原理、密钥生成、加密解密过程、数字签名应用,以及安全性分析和最佳实践
·37 分钟·
#RSA#非对称加密
Ed25519 - 现代高性能数字签名算法
深入解析 Ed25519 椭圆曲线签名算法的数学原理、性能优势、安全特性,以及在区块链和 SSH 中的应用实践
·32 分钟·
#Ed25519#椭圆曲线
撮合引擎 - 数字资产交易的核心技术
深入解析撮合引擎的工作原理、核心算法、订单类型和技术架构,理解交易所如何实现高效的买卖订单匹配
·45 分钟·
#撮合引擎#交易所