3、升级包制作与签名:固件打包工具、差分升级算法(bsdiff)、数字签名与验签机制
好,咱们进入第三个核心环节——升级包的制作与签名。说白了,就是怎么把新固件变成一个“安全、小巧、可信”的安装包。
我见过不少团队,升级包就是直接把整个固件扔过去。设备多、网络差的时候,那体验简直灾难。所以,这一章咱们重点聊三件事:打包工具怎么选、差分算法怎么用、签名机制怎么搭。
3.1 固件打包工具:不只是“打个包”那么简单
很多人觉得打包就是 zip 一下。嗯,在 CPE 设备上,真没那么简单。
一个标准的升级包,通常包含:
- 固件本体:新的系统镜像、内核、文件系统
- 元数据:版本号、硬件平台、校验和、签名
- 升级脚本:告诉设备先擦哪里、再写哪里
我个人习惯用 cpio + gzip 的组合。为什么?因为 Linux 内核启动时原生支持 cpio 格式,解压快,不依赖额外工具。你想想看,设备在升级模式下,能跑的文件系统很有限,越简单越可靠。
这里给一个我常用的打包脚本片段:
#!/bin/bash
# 打包固件升级包
FW_VERSION="v2.1.0"
HW_PLATFORM="mt7981"
# 创建临时目录
mkdir -p upgrade_pkg
# 拷贝固件
cp openwrt-ramips-mt7981-squashfs-sysupgrade.bin upgrade_pkg/firmware.bin
# 生成元数据
cat > upgrade_pkg/manifest.json << EOF
{
"version": "$FW_VERSION",
"platform": "$HW_PLATFORM",
"build_time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"checksum": "$(sha256sum upgrade_pkg/firmware.bin | cut -d' ' -f1)"
}
EOF
# 打包成 cpio 格式
cd upgrade_pkg
find . -print | cpio -o -H newc | gzip > ../upgrade_${FW_VERSION}_${HW_PLATFORM}.bin
cd ..
3.2 差分升级算法:bsdiff 的实战应用
全量升级包动辄几十 MB,对于 4G/5G CPE 设备来说,用户流量吃不消。这时候,差分升级就派上用场了。
bsdiff 是目前嵌入式领域最主流的差分算法。它的核心思想是:只传输新旧固件之间的差异部分。说白了,就是“找不同”,然后只发不同的字节。
我最早接触 bsdiff 是在一个智能路由器的项目上。当时固件从 20MB 涨到了 35MB,OTA 成功率直线下降。用了 bsdiff 后,差分包通常只有 3-8MB,成功率直接拉回到 99% 以上。
bsdiff 的使用非常简单:
# 生成差分包
bsdiff old_firmware.bin new_firmware.bin upgrade_diff.patch
# 在设备端合并
bspatch old_firmware.bin new_firmware.bin upgrade_diff.patch
但这里有个坑——bsdiff 对内存的消耗很大。生成差分包时,它需要把新旧固件都加载到内存里做比对。我遇到过 128MB 内存的设备,跑 bsdiff 直接 OOM(内存溢出)。
所以,我的建议是:
- 服务器端:用 bsdiff,因为服务器资源充足
- 设备端:如果内存 < 128MB,考虑 hdiffpatch 或 xdelta3
3.3 数字签名与验签机制:防止“假升级”
升级包做好了,也差分完了,但直接下发?不行。万一中间人篡改了升级包,设备升级后变砖怎么办?
数字签名,就是给升级包加一把“锁”。只有用你的私钥签名的包,设备才认。
我常用的签名流程是 ECDSA(椭圆曲线数字签名算法)。相比 RSA,ECDSA 的签名更短、计算更快,非常适合资源受限的嵌入式设备。
签名过程:
# 生成密钥对(服务器端执行)
openssl ecparam -genkey -name prime256v1 -out private.pem
openssl ec -in private.pem -pubout -out public.pem
# 对升级包签名
openssl dgst -sha256 -sign private.pem -out upgrade_pkg.sig upgrade_pkg.bin
# 将签名附加到升级包尾部
cat upgrade_pkg.bin upgrade_pkg.sig > upgrade_pkg_signed.bin
设备端验签:
// C 语言验签示例(使用 mbedtls)
#include <mbedtls/pk.h>
#include <mbedtls/sha256.h>
int verify_upgrade_package(const uint8_t *pkg, size_t pkg_len) {
mbedtls_pk_context pk;
uint8_t hash[32];
size_t sig_len = 64; // ECDSA P-256 签名长度
// 分离固件和签名
const uint8_t *firmware = pkg;
size_t fw_len = pkg_len - sig_len;
const uint8_t *signature = pkg + fw_len;
// 计算固件哈希
mbedtls_sha256(firmware, fw_len, hash, 0);
// 加载公钥
mbedtls_pk_init(&pk);
mbedtls_pk_parse_keyfile(&pk, "/etc/public.pem", NULL);
// 验签
int ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256,
hash, 0, signature, sig_len);
mbedtls_pk_free(&pk);
return ret;
}
3.4 完整流程串起来
好了,我们把这三步串起来,看看一个完整的升级包制作流程:
- 打包:用 cpio 将固件、元数据、升级脚本打包成 .bin 文件
- 差分:用 bsdiff 对比新旧固件,生成差分补丁
- 签名:用 ECDSA 私钥对差分包签名,附加到包尾部
- 下发:将签名后的差分包通过 OTA 服务器下发到设备
设备端收到后,先验签,再合并差分,最后校验完整性,确认无误后写入 Flash。
这个流程,我在三个不同的 CPE 项目上验证过,稳定运行超过两年。你照着搭,基本不会出大问题。
| 环节 | 工具/算法 | 注意事项 |
|---|---|---|
| 打包 | cpio + gzip | 注意文件权限和路径 |
| 差分 | bsdiff / hdiffpatch | 内存不足时切换算法 |
| 签名 | ECDSA P-256 | 公钥存储在 OTP 中 |