Nordic Secure DFU拥有签名(Signature)机制,制作升级包时会生成Init Packet,它记录了待升级文件的元信息(Meta)及其数字签名。
在执行Secure DFU过程中,主机程序先发送Init Packet,签名验证通过后再发送Firmware Image,以保证固件的合法性。
1. 基本信息
使用pc-nrfutil生成升级压缩包的命令为:
nrfutil pkg generate --application app.hex --application-version 0x01 --hw-version 52 --sd-req 0xB6 --key-file private_key.pem dfu_pkg.zip
解压dfu_pkg.zip,可以看到三个文件:
- app.bin
- app.dat
- manifest.json
manifest.json是清单文件,记录了dfu_pkg.zip中包含哪些文件。 app.bin是app.hex去掉地址信息后的二进制文件。 app.dat就是Init Packet的二进制形态。
从infocenter上得知,Init Packet中包含以下内容:
- Image type & size & hash (APP, BTL, SD or combination)
- Version of FW/HW
- sd_req
- Signature type & bytes
通过pc-nrfutil命令可以查看这些信息内容:
nrfutil pkg display dfu_pkg.zip
内容截取如下:
$ nrfutil pkg display dfu_pkg.zip
DFU Package: <dfu_pkg.zip>:
|
|- Image count: 1
|
|- Image #0:
|- Type: application
|- Image file: app.bin
|- Init packet file: app.dat
|
|- op_code: INIT
|- signature_type: ECDSA_P256_SHA256
|- signature (little-endian): 1e6011c9f3787641d920bd0d9f7c41b42f9c05fd9673552c626921f99512f616169533bd9d9c0d4d00a4ba626890d7ab4efbf1d8962afe7bafb96c89dd186fc5
|
|- fw_version: 0x00000001 (1)
|- hw_version 0x00000034 (52)
|- sd_req: 0xB6
|- type: APPLICATION
|- sd_size: 0
|- bl_size: 0
|- app_size: 60548
|
|- hash_type: SHA256
|- hash (little-endian): 2a23069c597cd72a15c2a0a24a1a09bbdbbbcab7fa1b68de119a871436d84cc2
|
|- boot_validation_type: ['VALIDATE_GENERATED_CRC']
|- boot_validation_signature (little-endian): ['']
|
|- is_debug: False
其中,Hash是对app.bin文件执行的hash结果。
下载openssl命令行工具,在cmd中执行openssl dgst -sha256 app.bin
,可获得app.bin文件的hash值。注意,openssl工具获得的hash是big-endian,而nrfutil显示的hash是little-endia,二者数据顺序相反,数据内容相同。
Signature不是根据app.bin文件生成的签名!对于相同的app.hex,每次执行命令生成的签名都不相同。
wikimedia.org的这张图描绘了数字签名校验过程:

