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 是现代密码学中最重要的消息认证方案之一:

  1. 安全性: 双层哈希结构提供强安全保证
  2. 通用性: 可与任何密码学哈希函数组合
  3. 效率: 计算速度快,适合大量数据
  4. 标准化: 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

相关推荐

RSA 算法 - 非对称加密的基石

深入解析 RSA 算法的数学原理、密钥生成、加密解密过程、数字签名应用,以及安全性分析和最佳实践

·37 分钟·
#RSA#非对称加密

Ed25519 - 现代高性能数字签名算法

深入解析 Ed25519 椭圆曲线签名算法的数学原理、性能优势、安全特性,以及在区块链和 SSH 中的应用实践

·32 分钟·
#Ed25519#椭圆曲线