3. 消息认证码(MAC)
聊完哈希,咱们得面对一个现实问题——哈希只能保证数据没被改过,但它没法证明是谁改的。你想想看,攻击者把固件内容和哈希值一起换了,你根本发现不了。这就是所谓的「哈希完整性校验的致命短板」。
我在做一款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 ) )
别被公式吓到,我拆开给你看:
- 密钥处理:把密钥K填充或哈希到哈希函数的分组长度(比如SHA256是64字节)
- 内层填充:用处理后的密钥异或ipad(0x36重复),然后拼接消息M,做一次哈希
- 外层填充:用处理后的密钥异或opad(0x5c重复),再拼接内层哈希结果,做第二次哈希
为什么要做两次哈希?说白了,就是为了防止长度扩展攻击。我见过一些新手工程师自己实现MAC时只做一次哈希,结果被攻击者轻松绕过。
我的经验:在嵌入式固件中,我习惯用HMAC-SHA256。SHA256的32字节输出长度足够安全,计算速度在Cortex-M4上实测大约2-3微秒/字节,完全够用。
3.3 基于密钥的完整性校验流程
在固件升级场景中,HMAC的典型用法是这样的:
- 固件发布端:用共享密钥K计算固件镜像的HMAC值,把固件 + HMAC一起打包发送
- 设备端:收到固件后,用同样的密钥K重新计算HMAC,对比收到的HMAC值
- 校验结果:一致则固件可信,不一致则拒绝升级
这里有个关键点——密钥怎么安全地存到设备里?我在项目中踩过这个坑。当时直接把密钥硬编码在固件里,结果攻击者通过调试接口读出了固件二进制,密钥直接暴露。
避坑指南:
- 密钥不要硬编码在固件中,建议使用芯片的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就形同虚设。我建议你在设计固件安全方案时,把密钥管理当作第一优先级来考虑。