4. 固件哈希计算

哈希计算,说白了就是给固件打个「数字指纹」。

我刚开始做嵌入式安全那会儿,总觉得哈希就是个简单的校验和。后来踩过坑才明白——哈希是可信启动的基石。你想想看,如果连固件的完整性都保证不了,那签名还有什么意义?

4.1 为什么需要哈希?

先说说我的理解。固件在传输、存储过程中,可能被篡改,也可能因为存储介质老化而出错。哈希的作用就是:

  • 完整性验证:确保固件没被改过
  • 防篡改检测:哪怕改了一个比特,哈希值都会变
  • 签名前置:签名是对哈希值签的,不是对固件本身

核心概念:哈希是单向函数。你可以从固件算出哈希,但不可能从哈希反推出固件。这就是「单向性」。

4.2 使用OpenSSL计算哈希

我个人习惯用SHA-256。为什么?因为SHA-1已经被证明有碰撞风险,而SHA-256目前还是安全的。

先看一个最简单的例子:

# 计算固件文件的SHA-256哈希
openssl dgst -sha256 firmware.bin

输出大概是这样的:

SHA256(firmware.bin)= a1b2c3d4e5f6...(64个十六进制字符)

嗯,这里要注意——默认输出是十六进制格式。如果你需要二进制格式,加个 -binary 参数:

# 输出二进制格式的哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash

我在项目中遇到过一个问题:固件文件很大,比如几十兆。这时候计算哈希会不会很慢?其实不用担心,OpenSSL的哈希计算是流式的,内存占用很小。

4.3 哈希值的存储

哈希值算出来了,存哪儿?这是个好问题。

我见过三种常见的存储方式:

存储方式 说明 适用场景
单独文件 把哈希值存成 .hash 或 .sha256 文件 开发调试阶段
嵌入固件头 在固件镜像的头部预留哈希字段 生产环境
安全存储区 存入OTP或eFuse 高安全要求场景

我个人推荐第二种——嵌入固件头。为什么呢?因为这样哈希值和固件是绑定的,不会出现「固件和哈希文件对不上」的尴尬情况。

举个例子,固件头结构可以这样设计:

// 固件头结构
typedef struct {
    uint32_t magic;          // 魔数,用于识别
    uint32_t version;        // 固件版本
    uint32_t length;         // 固件长度
    uint8_t  hash[32];       // SHA-256哈希值
    uint8_t  signature[256]; // RSA签名
    uint8_t  firmware[];     // 固件数据
} firmware_header_t;

4.4 哈希值的校验

校验其实很简单——重新算一遍哈希,然后跟存储的哈希值比较。

用OpenSSL校验的脚本:

#!/bin/bash
# 计算当前固件的哈希
CURRENT_HASH=$(openssl dgst -sha256 firmware.bin | awk '{print $2}')

# 读取存储的哈希
STORED_HASH=$(cat firmware.sha256)

# 比较
if [ "$CURRENT_HASH" == "$STORED_HASH" ]; then
    echo "✅ 固件完整性验证通过"
else
    echo "❌ 固件已被篡改或损坏"
    exit 1
fi

注意:比较哈希值时,一定要用恒定时间比较。用 strcmp 或 memcmp 可能会有时序攻击风险。我曾经见过一个产品,就是因为用了普通的字符串比较,被攻击者通过时序分析绕过了校验。

4.5 实战中的坑

说几个我踩过的坑:

  • 换行符问题:Windows和Linux的换行符不同,会导致哈希值不同。我建议统一用二进制模式处理文件。
  • 填充问题:有些工具会在文件末尾加填充字节。记得在计算哈希前确认文件的实际大小。
  • 大小写问题:十六进制哈希值的大小写要统一。我习惯用小写。

小技巧:可以用 xxd 或 hexdump 查看二进制哈希值。我经常用这个来调试哈希计算是否正确。

4.6 知识体系图

下面这张图展示了哈希计算在整个可信启动流程中的位置:

固件哈希计算知识体系 固件文件 SHA-256计算 (OpenSSL) 哈希值 单独文件存储 嵌入固件头 重新计算并比较 BootROM自动校验

4.7 总结

哈希计算看起来简单,但细节决定成败。我给你的建议是:

  • 用SHA-256,别用MD5或SHA-1
  • 哈希值要跟固件绑定存储
  • 校验时用恒定时间比较
  • 注意文件编码和换行符