第4章:固件签名与加密
各位同学,今天我们来聊聊固件安全里最核心的一环——签名与加密。说实话,我早年做点钞机固件时,对这块也不太重视。直到有一次,客户反馈机器被刷入了非官方固件,导致计数出错……嗯,从那以后,我再也不敢跳过签名验证了。
4.1 哈希算法:固件的“指纹”
哈希算法,说白了就是给固件算一个唯一的“身份证号”。不管你固件有多大,几十KB还是几MB,经过SHA256一算,输出就是一个256位的固定长度值。
我个人习惯用SHA256,原因很简单:它够安全,而且硬件加速支持很普遍。你在STM32、NXP这些主流MCU上,基本都能找到硬件SHA引擎。
核心要点:哈希是单向的。你没法从哈希值反推出原始数据。所以它适合做完整性校验,但不适合做加密。
我在项目中遇到过一件事:有个同事想用MD5做固件校验,觉得速度快。我劝他换SHA256,他不听。结果后来发现,MD5碰撞攻击已经可以在普通PC上几分钟内完成。你想想看,攻击者伪造一个固件,哈希值却和原厂一样,那验签还有什么意义?
SHA256计算示例(伪代码)
// 伪代码:计算固件哈希
uint8_t firmware_buffer[FIRMWARE_SIZE];
uint8_t hash_output[32]; // SHA256输出32字节
SHA256_CTX ctx;
sha256_init(&ctx);
sha256_update(&ctx, firmware_buffer, FIRMWARE_SIZE);
sha256_final(&ctx, hash_output);
// 此时hash_output就是固件的指纹
// 可以打印成64位十六进制字符串
for(int i = 0; i < 32; i++) {
printf("%02x", hash_output[i]);
}
4.2 非对称加密:RSA与ECC
哈希只能保证固件没被篡改,但没法证明这个固件是谁签的。这时候就需要非对称加密登场了。
非对称加密有两个密钥:私钥(你自己藏好)和公钥(公开给设备)。私钥签名,公钥验签。这个机制,说白了就是“我锁门,你开锁”——只有我能锁,但谁都能检查锁对不对。
| 算法 | 密钥长度 | 签名速度 | 验签速度 | 典型应用 |
|---|---|---|---|---|
| RSA-2048 | 2048位 | 较慢 | 较快 | 传统嵌入式设备 |
| RSA-4096 | 4096位 | 很慢 | 中等 | 高安全场景 |
| ECC-256 | 256位 | 快 | 快 | 资源受限MCU |
| ECC-384 | 384位 | 中等 | 中等 | 金融级设备 |
我个人更推荐ECC。为什么?同样的安全强度,ECC的密钥只有RSA的十分之一。你想想看,点钞机的MCU通常只有几十KB的Flash,省下来的空间可以放更多业务逻辑。
避坑指南:我曾经在选型时犯过一个错——选了RSA-4096,结果MCU验签一次要3秒多。用户按一下升级按钮,得等3秒才有反应。后来换成ECC-256,验签时间降到200毫秒以内。嗯,用户体验完全不一样。
4.3 固件签名流程
签名流程其实不复杂,我画个流程图给你看:
- 计算哈希:对固件二进制做SHA256,得到哈希值H
- 私钥签名:用私钥对H进行加密,生成签名S
- 打包:将固件 + 签名S + 公钥证书(可选)打包成一个升级文件
- 分发:把这个包通过网络或U盘发给点钞机
这里有个细节要注意:签名是对哈希值签名,不是对整个固件签名。为什么?因为固件可能很大,非对称加密处理大文件很慢。哈希相当于把大文件压缩成一个固定长度的摘要,然后再对这个摘要签名。
// 伪代码:固件签名流程(PC端)
uint8_t firmware[] = read_firmware_file("firmware.bin");
uint8_t hash[32];
uint8_t signature[64]; // ECC-256签名长度64字节
// 1. 计算哈希
sha256(firmware, sizeof(firmware), hash);
// 2. 使用私钥签名
ecc_sign(private_key, hash, sizeof(hash), signature);
// 3. 打包输出
write_output("firmware_signed.bin", firmware, sizeof(firmware));
write_output("firmware_signed.bin", signature, sizeof(signature));
// 可选:写入公钥证书
write_output("firmware_signed.bin", certificate, cert_len);
4.4 验签机制:设备端怎么做
设备端验签是反过来的过程:
- 提取签名:从升级包中分离出签名S
- 计算哈希:对固件部分重新计算SHA256,得到H'
- 公钥验签:用公钥解密签名S,得到H;比较H和H'是否一致
- 结果判断:一致则通过,不一致则拒绝升级
警告:公钥一定要烧死在MCU的只读区域(如OTP或安全Flash)。我曾经见过一个产品,公钥存在外部Flash里,结果攻击者直接替换了公钥,用自己的私钥签名固件……那验签就形同虚设了。
// 伪代码:设备端验签
uint8_t* received_firmware = ...; // 从升级包获取
uint8_t* received_signature = ...; // 从升级包获取
uint8_t public_key[32]; // 从MCU安全区读取
uint8_t hash_computed[32];
sha256(received_firmware, firmware_size, hash_computed);
// 验签:返回0表示成功,非0表示失败
int result = ecc_verify(public_key,
hash_computed, sizeof(hash_computed),
received_signature, sizeof(received_signature));
if(result == 0) {
// 验签通过,可以刷写固件
flash_erase_and_write(received_firmware, firmware_size);
printf("固件验证通过,开始升级...\n");
} else {
// 验签失败,拒绝升级
printf("错误:固件签名无效!\n");
return ERROR_SIGNATURE_INVALID;
}
4.5 实际项目中的避坑指南
做了这么多年固件,我总结了几条血泪教训:
- 私钥保管:私钥绝对不能放在代码仓库里。我见过有人把私钥硬编码在Git里,结果离职员工带走了整个仓库……建议用HSM(硬件安全模块)或密钥管理服务。
- 版本兼容:旧设备可能只支持RSA-2048,新设备支持ECC-256。升级时要兼容处理,不能一刀切。
- 验签失败处理:不要直接死机。我建议保留旧固件继续运行,同时记录错误日志。点钞机是金融设备,不能因为升级失败就罢工。
- 防回滚:签名只能保证固件是官方的,但没法防止用户刷回旧版本。如果旧版本有安全漏洞,那就危险了。建议在固件头里加版本号,验签通过后再检查版本是否大于等于当前版本。
总结一下:固件签名不是可选项,而是必选项。尤其是点钞机这种涉及金融安全的设备,没有签名验证,就等于把大门敞开让攻击者进来。哈希保证完整性,非对称加密保证真实性,两者结合,才能构建一个安全的升级链路。
好了,这一章的内容就到这里。下一章我们会讲固件加密传输——如何防止升级包在传输过程中被窃听。到时候见。