升级包制作与签名:全量包与差分包生成、数字签名与验签流程、升级包版本管理规范
升级包怎么做?说白了就两种路子:全量包和差分包。我刚开始带项目时,总觉得差分包高大上,恨不得每次都打差分。结果有一次差分算法选错了,升级包倒是小,设备升级后直接变砖……嗯,从那以后我学乖了——场景决定方案。
全量包:简单粗暴,但最稳
全量包就是把整个固件镜像打包成一个升级文件。设备拿到后,直接擦除旧分区,写入新数据。好处是啥?逻辑简单,容错率高。哪怕升级过程中断电,重启后还能进恢复模式重试。
我个人的习惯是:首次量产、重大版本升级、或者设备存储空间充裕时,优先用全量包。别为了省那几百KB的流量,把稳定性搭进去。
- 设备首次出厂或恢复出厂后的首次升级
- 跨大版本升级(如 v1.x → v2.x)
- 文件系统结构发生重大变化
- 设备存储空间充足,网络带宽不是瓶颈
差分包:省流量,但坑多
差分包只包含新旧版本之间的差异数据。算法上主流的有 bsdiff、hdiffpatch、xdelta 等。我踩过最大的坑是 bsdiff 在低端 MCU 上跑不动——内存不够,直接 OOM。
后来我改用 hdiffpatch,它对嵌入式更友好。但要注意:差分算法对二进制文件的布局敏感。编译器版本变了、链接顺序变了,哪怕源码没改,生成的 bin 也可能差异巨大,差分包反而比全量包还大。
数字签名与验签:防篡改的最后一道防线
升级包如果不签名,就等于把设备的安全拱手让人。我见过一个案例:某厂商的升级包没签名,黑客伪造了一个恶意固件,通过中间人攻击推送下去,整批设备全部被控。惨痛教训。
签名流程其实不复杂:
- 生成密钥对:私钥留在服务器,公钥烧录到设备 Bootloader 中
- 计算哈希:对升级包做 SHA-256 或 SHA-512
- 签名:用私钥对哈希值做 RSA 或 ECDSA 签名
- 打包:将签名值附加到升级包头部
验签流程在设备端:
- 设备收到升级包,提取签名值
- 用公钥解密签名,得到原始哈希
- 对升级包重新计算哈希
- 比对两个哈希值,一致则通过
升级包版本管理规范
版本号乱写,运维起来就是灾难。我接手过一个项目,版本号是“v1.0_final_真的最终版_不改了.bin”……你想想看,这种命名怎么自动化?
我建议采用 语义化版本 + 内部构建号 的双层结构:
| 字段 | 示例 | 说明 |
|---|---|---|
| 主版本号 | 2 | 不兼容的 API 或重大功能变更 |
| 次版本号 | 3 | 向下兼容的功能新增 |
| 修订号 | 1 | Bug 修复或小优化 |
| 构建号 | 20250315 | CI 自动生成,精确到日期 |
完整的版本号示例:v2.3.1_build20250315。这个格式既能让人看懂,也方便脚本做大小版本比较。
升级包结构设计
一个标准的升级包,我习惯这样组织:
ota_package_v2.3.1_build20250315.bin
├── 包头 (Header)
│ ├── 魔数 (Magic Number) // 0xOTA1
│ ├── 版本号 // v2.3.1
│ ├── 包类型 // 0x01 全量 / 0x02 差分
│ ├── 目标设备型号 // HW-03
│ ├── 包体长度 // 4 字节
│ ├── 包体哈希 (SHA-256) // 32 字节
│ └── 签名值 (RSA-2048) // 256 字节
├── 包体 (Payload)
│ ├── 固件镜像 // 实际升级数据
│ └── 元数据 // 分区表、校验和等
└── 尾部 (Footer)
└── 包尾标记 // 0xOTA_END
这个结构的好处是:设备可以先解析包头,验证签名和哈希,确认无误后再解析包体。如果签名验证失败,直接丢弃,不浪费后续的解析时间。
签名与验签的代码骨架
服务端签名(Python 示例):
import hashlib
from Crypto.Signature import pkcs1_15
from Crypto.PublicKey import RSA
def sign_package(package_bytes, private_key_path):
# 1. 计算哈希
h = hashlib.sha256(package_bytes).digest()
# 2. 加载私钥
with open(private_key_path, 'rb') as f:
key = RSA.import_key(f.read())
# 3. 签名
signature = pkcs1_15.new(key).sign(h)
return signature
设备端验签(C 伪代码):
int verify_signature(uint8_t *package, uint32_t len,
uint8_t *signature, uint8_t *public_key) {
uint8_t hash[32];
SHA256(package, len, hash);
// RSA 验签
if (RSA_verify(public_key, hash, signature) != 0) {
return VERIFY_PASS;
}
return VERIFY_FAIL;
}
升级包版本管理流程
我推荐用 Git Tag + CI 自动构建 来管理版本:
- 开发完成,合并到 master 分支
- 打 Git Tag:
git tag v2.3.1_build20250315 - CI 检测到新 Tag,自动拉代码、编译、打包、签名
- 生成的升级包上传到 OTA 服务器,版本号写入数据库
- 设备端轮询服务器,比对版本号,决定是否升级
这样做的好处是:版本号与代码提交一一对应,可追溯、可回滚。出了问题,直接查 Tag 对应的 commit,定位问题非常快。
- 全量包稳,差分包省,按场景选,别盲目
- 签名验签是安全底线,公钥必须放在 Bootloader 只读区
- 版本号用语义化版本 + 构建号,比较时按段解析整数
- 升级包结构要支持先验签后解析,避免无效操作
- CI 自动构建 + Git Tag,保证版本可追溯