3、固件升级核心流程:升级包制作、签名与加密、差分升级算法(bsdiff)、断点续传机制

好,咱们直接进入正题。固件升级这事儿,看着简单,做起来全是坑。我这些年经手的医疗设备,小到血糖仪,大到CT机,升级流程出过的事故,够我写一本《血泪史》了。

今天我把核心流程拆成四个环节来讲:升级包怎么做、怎么保证它没被篡改、怎么让升级包尽量小、以及万一断了怎么办。这四个环节,一个都不能少。

3.1 升级包制作:别把整个固件扔过去

很多人一开始的想法很直接:把新固件整个打包,发给设备,完事。但你想想看,一个嵌入式设备,Flash可能就几兆,网络带宽也有限。你扔一个几十兆的包过去,设备直接卡死。

我个人的习惯是,升级包要包含三个部分:

  • 元数据头:版本号、硬件平台ID、校验算法标识、包大小、签名信息。这部分是明文,方便设备快速判断要不要升级。
  • 固件数据体:可以是完整镜像,也可以是差分补丁。后面会细讲。
  • 尾部校验:通常是SHA256哈希值,加上数字签名。

这里有个小细节。元数据头里一定要放一个硬件平台ID。我在项目中遇到过,同一款设备的不同批次,MCU型号换了,固件不兼容。结果升级包发下去,设备直接变砖。从那以后,我强制要求所有升级包必须校验硬件ID。

核心原则:升级包制作,本质上是「最小必要信息」原则。只发设备需要的东西,别发多余的。

3.2 签名与加密:防的不是黑客,是误操作

说到安全,很多人第一反应是防黑客。但说实话,在医疗设备领域,更大的风险来自内部——比如测试人员拿错了固件版本,或者运维人员上传了未签名的包。

我建议的流程是这样的:

  1. 哈希计算:对固件数据做SHA256,得到一个摘要。
  2. 签名:用私钥对摘要做RSA或ECDSA签名。私钥必须离线保存,不能放在构建服务器上。
  3. 加密(可选):如果固件包含敏感算法或患者数据,用AES-256加密数据体。密钥可以派生自设备唯一ID。

设备端收到包后,先解密(如果有),再验签。验签通过,才允许写入Flash。

我的经验:签名算法用ECDSA,比RSA快,而且签名短。对于资源受限的MCU,ECDSA的验签时间能控制在100ms以内。RSA的话,搞不好要1秒以上。

嗯,这里要注意一点:签名和加密是两回事。签名保证「谁发的」,加密保证「别人看不懂」。我见过有人只加密不签名,结果设备被重放攻击——攻击者把旧版本的加密固件重新发一遍,设备就降级了。降级到有漏洞的版本,后果不堪设想。

避坑指南:我曾经接手过一个项目,他们用AES加密固件,但密钥硬编码在代码里。结果固件被反编译,密钥直接暴露。正确的做法是:密钥从设备唯一ID派生,或者用安全元件(SE)存储。

3.3 差分升级算法(bsdiff):让升级包瘦身90%

差分升级,说白了就是只发「变化的部分」。比如固件从v1.0升级到v1.1,可能只改了3个函数。你没必要把整个1MB的固件重新发一遍。

目前业界最成熟的是bsdiff算法。它的原理我简单说一下:

  • 把新旧固件做对比,找出差异部分。
  • 生成一个补丁文件,包含「删除、插入、替换」三种操作。
  • 设备端用旧固件 + 补丁,还原出新固件。

bsdiff的压缩率非常惊人。我做过一个实际案例:一个512KB的固件,从v2.3升级到v2.4,只改了20KB的代码。bsdiff生成的补丁只有35KB。相比发整个固件,节省了93%的流量。

但bsdiff有个缺点:计算量大。生成补丁时,需要在PC或服务器上跑,这没问题。但设备端还原补丁时,需要内存来存放新旧固件。对于只有64KB RAM的MCU,这很吃力。

解决方案:对于资源受限的设备,可以用xdelta或者HDiffPatch。它们的内存占用更低,但压缩率稍差。我一般这样选:

算法 压缩率 内存占用 适用场景
bsdiff 极高 高(需2倍固件大小) 服务器、网关、高端设备
xdelta 中等 低(可流式处理) MCU、低端设备
HDiffPatch 中等 通用场景

我个人习惯是:能用bsdiff就用bsdiff。如果设备内存不够,我会在设备端用「分块还原」——把旧固件分成256KB一块,逐块还原,逐块写入Flash。这样内存占用就降下来了。

3.4 断点续传机制:别让用户重头再来

医疗设备升级,最怕什么?最怕升级到一半,网络断了、电池没电了、用户把设备关机了。如果这时候让用户重头再来,体验极差,而且可能造成设备状态不一致。

断点续传的核心思路就一句话:记录进度,支持重入

具体做法:

  1. 分块下载:把升级包分成固定大小的块(比如4KB一块)。每下载完一块,就把块序号写入Flash的「进度记录区」。
  2. 校验每块:每块下载完成后,立即计算CRC32或SHA256,和包里的校验值对比。不对就重传这一块。
  3. 断点恢复:下次启动时,先检查进度记录区。如果发现已经下载了50块,就从第51块开始继续下载。

我的经验:进度记录区要放在独立的Flash扇区,和固件存储区分开。而且每次写入前,先擦除整个扇区,再写入。这样可以避免「写一半断电导致记录损坏」的问题。

还有一个容易被忽略的点:升级包的完整性校验要在所有块下载完成后做。不能只校验单个块,因为攻击者可能把块顺序调换。我建议的做法是:

  • 下载完成后,把所有块的哈希值拼接起来,再做一次整体哈希。
  • 和升级包元数据里的整体哈希对比。
  • 通过后,再验签。

我曾经遇到过一个案例:设备断点续传功能做好了,但测试发现,如果断点在「擦除Flash扇区」这一步,设备就变砖了。为什么?因为擦除扇区时,进度记录区也被擦掉了。修复方法很简单:先擦除固件区,再擦除进度记录区。顺序不能反。

避坑指南:断点续传不是万能的。如果设备在「写入Bootloader」这一步断电,那基本没救了。所以,我强烈建议:升级过程中,绝对不要更新Bootloader。Bootloader的更新,单独走一个流程,而且要有独立的回滚机制。

好了,这四个环节讲完了。总结一下:

  • 升级包制作:元数据 + 数据体 + 校验,缺一不可。
  • 签名与加密:签名防篡改,加密防泄露。私钥离线保存。
  • 差分升级:bsdiff是首选,内存不够就分块处理。
  • 断点续传:分块下载 + 进度记录 + 整体校验。注意擦除顺序。

下一章,我会讲升级过程中的「双备份机制」和「回滚策略」。这两个是保命用的,千万别错过。