2、固件完整性基础:完整性定义、哈希算法原理(SHA-256)、数字签名基础

聊固件安全,第一个绕不开的概念就是「完整性」。

很多人把完整性和机密性搞混。机密性是「不让别人看」,完整性是「不让别人改」——哪怕改了你也得知道。我见过不少项目,加密做得滴水不漏,但固件被篡改了却毫无察觉。嗯,这其实更危险。

2.1 完整性的定义

完整性,说白了就是:数据从产生到使用,中间没有被篡改过。

在固件场景下,完整性要保证三件事:

  • 源可信:固件确实来自你信任的发布方
  • 途未改:固件在传输、存储过程中没被插入恶意代码
  • 可验证:设备启动时能自己检查上述两点

核心观点:完整性 ≠ 安全性。完整性只是安全的基础前提。没有完整性,加密、认证都是空中楼阁。

我早期做过一个 IoT 项目,OTA 升级包只做了 AES 加密,没做完整性校验。结果黑客虽然解不开密文,但通过翻转某些比特位,让设备解压后执行了错误逻辑——整批设备变砖。那次之后,我所有项目都把完整性校验放在加密前面。

2.2 哈希算法原理

完整性校验最常用的工具就是哈希算法(Hash)。

哈希函数能把任意长度的数据,映射成一个固定长度的摘要(digest)。这个摘要就像数据的「指纹」——内容变一点点,指纹就完全不一样。

2.2.1 哈希算法的核心性质

性质 含义 为什么重要
确定性 同一输入,永远得到同一输出 验证时才能比对
单向性 从摘要反推原始数据,计算上不可行 摘要泄露不会暴露固件内容
抗碰撞性 找不到两个不同输入产生相同摘要 防止伪造固件通过校验
雪崩效应 输入改1比特,输出约一半比特翻转 微小篡改能被检测

我的习惯:选哈希算法时,优先看「抗碰撞性」是否已被攻破。MD5 和 SHA-1 在理论上已被证明可构造碰撞,坚决不用。

2.2.2 SHA-256 原理

SHA-256 是 SHA-2 家族的一员,输出 256 位(32 字节)摘要。目前仍是工业界最广泛使用的完整性哈希算法。

它的处理流程大致分三步:

  1. 填充:在消息末尾补位,使长度满足 512 位的整数倍
  2. 分块:将填充后的消息分成 512 位(64 字节)的块
  3. 压缩:每个块经过 64 轮迭代压缩,更新 8 个 32 位工作变量

最终这 8 个变量拼接起来,就是 256 位的摘要。

你想想看,64 轮迭代意味着什么?哪怕输入只差一个比特,经过这么多轮扩散,输出早就面目全非了。这就是雪崩效应的工程实现。

我曾经踩过的坑:在某个资源受限的 MCU 上,我直接用软件实现了完整的 SHA-256。结果每次校验耗时 3 秒,用户按电源键都等不及。后来改用硬件哈希加速器,耗时降到 50 毫秒。所以——能用硬件就别用软件,尤其是启动路径上的校验。

2.2.3 代码示例:计算固件 SHA-256 摘要

下面是一个使用 OpenSSL 库计算固件文件 SHA-256 的 C 代码片段。实际嵌入式环境中,你会用硬件驱动或 mbedTLS 的替代接口。

#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

void compute_firmware_sha256(const char *filepath) {
    FILE *fp = fopen(filepath, "rb");
    if (!fp) {
        perror("fopen");
        return;
    }

    SHA256_CTX ctx;
    SHA256_Init(&ctx);

    unsigned char buffer[4096];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), fp)) != 0) {
        SHA256_Update(&ctx, buffer, bytes);
    }

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_Final(hash, &ctx);

    printf("SHA-256: ");
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        printf("%02x", hash[i]);
    }
    printf("\n");

    fclose(fp);
}

注意:实际启动校验中,你通常不会在设备上重新计算整个固件的哈希——太慢了。更常见的做法是只校验关键区域(如引导加载程序、签名表),或者使用「分块哈希 + 哈希树」的方式。

2.3 数字签名基础

哈希能检测篡改,但解决不了一个问题:你怎么知道哈希值本身是可信的?

攻击者完全可以替换固件的同时,也替换掉你存储的哈希值。这时候就需要数字签名出场了。

2.3.1 数字签名的工作流程

数字签名基于非对称加密。简单说就是:

  1. 签名方(固件发布者)用私钥对固件的哈希值加密,生成签名
  2. 验证方(设备)用公钥解密签名,得到原始哈希值
  3. 设备再自己计算固件的哈希值,两者比对

如果一致,说明固件没被改,而且确实来自持有私钥的发布者。

关键点:数字签名同时提供了完整性和源认证。哈希只解决前者,签名把两者都解决了。

2.3.2 常见签名算法对比

算法 密钥长度 签名大小 计算开销 安全性
RSA-2048 2048 位 256 字节 验证较快,签名慢 高(广泛使用)
ECDSA P-256 256 位 64 字节 验证快,签名快 高(推荐)
Ed25519 256 位 64 字节 极快 极高(现代选择)

我个人偏好 ECDSA P-256 或 Ed25519。RSA 虽然成熟,但签名太大,在存储受限的 MCU 上不太友好。ECDSA 的 64 字节签名,放在启动镜像头部刚刚好。

2.3.3 一个常见的误解

有人觉得「我用了签名,固件就是安全的」。不对。签名只保证固件没被篡改且来源可信,但它不加密固件内容。如果你需要保密,还得在签名之上做加密。

我的建议:启动校验链路中,签名是必须的,加密是可选的。先把签名做对,再考虑加密。很多安全标准(如 NIST SP 800-193)也只要求启动时做完整性验证,不强制加密。

2.4 本章知识体系图

下面这张图展示了完整性、哈希、签名三者之间的关系,以及它们在固件启动校验中的位置。

固件完整性知识体系 固件完整性 哈希算法 (SHA-256) 数字签名 (ECDSA) 检测篡改 源认证 + 防篡改 固件发布签名 启动时校验 安全OTA更新 说明: 哈希算法提供篡改检测能力,但无法验证来源。 数字签名在哈希基础上增加源认证,是启动校验的核心技术。 实际产品中,SHA-256 + ECDSA 是最常见的组合。

2.5 小结

这一章我们拆解了三个层层递进的概念:

  • 完整性是目标——确保固件没被改
  • 哈希算法是工具——SHA-256 是目前最稳妥的选择
  • 数字签名是保障——在哈希之上加了源认证,防止伪造