3、升级包制作与签名:差分升级算法、全量包制作、数字签名与验签机制、升级包版本管理
升级包制作,是整个OTA流程里最核心的环节之一。说白了,你算法选得对不对,签名做没做全,版本号管没管好,直接决定了设备能不能安全、稳定地完成升级。我见过不少团队,前面协议栈调得飞起,结果在升级包这步栽了跟头——要么包太大烧了流量,要么签名漏了被攻击,要么版本号乱套导致设备反复升级。嗯,咱们今天就把这块彻底讲透。
3.1 差分升级算法:bsdiff 与 hdiffpatch
差分升级,就是只传输新旧版本之间的差异部分。你想想看,一个固件可能 20MB,但每次改动可能就几百 KB。如果每次都全量传,用户流量扛不住,服务器带宽也扛不住。
目前业界用得最多的两个差分算法,一个是 bsdiff,一个是 hdiffpatch。我个人的习惯是:资源受限的 MCU 用 hdiffpatch,性能强的 Linux 设备用 bsdiff。为什么?往下看。
| 对比项 | bsdiff | hdiffpatch |
|---|---|---|
| 算法原理 | 基于后缀排序 + 二分查找 | 基于哈希匹配 + 滑动窗口 |
| 差分率(压缩比) | 通常更小(5%~15%) | 稍大(10%~25%) |
| 内存占用(生成差分包) | 极高(O(n) 级别,1MB 固件约需 200MB 内存) | 较低(O(1) 级别,1MB 固件约需 2MB 内存) |
| 打补丁速度(设备端) | 较慢(需重建后缀数组) | 较快(流式处理) |
| 适用场景 | 服务器端生成,Linux 设备端打补丁 | MCU/RTOS 设备端打补丁 |
3.1.1 差分包生成流程(以 hdiffpatch 为例)
- 准备新旧固件: 确保两个固件都是编译好的二进制文件,且版本号明确。
- 运行差分工具: 在 PC 或服务器上执行
hdiffpatch -s old.bin -n new.bin -o diff.patch。 - 验证差分包: 用
hdiffpatch -v old.bin diff.patch new.bin验证打补丁后的结果是否与 new.bin 一致。 - 压缩差分包: 对 diff.patch 做 gzip 或 lzma 压缩,进一步减小传输体积。
# 生成差分包(服务器端)
hdiffpatch -s firmware_v1.0.bin -n firmware_v2.0.bin -o diff_v1_to_v2.patch
# 验证差分包(服务器端)
hdiffpatch -v firmware_v1.0.bin diff_v1_to_v2.patch firmware_v2.0.bin
# 压缩差分包
gzip -k diff_v1_to_v2.patch
--ffunction-sections 和 --gc-sections 来保持函数布局稳定。
3.2 全量升级包制作
全量升级包,就是把整个固件镜像打包成一个文件。虽然它比差分包大,但胜在简单、可靠。我建议:差分升级作为默认策略,全量升级作为降级兜底。当差分算法失败、或者设备端补丁校验不通过时,自动回退到全量升级。
全量包的制作其实不复杂,但有几个细节要注意:
- 镜像格式: 常见的有 raw binary、Intel HEX、SREC。MCU 设备通常用 HEX 或 SREC,因为它们包含地址信息。
- 元数据头: 在固件前面加一个自定义头部,包含魔数、版本号、固件大小、校验和、签名等信息。
- 填充对齐: 很多 MCU 要求固件按 4KB 或 64KB 对齐,否则烧录会失败。
// 全量包头部结构体示例
typedef struct {
uint32_t magic; // 魔数,例如 0xOTA1
uint32_t version; // 固件版本号
uint32_t firmware_size; // 固件大小(不含头部)
uint32_t checksum; // CRC32 校验值
uint8_t signature[256]; // RSA 签名(2048位)
uint8_t reserved[128]; // 保留字段
} firmware_header_t;
3.3 数字签名与验签机制
数字签名,是OTA安全的最后一道防线。你想想看,如果升级包被篡改了,设备刷了一个恶意固件,后果不堪设想。我见过一个案例:某厂商没做签名,黑客伪造了一个升级包,把设备变成了挖矿肉鸡。嗯,从那以后,我再也不敢省略签名步骤。
3.3.1 签名流程
- 生成密钥对: 在服务器端用 OpenSSL 生成 RSA 2048 位或 ECC P-256 密钥对。
- 计算哈希: 对升级包(差分或全量)计算 SHA-256 哈希值。
- 签名: 用私钥对哈希值进行加密,生成签名数据。
- 打包: 将签名数据附加到升级包中,或者放入固件头部。
# 生成 RSA 私钥(2048位)
openssl genrsa -out private.pem 2048
# 提取公钥
openssl rsa -in private.pem -pubout -out public.pem
# 对升级包签名
openssl dgst -sha256 -sign private.pem -out firmware_v2.0.sig firmware_v2.0.bin
# 验证签名(服务器端测试)
openssl dgst -sha256 -verify public.pem -signature firmware_v2.0.sig firmware_v2.0.bin
3.3.2 验签流程(设备端)
- 预置公钥: 在设备出厂时,将公钥烧录到 bootloader 的只读区域。
- 接收升级包: 设备下载升级包后,先不急着刷写。
- 验签: bootloader 读取升级包中的签名数据,用公钥解密,与本地计算的 SHA-256 哈希比对。
- 通过则刷写,不通过则丢弃: 验签失败时,记录错误日志,并上报服务器。
3.4 升级包版本管理
版本管理看似简单,但做不好会引发大问题。我见过最离谱的情况是:设备端版本号是 1.0.0,服务器端版本号是 1.0.0,但两个固件内容完全不同——因为版本号没更新。设备认为“版本相同,跳过升级”,结果一直跑着旧固件。
我建议采用语义化版本号,格式为 主版本.次版本.修订号:
- 主版本: 不兼容的 API 或协议变更。
- 次版本: 向下兼容的功能新增。
- 修订号: 向下兼容的问题修复。
除了版本号本身,还要管理版本依赖关系。比如:
| 当前版本 | 可升级到的版本 | 升级方式 |
|---|---|---|
| v1.0.0 | v1.1.0, v1.2.0 | 差分升级 |
| v1.1.0 | v1.2.0 | 差分升级 |
| v1.0.0 | v2.0.0 | 全量升级(跨主版本) |
| v1.2.0 | v2.0.0 | 全量升级(跨主版本) |
3.5 本章知识结构图
下面这张图,把升级包制作与签名的核心流程串起来了。你可以把它当作一个检查清单,做项目时对着看,不容易漏环节。
嗯,到这里,升级包制作与签名的核心内容就讲完了。你可能会觉得内容有点多,但每个环节都是环环相扣的。差分算法省流量,全量包兜底,签名保安全,版本管理防混乱——少了哪一个,整个 OTA 系统都可能出问题。我建议你先把差分算法和签名机制跑通,再逐步完善版本管理策略。毕竟,能跑起来比什么都重要。