4. 升级包制作与签名:差分升级算法、哈希校验、数字签名流程
好,咱们进入升级方案里最核心的一环——升级包怎么做,怎么保证它安全可靠。
说实话,我刚入行那会儿,升级包就是直接把完整固件扔过去。后来在电力项目里吃过亏——一个终端在偏远变电站,网络差得要命,传一个2MB的完整包要半小时,还经常断。从那以后,我就彻底转向差分升级了。
4.1 差分升级算法:bsdiff 实战
差分升级的核心思想很简单:只传变化的部分。你想想看,智能电表的固件,每次改的可能就几个功能模块,90%以上的代码都没动。何必每次都传完整包?
目前工业界用得最多的差分算法是 bsdiff。它基于后缀排序,能高效找出新旧文件之间的差异。
bsdiff 的工作流程:
- 扫描:对比旧固件 v1.0 和新固件 v2.0
- 匹配:找出相同的字节块,记录偏移量
- 编码:只存储「新增/修改/删除」的部分
- 压缩:用 bzip2 进一步压缩差分数据
我个人的习惯是,在服务器端用 bsdiff 生成补丁包,终端上跑 bspatch 来还原。下面给个实际命令示例:
# 生成差分补丁包(在服务器上执行)
bsdiff old_firmware_v1.0.bin new_firmware_v2.0.bin upgrade_patch.diff
# 查看补丁包大小
ls -lh upgrade_patch.diff
# 输出示例:-rw-r--r-- 1 root root 128K Mar 15 10:30 upgrade_patch.diff
# 终端上还原(在电表MCU上执行)
bspatch old_firmware.bin new_firmware.bin upgrade_patch.diff
嗯,这里要注意:bsdiff 对内存有一定要求。我在一个只有 64KB RAM 的 Cortex-M0 芯片上跑过,差点翻车。后来改用 HDiffPatch 才搞定,它内存占用更低,适合资源受限的嵌入式设备。
避坑指南:
我曾经遇到过一个问题——差分补丁包在终端上还原后,校验一直失败。排查了两天才发现,是旧固件在传输过程中被篡改了一个字节。所以,差分升级的前提是:终端上的旧固件必须是完整且正确的。否则还原出来的新固件就是垃圾。
4.2 哈希校验:确保数据完整性
差分补丁包生成后,怎么保证它在传输过程中没被改过?哈希校验就是干这个的。
说白了,哈希就是给文件算一个「指纹」。哪怕文件只改了一个比特,指纹都会完全不一样。
我推荐用 SHA-256,虽然比 MD5 慢一点,但安全性高得多。电力行业对安全要求严,MD5 已经被证明可碰撞,别用了。
# 计算补丁包的 SHA-256 哈希值
sha256sum upgrade_patch.diff
# 输出示例:a3f5b8c1d2e4f6... upgrade_patch.diff
# 将哈希值写入校验文件
echo "a3f5b8c1d2e4f6... upgrade_patch.diff" > checksum.sha256
# 终端上验证
sha256sum -c checksum.sha256
# 输出示例:upgrade_patch.diff: OK
实际项目中,我会把哈希值放在升级包的头部。终端收到包后,先算哈希,跟头部里的比对。不一致就直接丢弃,不执行后续步骤。
重要提醒:
哈希校验只能防「意外损坏」,不能防「恶意篡改」。因为攻击者可以同时修改补丁包和哈希值。要防篡改,必须上数字签名。
4.3 数字签名流程:让升级包「验明正身」
数字签名,你可以理解为给升级包盖一个「防伪章」。这个章只有厂家能盖,但任何人都能验证。
我常用的签名算法是 ECDSA(椭圆曲线数字签名算法)。相比 RSA,它的密钥更短,签名更小,适合嵌入式设备。
整个流程分三步:
- 签名(服务器端):用私钥对升级包的哈希值加密,生成签名
- 打包:将升级包 + 签名 + 公钥证书打包成一个文件
- 验签(终端):用公钥解密签名,比对哈希值
# 步骤1:生成密钥对(服务器端)
openssl ecparam -genkey -name prime256v1 -out private_key.pem
openssl ec -in private_key.pem -pubout -out public_key.pem
# 步骤2:对升级包签名
openssl dgst -sha256 -sign private_key.pem -out signature.bin upgrade_patch.diff
# 步骤3:打包(将补丁包、签名、公钥合并)
cat upgrade_patch.diff signature.bin public_key.pem > upgrade_package.bin
# 步骤4:终端验签(伪代码)
# 1. 从 upgrade_package.bin 中提取 signature.bin 和 public_key.pem
# 2. 计算 upgrade_patch.diff 的 SHA-256 哈希
# 3. 用公钥解密 signature.bin,得到原始哈希
# 4. 比对两个哈希值,一致则通过
签名流程的关键点:
- 私钥必须绝对保密:我见过有厂家把私钥硬编码在代码里,结果被反编译出来,整个产品线都得召回
- 公钥可以公开:但要在终端出厂时预置,且存储在安全区域(如 eFuse 或 TEE)
- 签名算法选型:对于资源受限的 MCU,ECDSA 比 RSA 快 3-5 倍,签名长度也短
我记得有一次,客户要求升级包必须支持「离线验证」——终端在没有网络的情况下也能验签。解决方案是把公钥证书链也打包进去,终端用预置的根证书来验证整个链。嗯,这个设计后来成了我们产品的标准功能。
4.4 完整的升级包结构
把上面所有东西串起来,一个完整的升级包长这样:
| 字段 | 大小 | 说明 |
|---|---|---|
| 魔数 | 4 字节 | 固定标识,如 0x55AA55AA,用于快速识别 |
| 版本号 | 4 字节 | 主版本 + 次版本 + 修订号 |
| 目标设备类型 | 2 字节 | 防止刷错设备(比如把单相表固件刷到三相表上) |
| 差分补丁数据 | N 字节 | bsdiff 生成的 .diff 文件 |
| SHA-256 哈希 | 32 字节 | 补丁数据的哈希值 |
| 数字签名 | 64 字节 | ECDSA 签名(P-256 曲线) |
| 公钥证书 | 可变 | X.509 格式,用于验签 |
我的经验:
升级包头部一定要留一个「扩展字段区」。我吃过亏——最初设计时没留扩展位,后来想加个「升级前校验旧固件版本」的功能,结果得改整个包格式,兼容性搞得一团糟。现在我的设计原则是:头部至少预留 32 字节的扩展空间。
4.5 总结一下
升级包制作与签名,说白了就是三件事:
- 差分:用 bsdiff 只传变化部分,省流量、省时间
- 哈希:用 SHA-256 保证数据没被改过
- 签名:用 ECDSA 保证数据来源可信
这三步缺一不可。我在项目里见过只做哈希不做签名的,结果被中间人攻击,终端全刷成了恶意固件。也见过只做签名不做差分的,升级包太大,偏远地区的终端根本下不完。
嗯,下一章咱们聊聊升级过程中的「断点续传」和「失败回滚」——这两个才是真正考验方案成熟度的地方。