3、升级包制作与签名:差分升级算法、全量包与差分包、签名与校验机制
好,咱们进入第三个核心环节——升级包的制作与签名。
说实话,很多团队在OTA上栽跟头,不是死在传输协议上,而是死在升级包本身。要么包太大下载超时,要么包被篡改导致设备变砖。我见过最惨的一次,是某款车机因为签名校验没做严格,被黑客注入了恶意固件,整个批次都得返厂重刷。
所以这一章,咱们把升级包的“里子”和“面子”都讲透。
3.1 全量包 vs 差分包:什么时候该用谁?
先搞清楚两个基本概念。
全量包,就是把整个系统镜像打包。比如你的系统固件是128MB,全量包就是128MB。好处是简单粗暴,刷完就是全新系统,不存在依赖关系。坏处也明显——大。在2G/3G网络下,128MB下载可能要十几分钟,用户早没耐心了。
差分包,只包含新旧版本之间的差异部分。比如从v1.0升级到v1.1,可能只改了5MB的代码,差分包就只打包这5MB。下载快、流量省、用户体验好。
我个人习惯这样划分场景:
- 大版本升级(比如从Android 9升到Android 11):用全量包。因为改动太大,差分算法生成的补丁可能比全量包还大,得不偿失。
- 小版本迭代(比如从v2.0.1升到v2.0.2):用差分包。改动小,差分效率高,用户几乎无感。
- 跨版本升级(比如从v1.0直接升到v3.0):建议用全量包。差分链太长,中间版本依赖复杂,容易出问题。
3.2 差分升级算法:bsdiff、hdiff 与 xdelta
差分算法的核心思想,就是找出新旧两个二进制文件之间的“差异块”,然后只传输这些差异。说白了,就是“找不同”。
目前主流的差分算法有三种:
| 算法 | 特点 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| bsdiff | 基于后缀排序,压缩率高,但内存消耗大 | 固件体积较大(>50MB)的场景 | 在低端MCU上跑bsdiff解压,内存直接爆了 |
| hdiff | 基于哈希匹配,速度快,内存占用低 | 嵌入式实时系统,资源受限的设备 | 压缩率比bsdiff低10%左右,但胜在稳定 |
| xdelta | 基于VCDIFF标准,平衡性好 | 通用场景,尤其是文件系统级别的差分 | 对非连续修改的二进制文件效果一般 |
为什么会这样?bsdiff虽然压缩率最高,但它需要在内存中构建整个后缀数组。你想想看,一个128MB的固件,bsdiff可能需要占用超过1GB的内存来做差分计算。这在服务器端没问题,但在设备端做解压时,同样需要较大的临时缓冲区。我曾经在一个只有256KB RAM的MCU上尝试用bsdiff解压,结果直接OOM(内存溢出),设备重启了。
所以我的建议是:
- 服务器端生成差分包:用bsdiff,追求极致压缩率。
- 设备端解压:用hdiff或xdelta,确保内存安全。
下面是一个典型的差分包生成命令示例:
# 生成差分包(服务器端)
bsdiff old_firmware.bin new_firmware.bin update.patch
# 应用差分包(设备端)
bspatch old_firmware.bin new_firmware.bin update.patch
3.3 签名与校验机制:别让你的升级包被“调包”
升级包做好了,怎么保证它在传输过程中不被篡改?签名。
签名说白了就是给升级包盖个章。设备端只认这个章,没章或者章不对,直接拒绝升级。
我常用的签名流程是这样的:
- 生成密钥对:私钥放在服务器端,绝对保密;公钥烧录在设备中,不可更改。
- 计算哈希:对升级包(全量包或差分包)计算SHA-256哈希值。
- 签名:用私钥对哈希值进行RSA或ECDSA签名,生成签名文件。
- 打包:将升级包 + 签名文件 + 公钥证书打包成一个分发文件。
设备端收到后,做反向操作:
- 用内置的公钥解密签名,得到原始哈希值。
- 对收到的升级包重新计算SHA-256。
- 比对两个哈希值,一致则通过,否则拒绝。
代码示例(设备端校验逻辑):
// 伪代码:设备端签名校验
bool verify_update_package(const uint8_t* package, size_t size,
const uint8_t* signature, size_t sig_len) {
// 1. 计算升级包的哈希
uint8_t hash[32];
sha256(package, size, hash);
// 2. 用公钥解密签名,得到原始哈希
uint8_t decrypted_hash[32];
rsa_decrypt(signature, sig_len, public_key, decrypted_hash);
// 3. 比对
if (memcmp(hash, decrypted_hash, 32) == 0) {
return true; // 校验通过
} else {
return false; // 校验失败,拒绝升级
}
}
3.4 多屏系统的特殊考量
在多屏系统中,升级包制作还有一个头疼的问题——依赖关系。
比如,仪表屏的固件v2.0依赖中控屏的固件v3.0的某个API。如果你只升级仪表屏,不升级中控屏,那仪表屏可能就罢工了。
我的做法是:
- 在升级包的元数据中,明确声明依赖版本号。
- 设备端在升级前,先检查所有相关屏的版本,不满足依赖条件则拒绝升级。
- 如果依赖不满足,提示用户“请先升级中控屏至v3.0”。
元数据示例(JSON格式):
{
"package_name": "仪表屏_v2.0_to_v2.1_差分包",
"target_device": "instrument_cluster",
"old_version": "2.0",
"new_version": "2.1",
"dependencies": [
{"device": "center_console", "min_version": "3.0"}
],
"checksum": "sha256:abcdef...",
"signature": "rsa:xxxx..."
}
3.5 小结
升级包制作,说白了就是三件事:
- 选对包类型:大版本用全量,小版本用差分。
- 选对差分算法:服务器端用bsdiff压到最小,设备端用hdiff保平安。
- 签好名、验好章:私钥锁死,公钥焊死,哈希比对不能省。
下一章,咱们聊聊升级包的存储与分发策略。嗯,那又是另一个坑了。