3、固件升级核心流程:升级包制作、签名与加密、差分升级算法(bsdiff)、断点续传机制
好,咱们直接进入正题。固件升级这事儿,看着简单,做起来全是坑。我这些年经手的医疗设备,小到血糖仪,大到CT机,升级流程出过的事故,够我写一本《血泪史》了。
今天我把核心流程拆成四个环节来讲:升级包怎么做、怎么保证它没被篡改、怎么让升级包尽量小、以及万一断了怎么办。这四个环节,一个都不能少。
3.1 升级包制作:别把整个固件扔过去
很多人一开始的想法很直接:把新固件整个打包,发给设备,完事。但你想想看,一个嵌入式设备,Flash可能就几兆,网络带宽也有限。你扔一个几十兆的包过去,设备直接卡死。
我个人的习惯是,升级包要包含三个部分:
- 元数据头:版本号、硬件平台ID、校验算法标识、包大小、签名信息。这部分是明文,方便设备快速判断要不要升级。
- 固件数据体:可以是完整镜像,也可以是差分补丁。后面会细讲。
- 尾部校验:通常是SHA256哈希值,加上数字签名。
这里有个小细节。元数据头里一定要放一个硬件平台ID。我在项目中遇到过,同一款设备的不同批次,MCU型号换了,固件不兼容。结果升级包发下去,设备直接变砖。从那以后,我强制要求所有升级包必须校验硬件ID。
核心原则:升级包制作,本质上是「最小必要信息」原则。只发设备需要的东西,别发多余的。
3.2 签名与加密:防的不是黑客,是误操作
说到安全,很多人第一反应是防黑客。但说实话,在医疗设备领域,更大的风险来自内部——比如测试人员拿错了固件版本,或者运维人员上传了未签名的包。
我建议的流程是这样的:
- 哈希计算:对固件数据做SHA256,得到一个摘要。
- 签名:用私钥对摘要做RSA或ECDSA签名。私钥必须离线保存,不能放在构建服务器上。
- 加密(可选):如果固件包含敏感算法或患者数据,用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 断点续传机制:别让用户重头再来
医疗设备升级,最怕什么?最怕升级到一半,网络断了、电池没电了、用户把设备关机了。如果这时候让用户重头再来,体验极差,而且可能造成设备状态不一致。
断点续传的核心思路就一句话:记录进度,支持重入。
具体做法:
- 分块下载:把升级包分成固定大小的块(比如4KB一块)。每下载完一块,就把块序号写入Flash的「进度记录区」。
- 校验每块:每块下载完成后,立即计算CRC32或SHA256,和包里的校验值对比。不对就重传这一块。
- 断点恢复:下次启动时,先检查进度记录区。如果发现已经下载了50块,就从第51块开始继续下载。
我的经验:进度记录区要放在独立的Flash扇区,和固件存储区分开。而且每次写入前,先擦除整个扇区,再写入。这样可以避免「写一半断电导致记录损坏」的问题。
还有一个容易被忽略的点:升级包的完整性校验要在所有块下载完成后做。不能只校验单个块,因为攻击者可能把块顺序调换。我建议的做法是:
- 下载完成后,把所有块的哈希值拼接起来,再做一次整体哈希。
- 和升级包元数据里的整体哈希对比。
- 通过后,再验签。
我曾经遇到过一个案例:设备断点续传功能做好了,但测试发现,如果断点在「擦除Flash扇区」这一步,设备就变砖了。为什么?因为擦除扇区时,进度记录区也被擦掉了。修复方法很简单:先擦除固件区,再擦除进度记录区。顺序不能反。
避坑指南:断点续传不是万能的。如果设备在「写入Bootloader」这一步断电,那基本没救了。所以,我强烈建议:升级过程中,绝对不要更新Bootloader。Bootloader的更新,单独走一个流程,而且要有独立的回滚机制。
好了,这四个环节讲完了。总结一下:
- 升级包制作:元数据 + 数据体 + 校验,缺一不可。
- 签名与加密:签名防篡改,加密防泄露。私钥离线保存。
- 差分升级:bsdiff是首选,内存不够就分块处理。
- 断点续传:分块下载 + 进度记录 + 整体校验。注意擦除顺序。
下一章,我会讲升级过程中的「双备份机制」和「回滚策略」。这两个是保命用的,千万别错过。