3. 升级包制作与签名:从打包到验签的完整链路
升级包,说白了就是车机要“吃”的那份数据。格式不对、签名无效、元数据缺失——任何一个环节出问题,整车的 OTA 流程就会卡住。我见过不少团队在升级包上栽跟头,今天咱们就把这块掰开揉碎了讲清楚。
3.1 升级包格式:.zip 还是 .tar.gz?
目前主流座舱方案里,升级包格式基本就两种:.zip 和 .tar.gz。选哪个?我个人的习惯是看目标系统的原生支持。
| 特性 | .zip | .tar.gz |
|---|---|---|
| 压缩率 | 中等(Deflate 算法) | 较高(gzip 压缩) |
| 流式处理 | 不支持(需随机访问) | 支持(可边下载边解压) |
| 嵌入式 Linux 原生支持 | 需额外库(minizip) | 内核自带(libtar + zlib) |
| 元数据存储 | 注释字段(有限制) | 可自定义 tar header |
我在一个项目里遇到过这样的坑:团队选了 .zip 格式,结果车机端解压时内存暴涨,因为 .zip 的中央目录在文件末尾,必须先把整个包下载完才能开始解压。而 .tar.gz 是流式结构,下载到哪就能解到哪,对嵌入式设备友好得多。
3.2 增量包 vs 全量包:什么时候用哪个?
全量包,就是把整个系统镜像打包,简单粗暴。增量包,只包含新旧版本之间的差异数据。你想想看,一个座舱系统动辄 2-3GB,全量下载对用户的流量和耐心都是考验。
我参与的第一个 OTA 项目,老板拍板说“全量包最稳”。结果每次升级要下载 2.8GB,用户投诉说“升级一次比看一部电影还慢”。后来我们切到增量包,平均大小降到 200-400MB,体验直接翻倍。
增量包的核心技术是 bsdiff 或 HDiffPatch。原理很简单:对比新旧两个文件的二进制差异,只打包变化的部分。
# 生成增量包(以 bsdiff 为例)
bsdiff old_firmware.bin new_firmware.bin delta_patch.bin
# 车机端应用增量包
bspatch old_firmware.bin new_firmware.bin delta_patch.bin
两种包的适用场景对比:
- 全量包:首次升级、跨大版本升级、恢复出厂后的升级、增量包应用失败后的回退方案
- 增量包:小版本迭代、安全补丁、配置更新、频繁推送的场景
3.3 数字签名与验签机制:防篡改的最后一道防线
升级包在传输过程中可能被劫持、篡改。没有签名验证的 OTA,相当于把车钥匙挂在门外。我见过一个案例:某团队没做签名,结果黑客伪造了一个升级包,把车机刷成了砖——最后只能返厂维修。
签名验签的标准流程:
- 服务端:用私钥对升级包的哈希值签名,生成签名文件
- 车机端:用预置的公钥验证签名,确认包未被篡改
- 验签通过后:再校验包的完整性(哈希比对)
常用的签名算法:
| 算法 | 密钥长度 | 签名大小 | 安全性 |
|---|---|---|---|
| RSA-2048 | 2048 bit | 256 字节 | 高(推荐) |
| ECDSA-P256 | 256 bit | 64 字节 | 更高(性能更好) |
| Ed25519 | 256 bit | 64 字节 | 极高(新兴方案) |
验签的代码示例(车机端,C 语言风格):
// 1. 读取升级包和签名文件
uint8_t* package_data = read_file("update.zip");
uint8_t* signature = read_file("update.zip.sig");
// 2. 计算包的哈希值
uint8_t hash[SHA256_DIGEST_LENGTH];
SHA256(package_data, package_size, hash);
// 3. 用公钥验证签名
EVP_PKEY* public_key = load_public_key_from_secure_storage();
int result = RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH,
signature, signature_size, public_key);
if (result == 1) {
// 验签通过,继续升级流程
apply_update(package_data);
} else {
// 验签失败,拒绝升级并记录日志
log_error("Signature verification failed!");
abort_update();
}
3.4 升级包元数据设计:让车机知道“升级什么”
元数据,就是升级包的“身份证”。车机拿到包后,第一件事就是读元数据,判断这个包能不能装、该不该装。
我习惯把元数据放在包内的固定位置,比如 metadata.json 或 MANIFEST 文件。下面是一个典型的元数据设计:
{
"format_version": "1.0",
"package_type": "incremental",
"target_version": "2.1.0",
"base_version": "2.0.0",
"product": "SmartCockpit_X1",
"hardware_revision": "rev_b",
"components": [
{
"name": "system",
"type": "firmware",
"size": 245760,
"hash": "sha256:a1b2c3d4e5f6...",
"install_path": "/ota/system"
},
{
"name": "hmi",
"type": "application",
"size": 102400,
"hash": "sha256:7890abcdef12...",
"install_path": "/ota/hmi"
}
],
"signature": {
"algorithm": "RSA-2048",
"value": "base64_encoded_signature..."
}
}
元数据里必须包含的字段:
- package_type:全量还是增量,车机据此选择不同的安装策略
- target_version / base_version:目标版本和基线版本,用于版本兼容性检查
- hardware_revision:硬件版本,防止把 rev_a 的包刷到 rev_b 的车上
- components:组件列表,每个组件独立校验哈希,支持部分升级
- signature:签名信息,用于验签
hardware_revision 字段,结果同一份包被刷到了不同硬件版本的车机上,导致屏幕驱动不兼容,黑屏了一大片。从那以后,我要求元数据里必须包含硬件版本,并且车机端要做严格匹配。
3.5 整体流程:一张图看懂升级包制作与签名
下面这张 SVG 图展示了从制作到验签的完整链路:
这张图把服务端和车机端的职责分得很清楚。服务端负责“做包”和“签名”,车机端负责“验签”和“安装”。中间通过 OTA 通道传输。安全区域(TEE/eSE)独立于主系统,专门存放公钥和执行验签,即使主系统被攻破,签名验证依然可信。