第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 固件签名流程

签名流程其实不复杂,我画个流程图给你看:

  1. 计算哈希:对固件二进制做SHA256,得到哈希值H
  2. 私钥签名:用私钥对H进行加密,生成签名S
  3. 打包:将固件 + 签名S + 公钥证书(可选)打包成一个升级文件
  4. 分发:把这个包通过网络或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 验签机制:设备端怎么做

设备端验签是反过来的过程:

  1. 提取签名:从升级包中分离出签名S
  2. 计算哈希:对固件部分重新计算SHA256,得到H'
  3. 公钥验签:用公钥解密签名S,得到H;比较H和H'是否一致
  4. 结果判断:一致则通过,不一致则拒绝升级

警告:公钥一定要烧死在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。升级时要兼容处理,不能一刀切。
  • 验签失败处理:不要直接死机。我建议保留旧固件继续运行,同时记录错误日志。点钞机是金融设备,不能因为升级失败就罢工。
  • 防回滚:签名只能保证固件是官方的,但没法防止用户刷回旧版本。如果旧版本有安全漏洞,那就危险了。建议在固件头里加版本号,验签通过后再检查版本是否大于等于当前版本。

总结一下:固件签名不是可选项,而是必选项。尤其是点钞机这种涉及金融安全的设备,没有签名验证,就等于把大门敞开让攻击者进来。哈希保证完整性,非对称加密保证真实性,两者结合,才能构建一个安全的升级链路。

好了,这一章的内容就到这里。下一章我们会讲固件加密传输——如何防止升级包在传输过程中被窃听。到时候见。