4. Secure DFU与签名验证

Legacy DFU 只校验固件包的 CRC,任何拿到 .zip 升级包的人都能改包重签,因此它只能防传输错误,不能防恶意固件。Secure DFU 在 Nordic nRF5 SDK 中引入了一套基于 ECDSA(椭圆曲线数字签名)的信任体系:升级包由私钥签名,设备端只保存公钥,Bootloader 在写入前用公钥验证签名,验证不通过就拒绝升级。本节围绕这条信任链,讲清密钥怎么生成、固件怎么签、公钥怎么进设备、启动时怎么验、以及如何防止把设备刷回旧的有漏洞版本。

一、信任链的整体结构

Secure DFU 的信任根是开发者自己生成的一对 ECDSA 密钥。私钥留在开发/发布环境,用于对固件镜像签名;公钥被编译进 Bootloader,随 Bootloader 一起烧录到设备。设备每次收到升级包,Bootloader 用内置公钥验证包内固件的签名,通过后才写入 Flash 并更新启动信息。这样即使攻击者截获并篡改升级包,也无法伪造出能被公钥验证通过的签名。

Secure DFU 信任链与启动验证 开发/发布环境 私钥 priv.pem(严格保密) nrfutil keys generate 公钥 dfu_public_key.c nrfutil keys display --key pk 固件签名 app.hex + init packet nrfutil pkg generate dfu_package.zip 含签名 + 版本号 nRF 设备 Bootloader(含公钥) SoftDevice + Bootloader Application 区 签名验证通过后写入 签名 分发 上电启动验证链(Bootloader 依次执行) ① 读 Bootloader 校验自身 CRC ② 读 SoftDevice 校验完整性 ③ 读 Application 校验签名 ④ 版本/回滚检查 版本 < 当前则拒绝 任一步失败 → 停留在 Bootloader,不跳转 Application;全部通过 → 跳转执行新固件

二、密钥生成与公钥烧录

Nordic 提供 nrfutil 工具完成密钥与签名操作。生成私钥的命令如下,它会输出一个 PEM 格式的私钥文件:

nrfutil keys generate priv.pem
nrfutil keys display --key pk --format code priv.pem --out_file dfu_public_key.c

第一条命令生成 ECDSA 私钥(默认 secp256r1 曲线),第二条把对应的公钥导出为 C 源码文件 dfu_public_key.c。这个文件必须替换掉 SDK 中 components/libraries/bootloader/dfu/ 目录下的同名文件,然后重新编译 Bootloader。也就是说,公钥不是运行时写入的,而是编译期固化进 Bootloader 镜像的。烧录时用 nrfjprog 或 nRF Connect Programmer 把 SoftDevice、Bootloader、Bootloader Settings 页一起写入,公钥随 Bootloader 一起生效。

私钥一旦泄露,任何人都能签出被设备信任的固件。私钥文件不要提交到 Git,不要放在 CI 的公共缓存里,建议用独立的签名机或密钥管理服务保存。如果私钥丢失,只能重新生成密钥对并重新烧录 Bootloader,已出厂设备无法远程更换公钥。

三、固件签名与升级包生成

签名发生在打包阶段。nrfutil 会对固件镜像计算哈希,再用私钥对哈希签名,把签名、公钥标识、版本号、固件类型等信息一起写进升级包。典型命令:

nrfutil pkg generate \
  --hw-version 52 \
  --sd-req 0xCA \
  --application-version 3 \
  --application app.hex \
  --key-file priv.pem \
  app_dfu_package.zip

其中 --application-version 3 是本次固件的版本号,它直接参与防回滚判断;--sd-req 指定所依赖的 SoftDevice 版本,Bootloader 会检查设备当前 SoftDevice 是否匹配;--hw-version 用于硬件版本匹配。生成的 zip 包就是通过 BLE 或串口下发给设备的升级包。设备端 Bootloader 收到后,先解包读取 init packet,用内置公钥验证签名,再决定是否写入。

四、防回滚策略

防回滚的核心是版本号比较。Bootloader 在 Bootloader Settings 页中保存了当前已激活固件的版本号。当新包版本号小于或等于当前版本时,Bootloader 会拒绝升级,从而阻止攻击者把设备降级到存在已知漏洞的旧版本。版本号由 --application-version 指定,必须是递增的整数。

需要注意两点:第一,防回滚只比较版本号,不比较内容,所以同一版本号重复刷入也会被拒绝;第二,如果确实需要降级(例如新版本有严重问题需要回退),必须通过调试接口擦除设备并重新烧录,不能走 DFU 通道。此外,Bootloader 自身也有版本,Bootloader 升级同样受签名和版本约束。

检查项数据来源不通过的结果
固件签名init packet 中的签名 + Bootloader 内置公钥拒绝写入,返回签名错误
硬件版本init packet 的 hw-version 与设备 FICR拒绝升级
SoftDevice 依赖init packet 的 sd-req 与当前 SD 版本拒绝升级
版本回滚新包版本号与 Settings 页记录版本号版本不递增则拒绝

五、实操中的常见问题

实际调试时,最常见的失败是“签名验证不通过”。原因通常是:公钥文件没有替换、替换后没有重新编译烧录 Bootloader、或者签名时用错了私钥。排查方法是确认设备里 Bootloader 的公钥与签名私钥是同一对。另一个常见问题是升级后设备停在 Bootloader 不跳转,这往往是因为版本号没有递增,被防回滚逻辑拦下。此时用 nRF Connect 连接设备,读取 DFU 服务状态或查看 Bootloader 日志即可确认。

建议在发布流程中固定三步:先生成密钥对并妥善保存私钥;每次发版用同一私钥签名并递增版本号;把公钥文件纳入 Bootloader 构建产物做版本管理。这样既能保证升级包可验证,也能保证版本可追溯。

总结来说,Secure DFU 的安全性来自“私钥签名、公钥验证、版本递增”三件事的配合。密钥生成和公钥烧录决定了谁能签出合法固件,签名和验证决定了固件在传输和写入过程中是否被篡改,防回滚则决定了设备能否被降级到不安全版本。三者缺一,信任链就不完整。