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应再次校验写入分区的镜像摘要,确认写入过程没有出错。
设备端判断逻辑可以概括为:包格式识别 → 头部解析 → 证书链验证 → 签名验证 → 摘要比对 → 版本与防回滚检查 → 写入或打补丁 → 安装后校验。任何一步失败都应记录错误码并停止升级,避免写入不完整镜像导致设备变砖。
五、制作与校验的完整闭环
把上述环节串起来,一次可复现的升级包制作流程如下:
- 从CI获取新固件
app_v2.3.0.bin,确认编译版本号与目标硬件型号一致。 - 若走差分升级,用旧固件
app_v2.2.0.bin生成补丁,记录旧固件SHA-256和新固件SHA-256。 - 按协议拼接头部,写入magic、格式版本、产品型号、硬件版本、目标分区、写入地址、载荷长度、摘要算法、签名算法、时间戳、防回滚计数器。
- 对头部和载荷计算SHA-256,调用签名服务用私钥生成ECDSA签名,将签名值追加到包尾。
- 输出最终包,并生成一份
manifest.json,记录包名、版本、摘要、签名算法、证书序列号,供发布系统存档。 - 设备端下载后按“格式识别→头部解析→证书链→签名→摘要→版本检查→写入→安装后校验”的顺序执行,任一步失败即中止。
其中,防回滚计数器是容易被忽略但很关键的一项。设备端保存一个单调递增的计数器,升级包头部中的计数器必须大于设备当前值,否则拒绝安装。这样可以防止攻击者把设备降级到存在已知漏洞的旧版本。计数器在每次成功升级后更新,并写入掉电不丢失的存储区。
最后,建议在发布前做一次“负向测试”:故意修改包中一个字节,确认设备端摘要校验失败;故意使用过期证书,确认证书链验证失败;故意把版本号改低,确认防回滚检查生效。只有正向能装、负向能拦,升级包制作与签名链路才算真正闭环。