4. 升级包制作与签名

OTA升级能否安全落地,很大程度上取决于升级包本身是否“可信、可验、可还原”。一个完整的升级包不只是把固件二进制文件改个后缀,它需要经过打包、压缩、签名、封装元数据,并在设备端通过证书链和摘要校验确认来源与完整性。本节围绕这条制作链路展开,重点说明每一步的输入输出、关键参数以及设备端如何判断“这个包能不能装”。

一、升级包的基本结构与打包

升级包通常由三部分组成:头部元数据、固件载荷和签名信息。头部元数据描述包的身份与安装约束,常见字段包括:包格式版本、产品型号、硬件版本、当前固件版本、目标固件版本、载荷长度、载荷摘要算法、签名算法、签名长度、时间戳、防回滚计数器等。固件载荷是实际写入Flash的镜像,可能是完整镜像,也可能是差分补丁。签名信息则是对头部与载荷计算摘要后再用私钥加密得到的签名值。

打包时先确定镜像布局。以常见的MCU OTA为例,Flash可划分为Bootloader区、应用A区、应用B区、参数区。升级包中的载荷对应“应用B区”的完整镜像或差分补丁。打包工具读取编译输出的app.bin,按固定偏移拼接头部,再追加签名块,最终生成firmware_v2.3.0.pkg。头部中必须包含目标分区和写入地址,否则设备端无法判断该写到哪个区域。

打包时建议将头部字段按固定字节序(如小端)和固定长度定义,避免不同工具链解析不一致。头部中应保留magic字段,例如0x4F544150,用于快速识别包格式。

二、差分压缩:减少传输量

当设备已经运行旧版本固件时,传输完整镜像可能达到数百KB甚至数MB,而差分升级只传输新旧版本之间的差异部分。差分算法常用bsdiff或hdiffpatch,其基本思路是:以旧固件为参考,找出新固件中可复用的数据块,只记录新增、修改和偏移信息,生成补丁文件。设备端再用旧固件和补丁在本地重建新固件。

差分压缩的关键参数包括:块大小、窗口大小、压缩级别。块大小过小会导致补丁索引膨胀,过大则复用率下降。以示例参数为例,旧固件app_v2.2.0.bin为512KB,新固件app_v2.3.0.bin为520KB,使用bsdiff生成补丁patch_v220_to_v230.bin,若差异主要集中在少量函数和配置区,补丁可能只有30~60KB。补丁生成后通常再用gzip或lzma压缩,进一步降低体积。

差分升级的约束是:设备端必须持有与生成补丁时完全一致的旧固件。如果旧固件被篡改或版本不匹配,重建结果会失败。因此补丁头部必须记录旧固件摘要和新固件摘要,设备端在应用补丁前先校验旧固件摘要,重建后再校验新固件摘要。

三、数字签名与证书链

数字签名解决的是“这个包是不是我们签的”以及“包有没有被改过”。制作方在服务器端生成密钥对,私钥严格保存在签名服务器或HSM中,公钥则以证书形式预置到设备。签名流程是:对头部和载荷计算摘要(如SHA-256),再用私钥对摘要进行签名(如ECDSA P-256或RSA-2048),将签名值写入包尾。

设备端验证时,需要一条证书链:设备内置根证书(Root CA),根证书签发中间证书,中间证书签发固件签名证书。设备收到升级包后,先验证签名证书是否由中间证书签发、中间证书是否由根证书签发,再检查证书有效期、用途扩展和吊销状态。证书链验证通过后,用签名证书中的公钥验证包签名。这样即使签名证书泄露,也可以通过吊销中间证书来阻断后续升级。

不要把私钥放在构建机或代码仓库中。签名操作应通过独立的签名服务完成,并记录每次签名的包摘要、版本号和操作人,便于追溯。

四、防篡改校验与设备端判断

防篡改校验贯穿制作与安装两端。制作端在打包完成后计算整个包的摘要,写入签名块;设备端在下载完成后重新计算摘要并与签名中的摘要比对。若不一致,直接丢弃。对于差分包,还要额外校验旧固件摘要和新固件摘要。安装完成后,Bootloader应再次校验写入分区的镜像摘要,确认写入过程没有出错。

设备端判断逻辑可以概括为:包格式识别 → 头部解析 → 证书链验证 → 签名验证 → 摘要比对 → 版本与防回滚检查 → 写入或打补丁 → 安装后校验。任何一步失败都应记录错误码并停止升级,避免写入不完整镜像导致设备变砖。

升级包制作与设备端校验链路 制作端(服务器/构建机) 编译输出 app_v2.3.0.bin 旧固件参考 app_v2.2.0.bin 差分压缩(bsdiff / hdiffpatch) 生成 patch_v220_to_v230.bin,记录新旧摘要 打包:头部 + 载荷 + 签名块 magic / 版本 / 分区 / 长度 / 摘要算法 SHA-256 摘要 + ECDSA 签名 私钥保存在签名服务/HSM,不落地构建机 输出:firmware_v2.3.0.pkg 设备端(Bootloader / OTA Agent) ① 接收 .pkg,识别 magic 与格式版本 ② 解析头部:型号 / 分区 / 版本 / 长度 ③ 证书链验证:Root → 中间 → 签名证书 ④ 用证书公钥验证包签名 ⑤ 摘要比对 + 旧固件摘要校验(差分包) ⑥ 写入/打补丁 → 安装后再次校验镜像摘要 传输 任一步失败:记录错误码,停止升级,保留旧固件

五、制作与校验的完整闭环

把上述环节串起来,一次可复现的升级包制作流程如下:

  1. 从CI获取新固件app_v2.3.0.bin,确认编译版本号与目标硬件型号一致。
  2. 若走差分升级,用旧固件app_v2.2.0.bin生成补丁,记录旧固件SHA-256和新固件SHA-256。
  3. 按协议拼接头部,写入magic、格式版本、产品型号、硬件版本、目标分区、写入地址、载荷长度、摘要算法、签名算法、时间戳、防回滚计数器。
  4. 对头部和载荷计算SHA-256,调用签名服务用私钥生成ECDSA签名,将签名值追加到包尾。
  5. 输出最终包,并生成一份manifest.json,记录包名、版本、摘要、签名算法、证书序列号,供发布系统存档。
  6. 设备端下载后按“格式识别→头部解析→证书链→签名→摘要→版本检查→写入→安装后校验”的顺序执行,任一步失败即中止。

其中,防回滚计数器是容易被忽略但很关键的一项。设备端保存一个单调递增的计数器,升级包头部中的计数器必须大于设备当前值,否则拒绝安装。这样可以防止攻击者把设备降级到存在已知漏洞的旧版本。计数器在每次成功升级后更新,并写入掉电不丢失的存储区。

最后,建议在发布前做一次“负向测试”:故意修改包中一个字节,确认设备端摘要校验失败;故意使用过期证书,确认证书链验证失败;故意把版本号改低,确认防回滚检查生效。只有正向能装、负向能拦,升级包制作与签名链路才算真正闭环。