3、消息认证码(MAC):HMAC原理、基于密钥的完整性校验、HMAC与哈希的区别

聊完哈希,咱们得面对一个现实问题——哈希只能保证数据没被改过,但它没法证明是谁改的。你想想看,攻击者把固件内容和哈希值一起换了,你根本发现不了。这就是所谓的「哈希完整性校验的致命短板」。

我在做一款IoT网关的固件升级模块时,就踩过这个坑。当时只用了SHA256做完整性校验,结果测试环境一切正常,到了客户现场,设备频繁变砖。后来一查,是中间人把固件包整个替换了,哈希值也跟着换了。嗯,从那以后,我再也不敢只用哈希了。

3.1 为什么需要消息认证码?

消息认证码(MAC)的核心思路很简单——给数据加一把锁,钥匙只有你和验证方知道。

哈希是公开的,谁都能算。MAC不一样,它需要一把共享密钥。只有持有密钥的人,才能生成正确的MAC值。这样一来,攻击者就算能篡改数据,也伪造不出合法的MAC。

核心区别一句话:

  • 哈希:防篡改,但不防伪造
  • MAC:防篡改 + 防伪造,因为有密钥

3.2 HMAC原理——最流行的MAC实现

HMAC(Hash-based Message Authentication Code)是目前最广泛使用的MAC算法。它不发明新的哈希函数,而是把现有的哈希函数(如SHA256)包装一层密钥逻辑。

HMAC的计算公式长这样:

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

别被公式吓到,我拆开给你看:

  1. 密钥处理:把密钥K填充或哈希到哈希函数的分组长度(比如SHA256是64字节)
  2. 内层填充:用处理后的密钥异或ipad(0x36重复),然后拼接消息M,做一次哈希
  3. 外层填充:用处理后的密钥异或opad(0x5c重复),再拼接内层哈希结果,做第二次哈希

为什么要做两次哈希?说白了,就是为了防止长度扩展攻击。我见过一些新手工程师自己实现MAC时只做一次哈希,结果被攻击者轻松绕过。

我的经验:在嵌入式固件中,我习惯用HMAC-SHA256。SHA256的32字节输出长度足够安全,计算速度在Cortex-M4上实测大约2-3微秒/字节,完全够用。

3.3 基于密钥的完整性校验流程

在固件升级场景中,HMAC的典型用法是这样的:

  1. 固件发布端:用共享密钥K计算固件镜像的HMAC值,把固件 + HMAC一起打包发送
  2. 设备端:收到固件后,用同样的密钥K重新计算HMAC,对比收到的HMAC值
  3. 校验结果:一致则固件可信,不一致则拒绝升级

这里有个关键点——密钥怎么安全地存到设备里?我在项目中踩过这个坑。当时直接把密钥硬编码在固件里,结果攻击者通过调试接口读出了固件二进制,密钥直接暴露。

避坑指南:

  • 密钥不要硬编码在固件中,建议使用芯片的OTP(一次性可编程)区域或安全元件存储
  • 每个设备使用不同的密钥,防止一个设备被攻破导致所有设备沦陷
  • 密钥长度至少128位(16字节),推荐256位(32字节)

3.4 HMAC与哈希的区别

很多初学者会把HMAC和哈希搞混。我整理了一张对比表,你看完就明白了:

对比维度 哈希(如SHA256) HMAC(如HMAC-SHA256)
是否需要密钥 不需要 需要共享密钥
防篡改 ✅ 能检测数据是否被改 ✅ 能检测数据是否被改
防伪造 ❌ 攻击者可同时替换数据和哈希 ✅ 无密钥无法伪造合法MAC
抗长度扩展攻击 ❌ 部分哈希算法存在此漏洞 ✅ 双重哈希结构天然免疫
计算速度 较快(少一次异或操作) 稍慢(多一次异或和一次哈希)
典型应用场景 文件完整性校验、密码存储 固件认证、API请求签名、TLS

说白了,哈希是「有没有被改过」,HMAC是「是不是你改的」。在固件完整性校验这个场景里,我们既要防篡改,也要防伪造,所以HMAC是更合适的选择。

3.5 HMAC在嵌入式固件中的实现要点

如果你要在MCU上实现HMAC,我建议直接用现成的加密库,别自己造轮子。常用的有:

  • mbedTLS:ARM出品,对Cortex-M系列优化很好,代码量可控
  • WolfSSL:轻量级,适合资源受限的设备
  • OpenSSL:功能全,但体积大,不太适合小Flash的MCU

下面是一个用mbedTLS计算HMAC-SHA256的示例:

#include "mbedtls/md.h"

// 计算HMAC-SHA256
int calc_hmac_sha256(const uint8_t *key, size_t key_len,
                     const uint8_t *data, size_t data_len,
                     uint8_t *output) {
    mbedtls_md_context_t ctx;
    const mbedtls_md_info_t *md_info;

    mbedtls_md_init(&ctx);
    md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);

    // 设置HMAC密钥
    mbedtls_md_setup(&ctx, md_info, 1);  // 1表示使用HMAC模式
    mbedtls_md_hmac_starts(&ctx, key, key_len);

    // 更新数据
    mbedtls_md_hmac_update(&ctx, data, data_len);

    // 获取结果
    mbedtls_md_hmac_finish(&ctx, output);

    mbedtls_md_free(&ctx);
    return 0;
}

这段代码我在好几个项目里直接复用,稳定可靠。注意mbedtls_md_setup的第三个参数要传1,否则它只会算普通哈希,不会走HMAC逻辑。

3.6 HMAC的局限性

HMAC不是万能的。它有一个根本问题——密钥分发。如果固件发布端和设备端共享同一个密钥,那这个密钥怎么安全地部署到所有设备上?

我遇到过最头疼的情况:某款产品出货10万台,每台都要烧录同一个密钥。结果生产线上有人把密钥泄露了,整个产品线的固件认证全部失效。

解决方案有两个方向:

  • 每个设备独立密钥:生产时烧录唯一密钥,服务器端维护密钥数据库。安全性高,但管理成本大
  • 结合非对称加密:用HMAC做完整性校验,用数字签名做身份认证。这是更推荐的做法,我们下一章会详细讲

我的建议:在固件升级场景中,不要把HMAC当作唯一的认证手段。它适合做「轻量级完整性校验」,但如果要对抗高等级攻击者,还是得上数字签名。

3.7 本章小结

HMAC是哈希的升级版,它用一把共享密钥解决了哈希的「谁都能伪造」问题。在嵌入式固件中,HMAC-SHA256是性价比很高的选择——安全强度够,计算开销小,实现也简单。

但记住,密钥的安全存储才是HMAC的命门。密钥丢了,HMAC就形同虚设。我建议你在设计固件安全方案时,把密钥管理当作第一优先级来考虑。

HMAC核心逻辑流程图 共享密钥 K 密钥填充到分组长度 内层:K' ⊕ ipad || M 第一次哈希 外层:K' ⊕ opad || H1 第二次哈希 内层哈希结果 H1 最终HMAC值 消息 M(固件数据) 关键设计要点 • 两次哈希防止长度扩展攻击 • ipad = 0x36重复,opad = 0x5c重复 • 密钥长度不足时填充0,超过时先哈希再填充