3. 升级包制作与签名:差分算法、加密与签名校验

升级包怎么做?说白了就两件事:怎么把新版本变小,以及怎么保证没人篡改它。

我见过不少团队,差分算法跑通了就急着上线,结果升级包被中间人替换,设备变砖。嗯,这一章咱们就把这两件事彻底讲透。

3.1 差分算法:为什么你的包那么大?

全量升级包就是把整个固件重新发一遍。如果固件 100MB,每次升级都发 100MB,流量和耗时都扛不住。

差分算法只计算新旧版本之间的差异。比如你只改了一行代码,差分包可能只有几百 KB。

3.1.1 bsdiff:老牌选手

bsdiff 是 Colin Percival 在 2003 年提出的算法。它的核心思路是:

  • 用后缀排序找出新旧文件的最长公共子串
  • 只记录新增、删除、修改的部分
  • 输出一个二进制补丁文件

我在项目中用过很多次 bsdiff。它的压缩率确实好,但有个痛点——内存占用高。处理 100MB 的固件,内存峰值能到 400MB 以上。低端 MCU 根本跑不动。

适用场景:Linux 网关、Android 设备、有足够 RAM 的嵌入式 Linux 系统。

3.1.2 hdiffpatch:轻量级替代

hdiffpatch 是 bsdiff 的改良版。它用哈希匹配代替后缀排序,内存占用大幅降低。

对比项 bsdiff hdiffpatch
压缩率 更优(通常小 10-20%) 略差
内存占用 高(约 4x 原文件大小) 低(约 1.5x 原文件大小)
打补丁速度 较慢 快 2-3 倍
适用硬件 服务器/PC 端生成 嵌入式端生成或解包

我个人习惯:服务器端用 bsdiff 生成差分包,设备端用 hdiffpatch 解包。这样既省流量,又不会把设备内存撑爆。

3.1.3 差分流程实战

下面是一个典型的差分包制作流程:

# 服务器端:生成差分包
bsdiff old_firmware.bin new_firmware.bin delta.patch

# 设备端:应用差分包
bspatch old_firmware.bin new_firmware.bin delta.patch

注意:旧固件必须完整保留在设备上。如果旧固件被损坏或擦除,差分包就无法应用。

我曾经踩过的坑:某次升级时,设备在差分过程中断电重启,旧固件被部分覆盖。结果新固件没合成成功,旧固件也废了。从那以后,我强制要求所有设备在差分前先备份旧固件到另一个分区。

3.2 升级包加密:别让固件裸奔

差分包虽然小了,但内容还是明文。如果有人抓包拿到你的固件,逆向工程、复制产品、植入后门……想想都头疼。

加密是必须的。常用的方式有两种:

3.2.1 对称加密(AES)

加密和解密用同一个密钥。速度快,适合资源受限的设备。

# 加密差分包
openssl enc -aes-256-cbc -salt -in delta.patch -out delta.enc -K $KEY -iv $IV

# 设备端解密
openssl enc -d -aes-256-cbc -in delta.enc -out delta.patch -K $KEY -iv $IV

密钥怎么存?这是个难题。我见过有人把密钥硬编码在固件里——那跟没加密有什么区别?

我的做法是:密钥由服务器下发,每次升级动态协商。设备用预置的公钥加密一个随机数发给服务器,服务器用私钥解密后,用这个随机数作为 AES 密钥。

3.2.2 非对称加密(RSA/ECC)

公钥加密,私钥解密。或者反过来,私钥签名,公钥验签。

非对称加密慢,不适合加密大文件。所以实际项目中,常用混合加密:

  1. 用 RSA 加密一个随机生成的 AES 密钥
  2. 用这个 AES 密钥加密差分包
  3. 设备先用 RSA 私钥解密出 AES 密钥
  4. 再用 AES 密钥解密差分包
性能提示:RSA-2048 在 Cortex-M4 上解密一次大约需要 200ms。如果设备对启动时间敏感,建议用 ECC(椭圆曲线),同样安全强度下速度快 5-10 倍。

3.3 签名校验:确保包没被换过

加密只能防偷看,不能防篡改。攻击者可以替换你的加密包,设备解密后照样执行——结果就是变砖。

签名校验才是最后一道防线。

3.3.1 签名流程

  1. 服务器计算差分包的哈希值(SHA-256)
  2. 用私钥对哈希值签名
  3. 将签名附加到加密包后面
  4. 设备用公钥验签,确认哈希值匹配
  5. 哈希值匹配后才解密、应用
# 服务器端签名
openssl dgst -sha256 -sign private.pem -out delta.sig delta.enc

# 设备端验签
openssl dgst -sha256 -verify public.pem -signature delta.sig delta.enc

3.3.2 公钥怎么保护?

公钥是公开的,但你不能让攻击者替换它。如果设备里的公钥被换了,攻击者就能用自己的私钥签名恶意包。

我建议:

  • 公钥烧死在芯片 OTP 区域(一次性可编程),出厂后不可更改
  • 或者使用安全元件(SE),公钥存储在硬件隔离区
  • 再或者签名链:用芯片厂商的根证书签你的公钥,设备只信任根证书
我曾经犯过的错:早期一个项目把公钥放在文件系统里,想着没人会去改。结果产品被破解,攻击者替换了公钥,推送了恶意升级包。那次教训让我明白——安全不能靠“没人会这么做”来保证。

3.4 完整升级包结构

把上面所有环节串起来,一个完整的升级包长这样:

升级包结构:
┌─────────────────────────────┐
│  包头(版本号、固件大小、CRC) │
├─────────────────────────────┤
│  加密的差分包数据             │
├─────────────────────────────┤
│  签名数据(SHA-256 + RSA)    │
└─────────────────────────────┘

设备端校验顺序:

  1. 解析包头,检查 CRC
  2. 用公钥验签,确认包未被篡改
  3. 解密差分包
  4. 应用差分,合成新固件
  5. 校验新固件的完整性
  6. 写入备用分区,标记启动
关键原则:先验签,再解密,最后应用。顺序不能乱。

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了:

升级包制作与签名流程 升级包制作与签名流程 旧固件 (v1.0) 新固件 (v2.0) bsdiff / hdiffpatch 差分包 (delta.patch) AES-256 加密 RSA-2048 签名 加密签名升级包 (delta.enc + .sig) 设备端:验签 → 解密 → 差分 → 写入 顺序不可颠倒

嗯,到这里,升级包的制作和签名就讲完了。差分算法选型、加密方式选择、签名链设计——每一步都关系到升级的安全性和可靠性。别图省事跳过任何一步,否则迟早要还债。