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 设备端打补丁
我的经验: 在 STM32 项目上,我曾经用 bsdiff 做差分,结果设备端 RAM 只有 64KB,根本跑不动补丁算法。后来换成 hdiffpatch,把补丁包大小控制在 8KB 以内,设备端用 16KB 的缓冲区就能完成打补丁。选算法之前,一定先看设备端的内存预算。

3.1.1 差分包生成流程(以 hdiffpatch 为例)

  1. 准备新旧固件: 确保两个固件都是编译好的二进制文件,且版本号明确。
  2. 运行差分工具: 在 PC 或服务器上执行 hdiffpatch -s old.bin -n new.bin -o diff.patch。
  3. 验证差分包: 用 hdiffpatch -v old.bin diff.patch new.bin 验证打补丁后的结果是否与 new.bin 一致。
  4. 压缩差分包: 对 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;
关键点: 全量包头部一定要放在固件的最前面,设备端 bootloader 读取时,先解析头部,校验签名和 CRC,再决定是否烧录。头部设计得越严谨,后期出问题的概率越低。

3.3 数字签名与验签机制

数字签名,是OTA安全的最后一道防线。你想想看,如果升级包被篡改了,设备刷了一个恶意固件,后果不堪设想。我见过一个案例:某厂商没做签名,黑客伪造了一个升级包,把设备变成了挖矿肉鸡。嗯,从那以后,我再也不敢省略签名步骤。

3.3.1 签名流程

  1. 生成密钥对: 在服务器端用 OpenSSL 生成 RSA 2048 位或 ECC P-256 密钥对。
  2. 计算哈希: 对升级包(差分或全量)计算 SHA-256 哈希值。
  3. 签名: 用私钥对哈希值进行加密,生成签名数据。
  4. 打包: 将签名数据附加到升级包中,或者放入固件头部。
# 生成 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 验签流程(设备端)

  1. 预置公钥: 在设备出厂时,将公钥烧录到 bootloader 的只读区域。
  2. 接收升级包: 设备下载升级包后,先不急着刷写。
  3. 验签: bootloader 读取升级包中的签名数据,用公钥解密,与本地计算的 SHA-256 哈希比对。
  4. 通过则刷写,不通过则丢弃: 验签失败时,记录错误日志,并上报服务器。
避坑指南: 公钥一定要放在 bootloader 的只读区域,不能放在可擦写的应用分区。我曾经见过一个产品,公钥放在文件系统里,结果黑客通过漏洞篡改了公钥,然后用自己的私钥签了一个恶意固件……嗯,那是一次惨痛的教训。

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 个版本的差分包,避免版本跨度太大导致差分算法失效。

3.5 本章知识结构图

下面这张图,把升级包制作与签名的核心流程串起来了。你可以把它当作一个检查清单,做项目时对着看,不容易漏环节。

升级包制作与签名核心流程 差分升级(bsdiff/hdiffpatch) 生成差分包 压缩差分包(gzip/lzma) 全量升级包制作 添加元数据头部 填充对齐(4KB/64KB) 数字签名与验签 SHA-256 哈希计算 RSA/ECC 私钥签名 升级包版本管理(语义化版本号 + 依赖路径表) 设备端 bootloader 验签 → 刷写 → 回滚保护 说明: 差分升级优先,全量升级兜底;所有升级包必须签名;版本管理要维护升级路径表,避免跨版本过大导致差分失败。

嗯,到这里,升级包制作与签名的核心内容就讲完了。你可能会觉得内容有点多,但每个环节都是环环相扣的。差分算法省流量,全量包兜底,签名保安全,版本管理防混乱——少了哪一个,整个 OTA 系统都可能出问题。我建议你先把差分算法和签名机制跑通,再逐步完善版本管理策略。毕竟,能跑起来比什么都重要。