第二章 升级包制作与签名:固件镜像的生成、差分升级包制作、升级包的加密与签名机制、签名验证流程

好,咱们进入第二章。这一章讲的是升级包的“生产”环节。说白了,就是你怎么把一份干净的固件,变成一份能安全、可靠地送到设备手里的升级包。

我在项目里见过太多人,觉得升级包制作就是“打个包、签个名”那么简单。结果呢?要么升级包太大,设备下载到一半就断连;要么签名没做好,设备死活不认。嗯,这些坑我都踩过。今天咱们就把它彻底讲透。

2.1 固件镜像的生成

先说说固件镜像。你想想看,你的代码编译出来,是一堆 .bin 或者 .hex 文件。但设备要的是什么呢?是一份完整的、带校验的、能直接烧录的镜像。

我个人习惯,在编译脚本里加一个后处理步骤。比如这样:

# 生成原始固件
make clean && make

# 添加头部信息
python3 gen_header.py \
    --input build/firmware.bin \
    --output build/firmware_with_header.bin \
    --version 2.1.0 \
    --hw_rev B \
    --crc32

# 生成校验和
sha256sum build/firmware_with_header.bin > build/firmware.sha256

这里要注意几个点:

  • 版本号:一定要嵌入镜像头部。我遇到过设备升级后版本号没更新,结果下次升级时系统以为还是旧版本,直接跳过。
  • 硬件版本:不同硬件版本可能对应不同固件。我在项目中就吃过这个亏——给 A 版硬件刷了 B 版的固件,WiFi 模块直接不工作。
  • 校验和:CRC32 或者 SHA256 都行。但建议用 SHA256,安全性更高。

重要:镜像头部信息一定要放在固定偏移位置。设备启动时,Bootloader 会先读头部,校验完整性。如果头部格式不对,设备直接变砖。

2.2 差分升级包制作

全量升级包简单,但太大了。尤其是物联网设备,动不动几十兆的固件,OTA 下载慢得要命。这时候就需要差分升级包。

差分升级的原理,说白了就是:只传输新旧固件之间的差异部分。设备收到后,自己把差异“补”到旧固件上,生成新固件。

常用的工具有 bsdiff、hdiffpatch 等。我一般用 bsdiff,因为它对二进制文件的压缩率很好。举个例子:

# 制作差分包
bsdiff old_firmware.bin new_firmware.bin delta.patch

# 查看差分包大小
ls -lh delta.patch
# 输出:-rw-r--r-- 1 user user 1.2M delta.patch

# 对比全量包大小
ls -lh new_firmware.bin
# 输出:-rw-r--r-- 1 user user 8.5M new_firmware.bin

你看,8.5M 的全量包,差分后只有 1.2M。节省了 85% 的传输量。这对低带宽、弱信号的设备来说,太关键了。

小技巧:差分升级包制作时,建议把新旧固件都做一次对齐。比如都按 4K 边界对齐。这样设备在打补丁时,内存操作更高效。我曾经遇到过因为对齐问题,设备打补丁时内存越界,直接重启。

但差分升级也有坑。比如:

  • 旧固件必须完整:如果设备上的旧固件已经损坏,差分升级会失败。所以我在项目中,会先做一次固件完整性校验,再决定用差分还是全量。
  • 版本跨度不能太大:从 v1.0 直接差分到 v2.0,差异太大,差分包可能比全量包还大。我一般建议,版本跨度不超过 3 个版本。

2.3 升级包的加密与签名机制

好,包做好了。但你能直接发给设备吗?当然不行。万一被中间人篡改了呢?万一被黑客抓包分析了呢?

所以,加密和签名是必须的。这两者有什么区别?

功能 加密 签名
目的 防止内容泄露 防止内容篡改
使用场景 固件包含敏感数据(如密钥) 所有升级包都必须签名
算法示例 AES-256-CBC RSA-2048 / ECDSA
验证方式 解密后使用 验签后使用

我个人习惯,加密和签名一起用。先加密,再签名。流程是这样的:

# 1. 生成随机密钥
openssl rand -hex 32 > aes_key.txt

# 2. 用 AES 加密固件
openssl enc -aes-256-cbc \
    -in firmware.bin \
    -out firmware.enc \
    -pass file:aes_key.txt

# 3. 用 RSA 私钥签名
openssl dgst -sha256 -sign private.pem \
    -out firmware.sig firmware.enc

# 4. 打包:加密固件 + 签名 + 公钥证书
tar -czf upgrade_package.tar.gz \
    firmware.enc firmware.sig public.pem

警告:千万不要把私钥放到升级包里!私钥必须保存在安全的服务器上。我曾经见过一个项目,把私钥硬编码在固件里,结果被逆向出来,所有设备都能被伪造升级包攻击。

2.4 签名验证流程

设备收到升级包后,怎么验证?流程是这样的:

  1. 解包:从升级包中提取出加密固件、签名、公钥证书。
  2. 验签:用公钥验证签名是否匹配。如果签名不匹配,直接丢弃。
  3. 解密:用预置的密钥(或者从安全芯片中读取)解密固件。
  4. 校验:对解密后的固件做 CRC 或 SHA 校验,确保完整性。
  5. 写入:校验通过后,才写入 Flash。

代码实现大概是这样的:

bool verify_upgrade_package(const char* package_path) {
    // 1. 解包
    extract_package(package_path);
    
    // 2. 验签
    FILE* sig_file = fopen("firmware.sig", "rb");
    FILE* enc_file = fopen("firmware.enc", "rb");
    FILE* pub_file = fopen("public.pem", "rb");
    
    RSA* rsa = PEM_read_RSA_PUBKEY(pub_file, NULL, NULL, NULL);
    bool sig_ok = rsa_verify(rsa, enc_file, sig_file);
    
    if (!sig_ok) {
        log_error("签名验证失败!可能被篡改");
        return false;
    }
    
    // 3. 解密
    decrypt_firmware("firmware.enc", "firmware.bin");
    
    // 4. 校验
    uint32_t expected_crc = read_header_crc("firmware.bin");
    uint32_t actual_crc = calculate_crc("firmware.bin");
    
    if (expected_crc != actual_crc) {
        log_error("CRC 校验失败!固件损坏");
        return false;
    }
    
    return true;
}

这里有个细节:公钥怎么存?我建议放在 Bootloader 里,或者安全芯片中。绝对不能从网络下载。为什么?你想想看,如果公钥能从网络下载,那黑客也能伪造一个公钥给你,然后签一个恶意固件。

核心原则:信任链必须从硬件开始。Bootloader 验证升级包签名,Bootloader 本身是只读的。这样,整个升级流程才是安全的。

2.5 避坑指南

最后,分享几个我踩过的坑:

  • 签名算法不要用 MD5:MD5 已经被破解了。至少用 SHA256。
  • 密钥要定期轮换:我见过一个项目,用了 5 年没换过密钥。结果密钥泄露,所有设备都得召回。
  • 升级包要带版本号:防止降级攻击。设备应该拒绝比当前版本更低的升级包。
  • 测试环境和生产环境用不同的密钥:我曾经不小心把测试密钥发到了生产环境,结果设备验签失败,全部变砖。

嗯,这一章的内容就到这里。升级包制作和签名,看似简单,但每个环节都有坑。你只要按照我上面说的流程走,基本不会出大问题。下一章,咱们聊聊升级策略的设计——什么时候升级、怎么升级、失败了怎么办。