3. 升级包制作与签名:从代码到可信分发的完整链路

升级包,说白了就是设备用来“自我更新”的那份数据。但这份数据不是简单地把新固件打个包就完事了。你想想看,如果升级包在传输过程中被篡改,或者设备下载了一个不完整的包,那后果可能就是变砖。所以,这一章我们重点聊聊升级包是怎么做出来的,怎么保证它安全、完整、还能高效传输。

3.1 差分算法:只传变化的部分

全量升级包很简单,就是把整个新固件发下去。但嵌入式设备往往带宽有限、存储也紧张。差分升级(也叫增量升级)就是为了解决这个问题——只传输新旧版本之间的差异部分。

我个人习惯把差分算法分成两类:二进制差分和文件级差分。前者直接操作二进制数据流,后者基于文件系统做对比。

3.1.1 bsdiff:二进制差分的经典方案

bsdiff 是目前最常用的二进制差分算法之一。它的核心思路是:找到旧文件中与新文件相同的字节块,只记录新增或修改的部分。bsdiff 生成的 patch 通常很小,但计算时比较吃内存。

我在项目中遇到过一个问题:某款 IoT 设备只有 256KB RAM,跑 bsdiff 生成差分包时直接 OOM 了。后来我们改用内存优化版的 bsdiff,才把问题解决。

bsdiff 的典型用法:

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

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

bsdiff 的优点是压缩率高,缺点是内存开销大。对于资源受限的设备,你可能需要评估一下是否适用。

3.1.2 hdiffpatch:轻量级的选择

hdiffpatch 是另一种差分算法,它比 bsdiff 更轻量。我记得在某个智能家居项目中,设备只有 64KB 可用内存,bsdiff 根本跑不起来,换成 hdiffpatch 后顺利搞定。

hdiffpatch 的算法设计更注重内存效率,适合资源受限的嵌入式环境。不过,它的压缩率通常略低于 bsdiff。

特性 bsdiff hdiffpatch
压缩率 高(通常 5%-15%) 中等(通常 10%-25%)
内存消耗 高(约旧文件大小 2-3 倍) 低(约旧文件大小 0.5-1 倍)
适用场景 服务器端生成、资源充足的设备 资源受限的嵌入式设备
算法复杂度 O(n log n) O(n)

我的建议:如果设备内存充足,优先用 bsdiff;如果内存紧张,选 hdiffpatch。另外,差分算法对固件结构敏感——如果新旧版本之间改动太大,差分效果会变差,这时候全量升级反而更省事。

3.2 升级包压缩:让传输更快

差分之后的数据通常还需要压缩。压缩的目的很简单:减少传输数据量,节省带宽和时间。

常用的压缩算法有 gzip、zlib、lzma、lz4 等。选择哪个,主要看你的设备解压能力和网络带宽的平衡。

  • gzip/zlib:通用性好,解压速度快,压缩率中等。大多数嵌入式设备都支持。
  • lzma:压缩率最高,但解压需要更多内存和 CPU。适合服务器端生成、设备端解压的场景。
  • lz4:解压速度极快,但压缩率较低。适合对实时性要求高的场景。

我曾经在一个项目中踩过坑:用了 lzma 压缩,结果设备解压时内存不够,直接崩溃。后来换成 zlib,虽然包大了 10%,但解压稳定多了。

注意:压缩和解压是 CPU 密集型操作。如果设备主频很低(比如几十 MHz),建议在服务器端完成压缩,设备端只做解压。另外,解压缓冲区要预留足够空间,避免内存溢出。

3.3 数字签名与验签流程:确保升级包可信

升级包做好了,也压缩了,但怎么保证它没有被篡改?答案就是数字签名。

数字签名的核心流程是:签名方用私钥对升级包生成签名,验签方用公钥验证签名是否匹配。如果签名不匹配,说明升级包被篡改过,设备应该拒绝升级。

3.3.1 签名流程

  1. 计算哈希:对升级包(压缩后的差分数据)计算哈希值,比如 SHA-256。
  2. 签名:用私钥对哈希值进行加密,生成签名数据。
  3. 打包:将升级包、签名数据、公钥证书(或证书链)打包成一个完整的升级包文件。

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

# 生成私钥
openssl genrsa -out private.pem 2048

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

# 计算哈希并签名
openssl dgst -sha256 -sign private.pem -out upgrade.sig upgrade.bin

# 打包
tar -czf upgrade_package.tar.gz upgrade.bin upgrade.sig public.pem

3.3.2 验签流程

  1. 解包:设备收到升级包后,解压得到升级数据、签名和公钥。
  2. 验证公钥:检查公钥是否可信(通常预置在设备中,或通过证书链验证)。
  3. 验签:用公钥解密签名,得到哈希值;再对升级数据计算哈希,两者对比。
  4. 通过/拒绝:如果哈希一致,升级包可信;否则拒绝升级。

我曾经踩过的坑:公钥硬编码在固件里,结果固件被破解后公钥也被改了。后来我们把公钥放在安全存储区(如 eFuse 或 TPM),才真正解决了信任根的问题。

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

版本管理听起来简单,但做不好会出大问题。我见过一个案例:某设备因为版本号比较逻辑写反了,导致设备从 v2.0 降级到 v1.0,结果功能异常。

版本管理需要关注几个点:

  • 版本号格式:建议使用语义化版本号(如 v1.2.3),主版本号、次版本号、修订号分别管理。
  • 版本比较逻辑:设备端要能正确判断哪个版本更新。不要用字符串比较,要用数值比较。
  • 降级保护:通常不允许降级,除非有特殊需求(比如回滚)。
  • 版本兼容性:升级包要标明兼容的旧版本范围,避免不兼容的升级。

我的习惯:在升级包头部加一个版本信息结构体,包含当前版本、兼容的最低版本、升级包类型(全量/差分)等字段。设备在升级前先解析头部,做版本校验,再决定是否继续。

3.5 整体流程:从制作到分发

把上面几个环节串起来,就是升级包从制作到分发的完整链路。我画了一张图,帮你理清关系。

升级包制作与分发流程 旧固件 (v1.0) 新固件 (v2.0) 差分算法 差分包 压缩 (zlib/lzma) 数字签名 升级包 分发到设备 验签 应用升级 版本管理贯穿始终 升级前校验版本号 防止降级或错误升级

从图中可以看到,升级包的制作不是一步到位的。先做差分,再压缩,然后签名,最后分发到设备。设备端收到后,先验签,再解压,最后应用差分恢复新固件。每一步都有它的意义,缺一不可。

总结一下本章的核心要点:

  • 差分算法(bsdiff/hdiffpatch)决定了升级包的大小,选型要看设备资源。
  • 压缩算法(gzip/lzma/lz4)影响传输效率和解压开销,需要权衡。
  • 数字签名是安全底线,私钥要保护好,公钥要安全存储。
  • 版本管理是“防呆”机制,避免设备升错级或降级。