STM32 提供了安全固件更新的参考设计。为了理解STM32 安全固件更新,我们可以先去看一看普通的固件更新式如何设计,包括一般流程、端到端之间的传输以及为了支持传输的数据结构,在MCU 中的存储以及支持存储的数据结构。在对固件更新的一般原理有了基本认知后,我们看下安全固件更新需要额外引入哪些改变,以及目前STM32 如何在安全固件更新里实现这些额外的需求。一定要谨记,STM32 安全固件更新离不开STM32 安全启动这个平台安全做基础。
固件更新的总体设计
固件更新的流程
一般情况下,固件更新由近距离两方参与:
◎ 设备端:固件的接受方STM32 MCU
◎ 服务器:固件的提供方比如PC
例如:开发人员可以用ST-Link,直接将新的固件通过JTAG 烧入到STM32 MCU。这个更新的过程可以是:
◎ 一次性擦除整个Flash 并写入新固件
◎ 根据STM32 MCU 的规范,使用ST-Link 擦除一部分Sectors 或者Pages,然后对这些部分区块进行更新。
这种有两方参与的固件更新的通讯方式,并不限于ST-Link JTAG 通讯。它还可以通过UART。这个时候需要将STM32 MCU 切换至System memory bootloader 启动模式。或者,用户使用自己的启动加载器。
更复杂的情形,尽管也是两方参与,但是他们通过其他通讯协议,比如以太网通讯。不过, 这种复杂性,只是体现在底层协议栈上。对于固件本身来说,还是不需要做任何改变。例如,用户可以开发bootloader 充当服务器,允许其它网络用户连上来,发送指令,发送固件,然后来更新系统。
在IoT 时代,需要一对多的更新,那么参与者一般是三个。可以参考下图。
◎ 固件的提供方:一般是管理员
◎ 固件的存储方与管理方:一般是IoT 平台
◎ 设备端,也就是固件的接受者:STM32 MCU
一对多情况下,管理员将固件上传至IoT 平台,触发所有设备的更新操作。而IoT 平台则根据各个STM32 设备的具体情况下进行固件更新的消息通知 ,以及固件内容推送。IoT 平台也可以被动接受请求。在这种情况下,固件更新的操作不一定是同步的,也就是说固件的更新可能要花费几天,甚至好几个月。考虑到有些设备的离线,若需要等待全部设备的版本升级完成,我们可能需要等待的时间更长。
固件更新的网络拓扑结构的改变,不改变传输的固件更新的内容组织。传输的内容,依然可以是:
◎ 从头开始完整的传递一个固件
◎ 只更新一部分
固件传输
固件更新的设计,在通讯上需要考虑服务器与设备之间的距离的影响。是否需要远距离更新,例如选择什么样的通讯接口;是否不依赖通讯的可靠性---例如是否需要使用断点续传以及校验和;以及是否假定通讯带宽总是很低,例如是否需要差分更新以及是否需要压缩。
距离:本地更新与远程更新
◎ 本地更新:一般是通过JTAG 或者UART,近距离连接设备,所进行的本地更新。
◎ 远程更新:FOTA,这个名字,是指远程更新。远程更新是个非常重要的功能。想象一下遍布在地球上各个角落的IoT 设备,如果没有远程更新,那么这些设备很快就会功能过时或者安全无法得到保证。考虑远距离更新,仅仅在安全启动里支持下载是不够的 ,需要用户固件支持下载功能 。
可靠:断点续传与校验和
◎ 断点续传:简单的固件更新传输,可以要求要么全部下载成功,要么从头开始。然而,在不稳定的网络情况下,会出现多次尝试都失败的情况。因此,在不稳定的网络情况下,一般考虑保存固件下载的状态。当网络连接断开又恢复时,下载不需要从头开始,而只需要从网络断开处继续。这样可以减少重复的数据下载,从而减少对网络带宽的要求。
◎ CRC 校验和:简单的固件更新设计,可以无需在应用层加入校验和。下载一个固件包,收到的固件包的大小符合预先通知的大小,则下载完整。然后,系统就可以切换到新固件运行。然而,存在一种网络发生错误的可能性,在这种情况下,大小相同,而内容却不可用。所以,在无法确定通讯一定可靠的情况下,应该对整个固件加入CRC 校验字段,而不仅判断固件包的带下 。CRC 校验和可以检测是否在通讯过程中发生了错误。
带宽:差分更新与压缩
◎ 是否差分更新:简单的固件更新设计,可以使用完全更新,不需要理会版本之间的差异大小,一次性传输完整的固件。差分更新则是建立在新版本和旧版本的差异上,只传输差异部分,由设备端恢复出完整需要更新的固件。一般情况下,邻近两个版本不可能差别太大,这个时候采用差分更新,只需要传输较小的差异部分。差分更新可以节省网络带宽,也可以降低设备的功耗。
◎ 是否压缩
有些时候为了节省传输的带宽,会对固件进行压缩。但是压缩 可能会带来安全弱点。这取决于我们压缩的内容是否会自动包括一些敏感信息,同时是否有接口允许黑客选择要发送的内容。压缩后的大小会构成提示,例如黑客所选择的内容是否与自动包含的敏感信息相同。
传输中的数据结构:针对固件更新的传输过程,需要设计相应的数据结构,对固件进行打包。
◎ 无额外数据结构:一种最简单的固件更新是不需要添加任何额外的数据结构。在通讯链路上的实际应用负载就是固件本身。当STM32 完整的收到该固件,直接将固定位置的程序代码擦除,然后将新的固件写到该位置。
◎ 带有元数据:一般推荐传输的固件加入元数据信息。这些元数据信息可以包括:
• 固件名称
• 固件大小
• 固件版本
• 固件校验和
• 固件在Flash 里的起地址
• 本次传输起地址
• 本次传输结束地址
• 元数据校验和
为了断点续传或者差分更新,是需要标记所传输的数据,在固件整体中的位置;而校验和,则需要包括元数据以及实际固件。这样可以检测固件头部和实际固件是否在传输过程中是否都没有损坏。
在实际开发固件更新时,元数据可以细分为固件整体的元数据以及每次传输的元数据。
固件存储
◎ 位置:MCU 所运行的固件总是可以存储在MCU 内部,或者使用外扩Flash。不考虑安全的情况
下,这两者都没有什么不妥。如果考虑到安全,前面我们已经分析了外扩Flash 会带来很大的风险。
◎ Bootloader
- 无bootloader:无启动加载器则需要利用STM32 的双bank 特性。不少STM32 型号支持双Bank 功能,也就是Flash 可以分成两个Bank,Bank A 和Bank B。当系统从Bank A 启动时可以更新Bank B。
确保更新成功后,可以将系统设置成从Bank B 启动。使用双Bank 启动的固件更新功能,整个用户固件的代码都可以被更新。这是相对于 bootloader 更新的优点。值得一提的是,在使用双Bank 的启动功能时,要确保固件更新成功。即使不考虑受攻击的情况下,也要考虑通讯过程可能带来的破坏。如果一个错误的固件被更新到 Flash Bank 里,同时系统又设置成从该Bank 启动。那么这个启动过程是无法知道这个固件是错误还是正确,只是去加载代码并执行。当然这种情况下,还是可以使用ST-Link 或者 System memory loader 重新烧入正确的固件。
根据STM32 手册,双Bank 启动功能依赖于System memory loader。那么,若使用该功能则RDP 不能设置成2。
- 有Bootloader:有Bootloader 的简单固件更新,很好理解,就是由bootloader 去接收新的固件,然后由bootloader 将该固件写入固定的位置。一般推荐固件更新使用bootloader。
固件冗余:
固件冗余一直是个现实的问题。不存在固件冗余的话,也就是单镜像的优点是:① 更多用户空间;② 适合更小的Flash 要求;缺点是,代码从Flash 里运行时没有办法更新自己的,不支持用户更新。
存在固件冗余,双镜像的优点是:① 可以回退,更安全;② 用户固件也可以更新。缺点是,需要更多的空间。
Bootloader 和固件冗余并不冲突,我们可以同时拥有 Bootloader 和固件冗余。STM32 SBSFU 提供了bootloader+ 是否固件冗余的选择,包括bootloader +单镜像和 bootloader + 双镜像,供用户选择。
整体存储结构
固件的存储位置不影响固件更新的存储的逻辑数据结构。然而是否冗余则意味着我们在设计时是否需要预留足够的空间。如果是bootloader 加固件冗余,则Flash 会被划分成几个部分:
◎ 启动加载器
◎ 启动数据区
◎ 固件区
◎ 备份固件区
启动加载器根据启动数据区决定从哪个固件引导系统。启动数据区是需要重点保护的区域。
相关文章