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 知识体系图
下面这张图展示了哈希计算在整个可信启动流程中的位置:
4.7 总结
哈希计算看起来简单,但细节决定成败。我给你的建议是:
- 用SHA-256,别用MD5或SHA-1
- 哈希值要跟固件绑定存储
- 校验时用恒定时间比较
- 注意文件编码和换行符