升级包制作与签名:从原始固件到安全交付
好,咱们进入第二章。上一章聊了日志系统怎么搭,这一章我们聚焦一个更核心的问题——你辛辛苦苦写完固件,怎么把它做成一个可靠的、安全的升级包?
我见过太多团队,代码写得漂亮,结果升级包格式搞错了,或者没做校验,现场升级直接变砖。说白了,升级包就是固件的「最后一公里」,这步出问题,前面全白干。
bin 与 hex:两种最常见的固件格式
先搞清楚我们要处理什么。嵌入式世界里,bin 和 hex 是两大主流格式。
| 特性 | bin 格式 | hex 格式 |
|---|---|---|
| 内容 | 纯二进制数据,无地址信息 | ASCII 文本,包含地址、长度、校验 |
| 体积 | 紧凑,与 Flash 大小一致 | 比 bin 大约 2-3 倍 |
| 可读性 | 不可读 | 文本编辑器可打开 |
| 烧录方式 | 需指定起始地址 | 自带地址,分段烧录 |
| 典型场景 | OTA 升级包、量产烧录 | 调试器下载、分段烧录 |
我个人习惯,量产和 OTA 用 bin,调试阶段用 hex。为什么?bin 干净利落,没有冗余信息,传输效率高。hex 虽然大,但每行都有地址和校验,烧录器可以逐行验证,出问题能精确定位到哪一行数据坏了。
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin。一行命令,搞定。
CRC 校验:快速检测数据完整性
升级包做好了,怎么保证它传输过程中没被篡改或损坏?最简单的办法就是加个校验。
CRC(循环冗余校验)是嵌入式里最常用的校验方式。它不像哈希那么重,但足够检测出常见的传输错误。
我常用的 CRC 参数是 CRC-32(IEEE 802.3 标准):
// CRC-32 查表法实现(简化版)
uint32_t crc32(const uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
}
return ~crc;
}
嗯,这里要注意:CRC 只能检测随机错误,不能防恶意篡改。如果有人故意修改了固件,重新算个 CRC 附上去,你根本发现不了。所以 CRC 适合做「完整性校验」,不适合做「安全校验」。
哈希算法:更可靠的完整性保证
哈希算法比 CRC 强在哪?它有两个关键特性:
- 单向性: 从哈希值无法反推出原始数据
- 抗碰撞性: 几乎不可能找到两个不同数据有相同哈希值
嵌入式里常用的哈希算法有 SHA-256 和 MD5。MD5 已经被证明不安全了,我建议直接用 SHA-256。
你想想看,SHA-256 输出 32 字节,CRC-32 输出 4 字节。这 28 字节的差距,就是安全性的差距。
// 伪代码:计算固件包的 SHA-256
uint8_t firmware_hash[32];
SHA256_CTX ctx;
sha256_init(&ctx);
sha256_update(&ctx, firmware_bin, firmware_len);
sha256_final(&ctx, firmware_hash);
// 将哈希值附加到升级包末尾
memcpy(package + package_len, firmware_hash, 32);
package_len += 32;
但哈希也有局限——它只能保证数据没变,不能证明数据是谁发的。这就引出了数字签名。
数字签名:让升级包「验明正身」
数字签名是解决「信任」问题的。它用非对称加密:私钥签名,公钥验证。
流程是这样的:
- 服务器用私钥对固件哈希值加密,生成签名
- 设备收到升级包后,用公钥解密签名,得到哈希值
- 设备自己算一遍固件的哈希值
- 两个哈希值一致,说明固件是官方发布的,且未被篡改
我常用的签名算法是 ECDSA(椭圆曲线数字签名算法)。它比 RSA 密钥更短,计算更快,特别适合资源受限的 MCU。
升级包结构设计
说了这么多,一个完整的升级包到底长什么样?我画个图给你看。
这个结构我用了好多年,基本没出过问题。注意几点:
- 魔数: 一个固定值(比如 0xAA55),用来快速判断是不是合法的升级包
- 版本号: 必须包含主版本、次版本、修订号,防止降级攻击
- 签名区: 放在最后,方便设备边接收边计算哈希,最后再验证签名
签名验证的完整流程
最后,我把整个流程串起来,给你一个可落地的参考。
// 设备端:升级包验证流程(伪代码)
int verify_firmware_package(const uint8_t *package, uint32_t pkg_len) {
// 1. 解析包头
PackageHeader *hdr = (PackageHeader *)package;
if (hdr->magic != PACKAGE_MAGIC) return ERR_INVALID_MAGIC;
if (hdr->version <= current_version) return ERR_VERSION_ROLLBACK;
// 2. 提取固件数据和签名
uint32_t fw_len = hdr->fw_length;
const uint8_t *fw_data = package + sizeof(PackageHeader);
const uint8_t *signature = fw_data + fw_len;
// 3. 计算固件哈希
uint8_t hash[32];
sha256(fw_data, fw_len, hash);
// 4. 用公钥验证签名
if (ecdsa_verify(public_key, hash, signature) != 0) {
return ERR_SIGNATURE_MISMATCH;
}
// 5. 可选:再算一次 CRC 做快速校验
uint32_t crc = crc32(fw_data, fw_len);
if (crc != hdr->crc32) return ERR_CRC_MISMATCH;
return ERR_OK;
}
好了,这一章的内容就到这。升级包制作和签名,说白了就是三件事:选对格式、算对校验、签对名。每一步都有坑,但只要你按这个流程走,基本稳了。