4. 固件签名流程详解:签名前的准备(哈希计算)、签名生成(私钥签名)、签名验证(公钥验证)、完整签名流程拆解
固件签名这件事,说白了就是给固件贴上一张「防伪封条」。这张封条有两个作用:一是证明固件确实来自你(身份认证),二是证明固件没有被篡改过(完整性校验)。
我刚开始做嵌入式安全时,总觉得签名就是个数学魔术。后来踩过几次坑才明白,整个流程其实就三步:哈希 → 签名 → 验证。每一步都有讲究,咱们一个一个拆开看。
核心要点:签名不是加密,而是「哈希 + 非对称密钥」的组合拳。哈希保证内容完整性,私钥签名保证身份真实性。
4.1 签名前的准备:哈希计算
为什么签名前要先算哈希?你想想看,固件动辄几兆甚至几十兆,直接用私钥对整个固件做签名运算,性能扛不住。所以我们的做法是:先对固件算一个固定长度的哈希值,再对这个哈希值签名。
哈希函数的选择很关键。我个人习惯用 SHA-256,原因很简单:它足够安全,而且几乎所有硬件平台都支持硬件加速。SHA-1 已经被证明有碰撞风险,MD5 就更不用说了——我在一个老项目里见过还在用 MD5 的,当时就建议客户赶紧升级。
// 伪代码:计算固件哈希
#include <openssl/sha.h>
uint8_t firmware_buffer[FIRMWARE_SIZE];
uint8_t hash_output[SHA256_DIGEST_LENGTH];
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, firmware_buffer, FIRMWARE_SIZE);
SHA256_Final(hash_output, &ctx);
// 此时 hash_output 就是 32 字节的固件指纹
我的经验:哈希计算时,建议把固件的元数据(版本号、芯片ID、时间戳)也一起算进去。这样签名就绑定了特定版本和特定设备,防止固件被挪用到其他设备上。
4.2 签名生成:私钥签名
拿到哈希值之后,就该私钥上场了。私钥签名本质上就是用你的私钥对哈希值做一次「加密」——注意这里的引号,严格来说不是加密,而是签名算法。
常用的签名算法有两种:RSA 和 ECDSA。我做过对比,给你列个表:
| 特性 | RSA (2048位) | ECDSA (P-256) |
|---|---|---|
| 签名长度 | 256 字节 | 64 字节 |
| 签名速度 | 较慢 | 较快 |
| 验证速度 | 较快 | 较慢 |
| 密钥生成 | 慢 | 快 |
| 硬件支持 | 广泛 | 逐渐普及 |
我个人更倾向于 ECDSA,尤其是资源受限的嵌入式设备。签名短、生成快,虽然验证稍慢,但固件升级场景下验证次数远少于签名次数。
// 伪代码:使用 ECDSA 签名
EVP_PKEY *pkey = load_private_key("private_key.pem");
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey);
EVP_DigestSignUpdate(mdctx, hash_output, SHA256_DIGEST_LENGTH);
size_t sig_len;
EVP_DigestSignFinal(mdctx, NULL, &sig_len); // 获取签名长度
uint8_t *signature = malloc(sig_len);
EVP_DigestSignFinal(mdctx, signature, &sig_len);
// signature 就是最终的签名数据
注意:私钥必须妥善保管!我曾经见过一个团队把私钥直接放在 Git 仓库里,结果被外部人员拿到,整个产品线的固件都被伪造了。私钥应该存储在 HSM(硬件安全模块)或安全芯片中,至少也要加密存储。
4.3 签名验证:公钥验证
设备端拿到固件和签名后,怎么验证?很简单:用公钥解密签名,得到哈希值,再和固件自己算出来的哈希值对比。如果一致,说明固件没被改过,而且确实是私钥持有者签发的。
这里有个细节:公钥怎么安全地放到设备里?我建议在产线烧录时就把公钥写进一次性可编程(OTP)区域,或者用证书链的方式分发。千万别把公钥写在固件里——那等于没锁门。
// 伪代码:验证签名
EVP_PKEY *pubkey = load_public_key_from_otp();
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey);
EVP_DigestVerifyUpdate(mdctx, hash_output, SHA256_DIGEST_LENGTH);
int ret = EVP_DigestVerifyFinal(mdctx, signature, sig_len);
if (ret == 1) {
// 验证通过,固件可信
} else {
// 验证失败,拒绝启动
}
4.4 完整签名流程拆解
好了,把上面三步串起来,就是完整的固件签名流程。我画了一张图,帮你理清整个链条:
整个流程走下来,你会发现一个关键原则:私钥永不离开签名端。设备端只存公钥,公钥泄露了也没关系——它只能用来验证,不能用来签名。
我的建议:在构建服务器上做签名时,一定要把签名步骤做成独立的流水线阶段。我见过有人把私钥路径写死在构建脚本里,结果一次误操作把私钥打印到了日志中。正确的做法是用环境变量或密钥管理服务来传递私钥路径。
4.5 避坑指南
做固件签名联调时,有几个坑我反复踩过,今天一并告诉你:
- 哈希算法不一致:签名端用 SHA-256,验证端用 SHA-1,结果验证永远失败。检查算法 ID 是否匹配。
- 签名格式问题:ECDSA 签名有 DER 编码和原始编码两种格式。我建议统一用 DER 编码,兼容性更好。
- 字节序问题:有些 MCU 是大端,有些是小端。签名数据在传输时一定要约定好字节序,否则验证端解出来是乱码。
- 公钥存储位置:别把公钥放在可擦写的 Flash 区域。万一攻击者改了公钥,你的签名体系就形同虚设了。
曾经有一次:我在一个项目里发现设备验证签名总是失败,查了两天才发现是签名时把固件版本号也算进了哈希,但验证端用的版本号是从另一个地方读的,两个版本号差了 1。从那以后,我要求签名和验证必须用同一份元数据。
好了,签名流程的核心就这些。记住:哈希是基础,私钥是核心,公钥是门锁。把这三样管好了,固件安全就有了八成把握。