3. 升级包制作与签名:差分算法、压缩与安全机制

升级包怎么做?说白了就三步:算出差分、打成压缩包、签上名。每一步都有坑,我一个个讲。

3.1 差分算法:bsdiff vs hdiffpatch

差分算法的核心目标只有一个——让升级包尽可能小。你想想看,一个 100MB 的固件,如果每次都要全量下载,用户流量扛不住,服务器带宽也扛不住。

我个人习惯在两种算法里选:bsdiff 和 hdiffpatch。

对比项 bsdiff hdiffpatch
算法原理 基于后缀排序 + 二分匹配 基于哈希 + 滑动窗口
差分包大小 通常更小(5%~15%) 稍大(10%~20%)
内存占用 高(约原固件 5 倍) 低(约原固件 1.5 倍)
生成速度 慢(大文件可能数分钟) 快(秒级)
适用场景 服务器端生成,资源充足 嵌入式端生成或资源受限环境
我的建议:服务器端生成差分包,优先用 bsdiff。嵌入式端做差分(比如 A/B 分区回滚场景),用 hdiffpatch 更稳。

我曾经在一个智能家居项目里踩过坑。当时用 bsdiff 给一个 80MB 的摄像头固件做差分,生成时内存飙到 400MB,直接把构建服务器搞 OOM 了。后来加了 --memlimit 256MB 参数才解决。

bsdiff 的基本用法:

# 生成差分包
bsdiff old_firmware.bin new_firmware.bin upgrade.patch

# 应用差分包
bspatch old_firmware.bin new_firmware.bin upgrade.patch

hdiffpatch 的用法类似:

# 生成差分包
hdiffz -s old_firmware.bin -d new_firmware.bin -o upgrade.patch

# 应用差分包
hpatchz -s old_firmware.bin -d new_firmware.bin -o upgrade.patch
注意:差分算法对固件结构敏感。如果新旧固件之间改动过大(比如换了文件系统),差分包可能比全量包还大。我建议设定一个阈值——差分包超过全量包 80% 时,自动回退到全量升级。

3.2 升级包压缩:选对格式,省一半流量

差分包生成后,还要再压缩一层。为什么?因为差分算法只处理二进制差异,不负责通用压缩。再压一层,包体还能再小 20%~40%。

常用的压缩格式就三种:

  • gzip:兼容性最好,几乎所有嵌入式系统都支持。压缩率中等。
  • lzma / xz:压缩率最高,但解压需要更多内存和 CPU。适合资源充足的设备。
  • zstd:我最近两年最常用的。压缩率接近 lzma,但解压速度是 gzip 的 3~5 倍。

来看一组实测数据(基于一个 32MB 的差分包):

压缩格式 压缩后大小 压缩耗时 解压耗时(嵌入式 ARM Cortex-A7)
gzip -6 18.2 MB 2.1s 1.8s
lzma -6 14.5 MB 12.4s 4.2s
zstd -6 15.1 MB 1.5s 0.9s
我的选择:嵌入式端解压资源有限,我一般用 zstd 级别 3~6。既省流量,又不拖慢升级速度。

压缩命令示例:

# zstd 压缩
zstd -6 upgrade.patch -o upgrade.patch.zst

# zstd 解压
zstd -d upgrade.patch.zst -o upgrade.patch

3.3 数字签名与验签机制:防篡改的最后一道防线

升级包在传输过程中可能被篡改。你想想看,如果攻击者替换了你的升级包,设备刷了恶意固件,后果是什么?

所以,签名是必须的。我常用的方案是 ECDSA(椭圆曲线数字签名算法),相比 RSA,它的签名更短、验签更快。

签名流程:

  1. 对升级包(压缩后的差分包)计算哈希值,比如 SHA-256。
  2. 用私钥对哈希值签名,生成签名文件。
  3. 将签名文件与升级包一起打包分发。

验签流程:

  1. 设备收到升级包后,先计算本地哈希值。
  2. 用公钥解密签名,得到原始哈希值。
  3. 对比两个哈希值,一致则通过,否则拒绝升级。

我曾经在一个 IoT 项目里发现,有些设备因为 RTC 电池没电,系统时间回到了 1970 年,导致证书验证失败。后来我改成了 固定公钥硬编码 + 签名验签,不再依赖证书链,问题就解决了。

签名命令示例(使用 OpenSSL + ECDSA):

# 生成私钥
openssl ecparam -genkey -name prime256v1 -out private.pem

# 提取公钥
openssl ec -in private.pem -pubout -out public.pem

# 对升级包签名
openssl dgst -sha256 -sign private.pem -out upgrade.patch.sig upgrade.patch.zst

# 验签
openssl dgst -sha256 -verify public.pem -signature upgrade.patch.sig upgrade.patch.zst
关键点:公钥必须安全存储在设备端。如果公钥被替换,签名就形同虚设。我建议把公钥放在 BootROM 或安全元件(SE)里,至少也要放在只读分区。

3.4 升级包版本管理:别让设备升错级

版本管理看起来简单,但做不好会出大问题。我记得有一次,一个设备从 v2.0 直接升到 v3.0,跳过了 v2.5 的中间版本,结果因为数据库 schema 不兼容,设备直接变砖。

所以,升级包必须包含完整的版本信息。我一般用 JSON 格式的元数据文件:

{
  "package_version": "3.0.0",
  "target_version": "3.0.0",
  "min_supported_version": "2.5.0",
  "max_supported_version": "2.9.9",
  "checksum": "sha256:abcdef...",
  "signature": "base64:xxxx...",
  "release_date": "2025-03-15",
  "hardware_compatibility": ["HW_REV_A", "HW_REV_B"]
}

版本管理的几个原则:

  • 语义化版本:主版本.次版本.修订号。主版本不兼容时,必须全量升级。
  • 版本范围限制:每个差分包只适用于特定版本区间。超出范围就拒绝。
  • 硬件兼容性检查:同一款产品可能有不同硬件版本,升级包必须标明兼容的硬件。
  • 回滚保护:升级成功后,保存旧版本号。如果新版本有问题,可以回滚。
避坑指南:我曾经遇到过设备从 v1.0 升到 v1.1,差分包只有 2KB,但升级后 Wi-Fi 模块不工作了。查了半天,发现 v1.0 的 Wi-Fi 固件有 bug,v1.1 修复了,但差分算法没检测到 Wi-Fi 固件的变化。从那以后,我要求每次发版前做一次全量比对,确保差分包覆盖了所有变更文件。

3.5 本章知识体系

下面这张图概括了升级包制作与签名的完整流程:

升级包制作与签名流程 升级包制作与签名流程 新旧固件 差分算法 差分包 压缩(zstd) SHA-256 哈希 ECDSA 签名 版本元数据 最终升级包(差分+签名+元数据) 关键检查点 • 版本范围校验 • 哈希一致性校验 • 签名有效性校验

整个流程环环相扣。差分算法决定了包大小,压缩进一步优化传输效率,签名保证安全性,版本管理防止升级错乱。任何一个环节出问题,都可能导致升级失败甚至设备变砖。

总结一句话:差分做小、压缩做快、签名做牢、版本做对。这四件事做好了,OTA 升级就成功了一半。