4. 固件打包与签名:固件镜像格式、CRC校验、哈希摘要、数字签名(RSA/ECDSA)原理

好,咱们进入第四章。这一章讲的是固件在“出厂”前,最后一道关键工序——打包与签名。

你想想看,OTA升级最怕什么?最怕升级到一半,发现固件坏了,或者更糟——升级了一个被篡改过的恶意固件,设备直接变“砖”。我早年做一款智能门锁时就吃过这个亏,升级包校验没做好,结果远程升级后门锁彻底罢工,最后只能派人上门拆机重刷。从那以后,我对固件打包和签名这件事,就再也不敢马虎了。

4.1 固件镜像格式:不只是把二进制堆在一起

很多人觉得固件镜像就是把编译出来的 .bin 文件直接扔过去。其实没那么简单。一个完整的固件包,通常包含多个部分。

我习惯把固件镜像分成三个区域:

  • 镜像头(Image Header):描述固件的基本信息,比如版本号、芯片型号、固件大小、校验值等。
  • 固件数据(Firmware Data):真正的可执行代码和只读数据。
  • 尾部信息(Trailer):有时会附加签名、哈希值或填充字节。

下面是一个我在项目中常用的镜像头结构,你可以参考一下:

typedef struct {
    uint32_t magic;          // 魔数,用于识别固件格式,比如 0xAA55AA55
    uint32_t version;        // 固件版本号,用于版本比较
    uint32_t size;           // 固件数据区大小(不含头部)
    uint32_t crc32;          // 整个固件的 CRC32 校验值
    uint8_t  hash[32];       // SHA-256 哈希摘要
    uint8_t  signature[256]; // RSA-2048 数字签名
    uint32_t reserved[8];    // 保留字段,用于扩展
} firmware_header_t;

嗯,这里要注意:魔数很重要。我见过有人忘记校验魔数,结果把别的芯片的固件刷了进来,设备直接死机。魔数就是第一道防线。

4.2 CRC校验:快速检测传输错误

CRC(循环冗余校验)是OTA升级中最常用的校验方式之一。它的特点是计算快、硬件支持好,很多MCU都有CRC硬件外设。

说白了,CRC就是把整个固件当成一个大数字,然后除以一个固定的多项式,得到的余数就是CRC值。接收端用同样的方法计算,如果结果一致,说明数据在传输过程中没有被破坏。

关键点:CRC只能检测随机错误,不能防止恶意篡改。如果有人故意修改了固件并重新计算CRC,你根本发现不了。所以CRC只用于传输完整性校验,不能用于安全校验。

我在项目中通常这样用:

  • 发送端:计算整个固件的CRC32,写入镜像头。
  • 接收端:下载完成后,重新计算CRC32,与头部中的值比对。
  • 如果不一致:直接丢弃固件,请求重传。

这里有个小技巧:CRC计算时,记得把头部中的CRC字段本身置为0,否则算出来的值会包含自身,导致校验永远对不上。我曾经在这个坑里爬了半小时。

4.3 哈希摘要:给固件打上“指纹”

CRC虽然快,但不够安全。哈希摘要(Hash Digest)就不同了。它能把任意长度的数据,映射成一个固定长度的摘要值。而且,哪怕只改了一个比特,摘要值也会完全不一样。

常用的哈希算法有:

算法 摘要长度 安全性 适用场景
MD5 128位(16字节) 已破解,不推荐 仅用于非安全场景
SHA-1 160位(20字节) 已破解,不推荐 仅用于兼容旧系统
SHA-256 256位(32字节) 安全 推荐用于OTA升级
SHA-512 512位(64字节) 更安全 资源充足的场景

我个人建议,在MCU OTA中优先使用SHA-256。它足够安全,计算速度也能接受。我在STM32F4上实测过,计算256KB的固件,SHA-256大概需要200ms左右,完全在可接受范围内。

提示:哈希摘要只能保证数据没有被修改,但不能保证数据来自合法的发布者。因为攻击者也可以计算哈希值。要解决“谁发的”这个问题,就需要数字签名了。

4.4 数字签名:RSA与ECDSA原理

数字签名是OTA安全的最后一道防线。它的核心思想是:私钥签名,公钥验签。

你想想看,固件发布者持有私钥,对固件的哈希值进行签名,生成一个签名值。设备端只保存公钥,用公钥验证签名。只要私钥不泄露,任何人都无法伪造合法的固件。

4.4.1 RSA签名

RSA是目前最广泛使用的非对称加密算法。它的签名过程大致如下:

  1. 计算固件的SHA-256哈希值。
  2. 用私钥对哈希值进行加密(实际是签名操作),得到签名。
  3. 将签名附加到固件包中。

验签过程:

  1. 接收端计算固件的SHA-256哈希值。
  2. 用公钥解密签名,得到原始的哈希值。
  3. 比对两个哈希值,一致则验签通过。

RSA的优点是成熟、稳定,几乎所有MCU都有现成的库支持。但缺点是签名长(RSA-2048签名256字节),而且验签慢。我在STM32H7上测试,RSA-2048验签一次大约需要50ms,对于OTA升级来说还能接受。

4.4.2 ECDSA签名

ECDSA(椭圆曲线数字签名算法)是RSA的现代替代方案。它的最大优势是:同样的安全强度下,密钥更短,签名更小,计算更快。

对比项 RSA-2048 ECDSA P-256
安全强度 112位 128位
公钥长度 256字节 64字节
签名长度 256字节 64字节
验签速度 较慢 较快

我个人更倾向于在资源受限的MCU上使用ECDSA P-256。签名只有64字节,比RSA少了四分之三。而且验签速度更快,对用户体验更好。

警告:私钥必须妥善保管!我见过有人把私钥硬编码在PC端的升级工具里,结果工具被反编译,私钥泄露,所有设备都面临被攻击的风险。正确的做法是:私钥存储在离线环境或硬件安全模块(HSM)中,公钥才烧录到设备里。

4.5 整体流程:从编译到签名

好了,我们把上面的知识点串起来,看看一个完整的固件打包与签名流程是什么样的。

固件打包与签名流程 1. 编译固件 2. 计算SHA-256哈希 3. 私钥签名哈希 4. 打包成镜像 5. 发布到OTA服务器 关键说明 • 私钥:服务器端保管 • 公钥:烧录在MCU中 • 镜像头包含: 魔数 + 版本 + CRC + 哈希 + 签名

这个流程看起来简单,但每一步都有坑。我总结了几条经验:

  • 先哈希,后签名:签名的是哈希值,不是整个固件。否则签名会变得非常大。
  • 版本号要递增:防止回滚攻击。我见过有人忘记检查版本号,结果设备被刷回了有漏洞的旧版本。
  • 公钥要防篡改:公钥通常烧录在MCU的一次性可编程(OTP)区域,或者用硬件唯一密钥保护。

4.6 避坑指南

我曾经踩过的坑:

  • 签名放在固件数据区:有一次我把签名放在了固件数据区,结果计算哈希时把签名也算进去了,导致验签永远失败。正确的做法是:签名放在镜像头中,计算哈希时排除签名区域。
  • 公钥硬编码在代码中:公钥虽然可以公开,但硬编码在代码中容易被替换。我后来改用OTP区域存储公钥的哈希值,启动时校验公钥的完整性。
  • 忽略CRC:有人觉得有签名就够了,不需要CRC。但签名验签比较慢,不能每包都验。CRC可以快速检测传输错误,和签名配合使用,效率更高。