数字签名验证过程需要两个步骤:
- 使用原始数据文件的Hash值和私钥,生成数字签名。
- 对方收到数据明文后,计算Hash值(hash-1)。根据数字签名和公钥生成Hash值(hash-2)。比较二者,如果相等则签名验证通过。
Init Packet将固件镜像的元信息和Hash值放在一起形成结构体,取名为Init Command,再将Init Command序列化(Serialize)成二进制数据,Signature是根据Init Command的二进制数据内容生成的数字签名。
(我希望能够通过openssl模拟生成Signature,然后再对Hash进行验证,但是一直未能操作成功。)
2. 二进制文件
将结构化的数据序列化,Nordic Secure DFU选择了Protocol Buffer的嵌入式方案nano-pb。在许多场景下,Protocol Buffer简写为ProtoBuf或pb。
Protocol Buffer解决了不同平台序列化数据的互通性。比如pc-nrfutil是一个Python工具,它所生成的Init Packet要能够被C语言程序读取,那么二者应遵守相同的约定,包括数据类型定义、数据存储形式、复杂数据描述等。
Protocol Buffer定义了这样一套约定,以*.proto文件保存。不同平台的程序只要使用相同的Protocol Buffer文件,它们之间的序列化数据即可被正确解析。
Init Packet使用了dfu-cc.proto作为格式规范,pc-nrfutil的文件路径为:https://github.com/NordicSemiconductor/pc-nrfutil/blob/master/nordicsemi/dfu/dfu-cc.proto ,Bootloader所使用的dfu-cc.proto路径为:<sdk>\components\libraries\bootloader\dfu\dfu-cc.proto
,它们的内容基本一致。
打开dfu-cc.proto文件,定位到文件底部:
// Parent packet type
message Packet {
optional Command command = 1;
optional SignedCommand signed_command = 2;
}
它就是Init Packet的数据原型。
message Packet相当于C语言中的结构体,optional关键字表示该项可以不存在,数字1和2表示该成员在结构体中的位置序号,称为field_num。
Command和SignedCommand本身也是message,所以它们是嵌套结构体。在文件中,也可以找到它们的结构体定义。
在文件中找到Init Command:
// Commands data
message InitCommand {
optional uint32 fw_version = 1;
optional uint32 hw_version = 2;
repeated uint32 sd_req = 3 [packed = true];
optional FwType type = 4;
optional uint32 sd_size = 5;
optional uint32 bl_size = 6;
optional uint32 app_size = 7;
optional Hash hash = 8;
optional bool is_debug = 9 [default = false];
repeated BootValidation boot_validation = 10;
}
它就是上面提到的生成Signature用的Init Command。它也是Init Packet的核心内容。
现在我们知道了Init Packet其实就是dfu-cc.proto文件中的变量所包含的信息。
那么它是如何变成app.dat的呢?
Protocol Buffer定义message的构成形式为:<field_num><wire_type>[length]<data_content>
- field_num是结构体成员序号
- wire_type是个枚举值,它仅有几种选项,在下面给出
- length为可选值,如果数据长度不可预先确定,则它存在,否则不存在
- data_content为数据内容,它使用”Base 128 Varints”数据表示法
根据Protocol Buffer官方文档,可选的wire_type为:
Type | Meaning | Used for |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
wire_type仅有6种,可以用3个比特表示。规范中,<field_num>占5 bit,<wire_type>占3 bit,二者共同占用1字节。
Base 128 Varints实现了数据自身携带数据长度信息,如果一个字节的首位比特(msb)为1,表示还有后续字节,如果msb=0,表示这是最后一个字节。比如 0xCC33: 1100 1100 0011 0011,由于第一字节的msb=1,第二字节msb=0,说明它是个双字节数。
计算数据实际值的算法为:(1)去掉各字节的msb,(2)逆序全部字节,(3)合并。那么0xCC33的实际值为:
- 去msb:100 1100 011 0011
- 逆序:011 0011 100 1100
- 合并:0001 1001 1100 1100 = 0x19CC
更详细的规则请阅读官方文档:https://developers.google.com/protocol-buffers/docs/encoding
下面开始分析app.dat的文件内容。
使用十六进制工具打开app.dat。我将其十六进制内容复制到Excel表中方便查看,如下:

[1A] 12 指示signed_command,field_num=2,wire_type=2。
[1B – 1C] 8A 01指示Length=138。138正好是D1-E18之间的数据总数。 由于signed_command本身是个message,后面的数据是它的内容。
[3H] 38 指示app_size。
[4A – 4C] 84 D9 03 指示app_size=60548,根据“Base 128 Varints”算法可以获得该结果。
图中绿色表示数组型数据,蓝色表示长度,橙色表示<field_num>+<wire_type>。
第一个绿色色块[5B – 9A] 指示Hash值,第二个绿色色块[10F – 18E] 指示签名。
使用Protocol Buffer编译器protoc.exe可以将dfu-cc.proto编译成各种平台适用的结构体定义。
对于Nordic SDK的dfu-cc.proto,编译它可以生成dfu-cc.pb.c和dfu-cc.pb.h,后者实现了相同的结构体,但是遵守C语言的规范。 比如dfu-cc.proto文件中的message Packet{}编译后在dfu-cc.pb.h中变成如下形式:
typedef struct {
bool has_command;
dfu_command_t command;
bool has_signed_command;
dfu_signed_command_t signed_command;
/* @@protoc_insertion_point(struct:dfu_packet_t) */
} dfu_packet_t;
3. 解析过程
执行Secure DFU时候,主机先发送Init Packet,再发送固件镜像。
发送过程如下图:

主机依次执行:
- Select Request
- Create Request
- Write Request
- Get CRC Requset
- Execute Request
打开secure_bootloader工程,找到nrf_dfu_req_handler.c,会看到以下函数:
- on_cmd_obj_select_request
- on_cmd_obj_create_request
- on_cmd_obj_write_request
- on_cmd_obj_crc_request
- on_cmd_obj_execute_request
它们与上述Request对应,当主机向从机发出Request,从机程序则进入相应的函数中执行。
Init Packet的数据内容在Write Request中发送,如果使用了长包(MTU=247),那么一次即可发送完毕。
执行on_cmd_obj_execute_request时,追踪代码可以找到stored_init_cmd_decode函数,该函数使用pb_decode将二进制数据序列解析成proto文件定义的形式,并保存在mp_init结构体中。
mp_init的结构体定义在dfu-cc.pb.c/h中,它正是上面提到的利用dfu-cc.proto编译生成的C语言代码文件。
参考链接
Protocol Buffer中文介绍: http://bigdata.51cto.com/art/201805/574782.htm
Base 128 Varints中文介绍:https://www.jianshu.com/p/814a5dd86561
(完)
讲的非常好,也很详细,
多谢
好详细~赞
一年过去了,终于一知半解了
赞