第八讲:消息认证码(HMAC)——给数据加把“防伪锁”

大家好,欢迎来到第八讲。今天咱们聊聊 HMAC,全称是 Hash-based Message Authentication Code,也就是基于哈希的消息认证码。

说实话,我刚开始做安全编程那会儿,总觉得“加密了不就安全了吗”?后来踩了坑才明白——加密只能防偷看,防不了篡改。你想想看,如果别人改了你加密后的数据,你解密出来也是一堆乱码,但你根本不知道数据被动了手脚。这就是 HMAC 要解决的问题。

一、HMAC 到底是个啥?

简单说,HMAC 就是“哈希函数 + 密钥”的组合拳。它用密钥对消息做一次带“身份”的哈希运算,生成一个固定长度的认证码。接收方用同样的密钥和算法重新计算,比对结果就知道消息有没有被篡改。

我在项目中遇到过这样一个场景:两个服务之间通过 API 通信,传输的是 JSON 数据。虽然用了 HTTPS,但内部审计要求“端到端完整性校验”。这时候 HMAC 就派上用场了——发送方对请求体计算 HMAC,放在 Header 里;接收方收到后重新计算,比对一致才处理。简单、高效、可靠。

核心要点:HMAC 保证的是完整性真实性,不是机密性。它不加密数据,只验证数据有没有被改过。

二、HMAC 的原理——其实不复杂

HMAC 的公式长这样(RFC 2104 标准):

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

别被公式吓到,拆开看就清楚了:

  1. 密钥处理:如果密钥 K 比哈希块长度长,先对 K 做一次哈希,缩短它。如果短,补零到块长度。得到 K'。
  2. 内层填充:K' 与 ipad(0x36 重复)做异或,然后拼接消息 M,做第一次哈希。
  3. 外层填充:K' 与 opad(0x5C 重复)做异或,再拼接内层哈希结果,做第二次哈希。
  4. 输出:最终得到的就是 HMAC 值。

为什么要做两次哈希?嗯,这里要注意——两次哈希能有效抵抗长度扩展攻击。单纯用 H(K || M) 的方式,攻击者可以在不知道 K 的情况下,基于已知的 H(K || M) 伪造出 H(K || M || padding || extra) 的哈希值。HMAC 的双层结构彻底堵死了这个漏洞。

我的经验:曾经有个同事图省事,直接用 H(key || message) 做校验。结果被安全审计发现后,整个模块重写。从那以后,我但凡看到“自定义哈希认证”都会多问一句:“为什么不用标准 HMAC?”

三、HMAC 与哈希函数的区别

很多人容易把 HMAC 和哈希函数搞混。我列个表,一目了然:

对比维度 哈希函数(如 SHA-256) HMAC
输入 任意长度消息 消息 + 密钥
输出 固定长度摘要 固定长度认证码
安全性依赖 抗碰撞性 抗碰撞性 + 密钥保密性
能否防篡改 不能(无密钥,谁都能算) 能(密钥只有双方知道)
典型用途 文件完整性校验、密码存储 API 认证、消息完整性验证

说白了,哈希函数是“公开的指纹”,谁都能算;HMAC 是“带密钥的签名”,只有持有密钥的人才能验证

四、C 语言实现 HMAC-SHA256

下面我给出一个完整的 C 语言实现。这个代码我实际用在过嵌入式设备上,跑在 ARM Cortex-M4 上,性能完全够用。

#include <stdio.h>
#include <string.h>
#include <stdint.h>

// 假设已有 SHA256 实现,这里只声明
void sha256(const uint8_t *data, size_t len, uint8_t *out);

#define SHA256_DIGEST_LENGTH 32
#define SHA256_BLOCK_SIZE    64

