4. 升级镜像与包格式

在多分区 OTA 部署中,升级包并不是一个简单的“固件文件”,而是由镜像、元数据、签名、压缩层共同组成的结构化容器。理解包格式,是设计升级流程、控制升级体积、保证升级安全的前提。本节从包类型、镜像组织、签名机制、压缩策略和包结构解析五个维度展开,帮助你在实际工程中做出正确的格式选型。

一、四种升级包类型的本质区别

OTA 升级包按“传输内容”可分为全量包、增量包、差分包和混合包。它们的核心差异在于:升级时设备端需要写入多少数据,以及包本身携带的是“完整镜像”还是“变化描述”。

  • 全量包(Full Package):包含所有分区的完整镜像,设备端直接擦写目标分区。优点是逻辑简单、可独立恢复;缺点是体积大,通常为固件总大小的 100%。
  • 增量包(Incremental Package):只包含发生变化的文件或分区块,通常以文件系统级差异描述。体积约为全量包的 10%~30%,但依赖设备端当前版本与基线版本严格一致。
  • 差分包(Differential Package):基于二进制差分算法(如 bsdiff、hdiffpatch)生成旧镜像到新镜像的补丁。体积最小,可低至全量包的 5%~15%,但需要设备端具备旧镜像并执行 patch 运算。
  • 混合包:对关键分区使用全量镜像,对非关键分区使用差分补丁,兼顾可靠性与体积。
选型原则:若设备存储紧张且版本基线可控,优先差分包;若升级可靠性要求极高、网络带宽充足,优先全量包;增量包适合文件系统级更新,但需严格校验基线版本。

二、镜像签名与信任链

升级包在分发前必须经过签名,设备端在写入前必须验签。典型做法是对包内元数据(manifest)计算摘要,再用私钥签名;设备端用预置公钥验签,验签通过后才允许解压和写入。签名对象通常包括:包版本号、目标设备型号、分区列表、各镜像哈希值、时间戳和随机数。

# 典型签名与验签流程(示意)
# 1. 生成镜像哈希
sha256sum system.img > system.img.sha256
# 2. 生成 manifest
cat manifest.json
{
  "version": "2.3.1",
  "device": "edge-gw-x1",
  "partitions": [
    {"name": "boot", "hash": "a1b2..."},
    {"name": "system", "hash": "c3d4..."}
  ],
  "timestamp": 1710000000
}
# 3. 对 manifest 签名
openssl dgst -sha256 -sign private.pem -out manifest.sig manifest.json
# 4. 设备端验签
openssl dgst -sha256 -verify public.pem -signature manifest.sig manifest.json

签名必须覆盖“包内容摘要”,而不是仅覆盖文件名或版本号。否则攻击者可以替换镜像内容而保留合法签名。推荐使用 ECDSA P-256 或 RSA-3072,并在设备端安全存储公钥,防止被篡改。

三、压缩格式与包结构解析

升级包通常采用“外层容器 + 内层压缩”的结构。外层容器负责描述分区布局和元数据,内层压缩负责减小传输体积。常见压缩格式包括 gzip、xz、zstd 和 lz4。zstd 在压缩率和解压速度之间平衡较好,适合资源受限设备;xz 压缩率最高但解压内存占用大;lz4 解压极快但压缩率一般。

压缩格式 压缩率 解压速度 内存占用 适用场景
gzip中中低通用兼容
xz高慢高带宽受限、存储充足
zstd中高快中推荐默认
lz4低极快低实时性要求高

一个典型的 OTA 包结构如下:包头(magic、版本、包类型)、manifest(分区表、哈希、签名)、压缩数据段(各分区镜像或补丁)、尾部校验(CRC 或整体哈希)。解析时先读包头定位 manifest,验签后再按分区表逐段解压写入。

# 包结构示意
OTA_PACKAGE
├── header (magic, version, type)
├── manifest.json
├── manifest.sig
├── payload/
│   ├── boot.img.zst
│   ├── system.img.zst
│   └── vendor.patch.zst
└── footer (crc32, total_hash)

四、实战场景:差分包生成与校验

假设设备当前运行 v2.2.0,目标版本 v2.3.1。使用 hdiffpatch 生成差分包:

# 生成差分包
hdiffz -f -c-zstd old_system.img new_system.img system.patch
# 设备端应用补丁
hpatchz old_system.img system.patch new_system.img
# 校验新镜像哈希
sha256sum new_system.img

设备端应用补丁前,必须校验旧镜像哈希与 manifest 中记录的基线哈希一致。若不一致,说明设备当前版本被篡改或分区损坏,应拒绝升级并回退到全量包。这是差分包最容易出问题的环节。

常见误区:只校验补丁包签名,不校验旧镜像基线哈希。攻击者可以保留合法补丁包,但替换设备端旧镜像,导致 patch 后得到恶意镜像。正确做法是“补丁包签名 + 旧镜像哈希 + 新镜像哈希”三重校验。

五、包结构解析的工程要点

  • 分区对齐:镜像写入需按分区块大小对齐,避免写入越界或校验失败。
  • 流式解压:设备端应支持边解压边写入,避免将整个镜像缓存在内存中。
  • 断点续传:包结构应支持分块校验,便于网络中断后从断点继续下载。
  • 版本兼容:manifest 中应声明最低可升级版本和最高可升级版本,防止跨版本差分失败。
  • 回滚包:建议同时保留一份全量回滚包,在升级失败时快速恢复。

下图展示了升级包从生成到设备端解析的完整数据流,包括全量包、增量包、差分包三种路径在签名、压缩和校验环节的差异。

OTA 升级包生成与设备端解析数据流 包生成侧 旧镜像 v2.2.0 boot / system / vendor 新镜像 v2.3.1 boot / system / vendor 差分/增量算法 bsdiff / hdiffpatch 签名 + 压缩 ECDSA + zstd OTA 包 (.ota) OTA 包结构 Header:magic / version / type Manifest:分区表 / 哈希 / 版本约束 Manifest.sig:签名(ECDSA P-256) Payload(压缩数据段) boot.img.zst / system.img.zst vendor.patch.zst(差分包) Footer:CRC32 / total_hash 校验链:签名 → 旧镜像哈希 → 新镜像哈希 任一失败则拒绝写入并回滚 设备端解析 1. 读取 Header 识别包类型与版本 2. 验签 Manifest 公钥验证签名 3. 校验旧镜像哈希 差分包基线一致性 4. 流式解压写入 zstd 解压 + 分区写入 5. 校验新镜像哈希 写入后回读校验 6. 标记启动分区 镜像/元数据 签名/差分 压缩/校验 包容器 失败回滚

总结来看,升级包格式的选择本质上是“体积、可靠性、计算开销”三者的权衡。全量包最稳但最大,差分包最小但依赖基线,增量包居中。无论选择哪种,签名覆盖内容摘要、旧镜像哈希校验、流式解压写入和写入后回读校验,都是不可省略的工程底线。