void hmac_sha256(const uint8_t *key, size_t key_len,
                 const uint8_t *msg, size_t msg_len,
                 uint8_t *hmac_out) {
    uint8_t k_ipad[SHA256_BLOCK_SIZE];
    uint8_t k_opad[SHA256_BLOCK_SIZE];
    uint8_t temp_key[SHA256_DIGEST_LENGTH];
    uint8_t inner_hash[SHA256_DIGEST_LENGTH];
    uint8_t inner_data[SHA256_BLOCK_SIZE + msg_len];
    uint8_t outer_data[SHA256_BLOCK_SIZE + SHA256_DIGEST_LENGTH];
    size_t i;

    // 1. 密钥处理:如果密钥太长,先哈希
    if (key_len > SHA256_BLOCK_SIZE) {
        sha256(key, key_len, temp_key);
        key = temp_key;
        key_len = SHA256_DIGEST_LENGTH;
    }

    // 2. 初始化 ipad 和 opad
    memset(k_ipad, 0, SHA256_BLOCK_SIZE);
    memset(k_opad, 0, SHA256_BLOCK_SIZE);
    memcpy(k_ipad, key, key_len);
    memcpy(k_opad, key, key_len);

    for (i = 0; i < SHA256_BLOCK_SIZE; i++) {
        k_ipad[i] ^= 0x36;
        k_opad[i] ^= 0x5C;
    }

    // 3. 内层哈希:H(K' ⊕ ipad || M)
    memcpy(inner_data, k_ipad, SHA256_BLOCK_SIZE);
    memcpy(inner_data + SHA256_BLOCK_SIZE, msg, msg_len);
    sha256(inner_data, SHA256_BLOCK_SIZE + msg_len, inner_hash);

    // 4. 外层哈希:H(K' ⊕ opad || inner_hash)
    memcpy(outer_data, k_opad, SHA256_BLOCK_SIZE);
    memcpy(outer_data + SHA256_BLOCK_SIZE, inner_hash, SHA256_DIGEST_LENGTH);
    sha256(outer_data, SHA256_BLOCK_SIZE + SHA256_DIGEST_LENGTH, hmac_out);
}

// 测试用例
int main() {
    uint8_t key[] = "my_secret_key_123";
    uint8_t msg[] = "Hello, HMAC!";
    uint8_t hmac[SHA256_DIGEST_LENGTH];
    char hex[65];
    int i;

    hmac_sha256(key, strlen((char*)key),
                msg, strlen((char*)msg),
                hmac);

    // 转十六进制输出
    for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(hex + i * 2, "%02x", hmac[i]);
    }
    hex[64] = '\0';
    printf("HMAC-SHA256: %s\n", hex);

    return 0;
}

避坑指南:我曾经在项目中犯过一个低级错误——把密钥直接硬编码在代码里,结果代码被反编译后密钥泄露。记住:密钥管理是 HMAC 安全性的基石。生产环境中,密钥应该从安全存储(如 HSM、密钥管理服务)中读取,或者通过安全信道分发。

五、HMAC 的典型应用场景

  • API 签名认证:比如 AWS 的 Signature V4,就是用 HMAC-SHA256 对请求做签名。
  • JWT(JSON Web Token):HS256 算法本质上就是 HMAC-SHA256。
  • 网络协议完整性校验:TLS 1.3 中仍然使用 HMAC 作为伪随机函数的一部分。
  • 固件升级校验:嵌入式设备接收固件包时,用 HMAC 验证固件来源是否可信。

六、知识体系总览

下面这张图总结了 HMAC 的核心逻辑和与哈希函数的关系:

HMAC 知识体系总览 消息 M 密钥 K 密钥处理(补零/哈希) ⊕ ipad 内层哈希 H(K'⊕ipad || M) ⊕ opad 外层哈希 H(K'⊕opad || inner) HMAC 输出 哈希函数:无密钥,公开 用途:完整性校验 输入 哈希运算 异或操作 最终输出

七、总结

HMAC 是安全编程中一个基础但极其重要的工具。它不复杂,但用错了地方或者用错了方式,后果很严重。我个人习惯是:只要涉及消息完整性校验,优先考虑标准 HMAC,绝不自己“发明”认证方案

记住三点:

  • HMAC 需要密钥,哈希函数不需要
  • HMAC 能防篡改,哈希函数不能
  • 永远使用标准实现,别自己造轮子

好,这一讲就到这里。代码可以直接拿去用,但记得把密钥管理好。


公众号:蓝海资料掘金营,微信deep3321