FIT(2):基于FIT对镜像/配置进行签名和uboot验签启动
关键词:hash、sha1、sha256、md5、crc32、rsa、pkcs-1.5、signature等等。
接前文《FIT(1):基于FIT的镜像创建和解析/启动》,重点梳理签名/验签流程和hash校验流程。
mkimage对镜像或者配置进行签名并生成FIT镜像,uboot执行相反的过程解析FIT镜像并验签。
1 基于FIT的签名流程和手动验签
关于签名和验签更多参考《doc/uImage.FIT/signature.txt》。
1.1 创建RSA2048私钥及其X509证书
通过openssl创建RSA2048的私钥:
openssl genpkey -algorithm RSA -out dev.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
或者使用如下命令创建:
openssl genrsa -out dev.key 2048
然后创建RSA2048私钥的X509证书:
openssl req -batch -new -x509 -key dev.key -out dev.crt
1.2 编写支持签名功能的its
关于支持对configuration签名的its,可以参考《doc/uImage.FIT/sign-configs.its》;对镜像签名的its,可以参考《doc/uImage.FIT/sign-images.its》。
编写multi.its增加对每个子镜像和configuration的签名:
/* * Simple U-Boot uImage source file containing a single kernel and FDT blob */ /dts-v1/; / { description = "Simple image with single Linux kernel and FDT blob"; #address-cells = <1>; images { kernel { description = "Linux kernel"; data = /incbin/("zImage"); type = "kernel"; arch = "arm"; os = "linux"; compression = "none"; load = <0x830000e8>;--通过fit_image_get_load()获取到地址,存到image.os.load中。 entry = <0x830000e8>;--通过fit_image_get_entry()获取到地址,存到image.ep中。 hash {--对子镜像进行哈希。 algo = "crc32";--哈希算法。 }; signature {--单独对子镜像进行验签时需要;如果对configuration进行签名不需要此部分。 algo = "sha256,rsa2048";--签名所使用的哈希和rsa算法。 key-name-hint = "dev";--签名所使用的带x509证书的私钥。 }; }; kernel-fdt { description = "FDT blob"; data = /incbin/("nand.dtb"); type = "flat_dt"; arch = "arm"; compression = "none"; load = <0x86F00000>; hash { algo = "crc32"; }; signature { algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk { description = "Linux ramdisk"; data = /incbin/("rootfs.cpio.gz"); type = "ramdisk"; arch = "arm"; os = "linux"; compression = "none"; hash { algo = "crc32"; }; signature { algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk-fdt { description = "ramdisk FDT blob"; data = /incbin/("ramdisk.dtb"); type = "flat_dt"; arch = "arm"; compression = "none"; load = <0x86F00000>; hash { algo = "crc32"; }; signature { algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; }; configurations { default = "kernel"; kernel { description = "Boot Linux kernel with FDT blob"; kernel = "kernel"; fdt = "kernel-fdt"; signature {--对当前configuration进行签名。涉及到的子镜像需要提供hash值,即子镜像节点需要增加hash子节点。 algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk { description = "Boot Linux kernel with FDT blob and ramdisk"; kernel = "kernel"; ramdisk = "ramdisk"; fdt = "ramdisk-fdt"; signature { algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; }; };
如果单独对子镜像进行签名,有可能被kernel+dtb交叉组合欺骗验签,或者签名kernel+未签名dtb等。
对configuration进行签名是一种好的选择。
本质上itb文件是dtb类型的,通过dtc反编译得到:
/dts-v1/; / { timestamp = <...>;--生成镜像的时间戳。 description = "Simple image with single Linux kernel and FDT blob"; #address-cells = <0x01>; images { kernel { description = "Linux kernel"; data = <...>;--通过incbin嵌入的数据。 type = "kernel"; arch = "arm"; os = "linux"; compression = "none"; load = <0x84000000>; entry = <0x84000000>; hash { value = <0x988ee278>;--哈希结果。 algo = "crc32";--哈希算法,使用此算法对数据进行运算得到结果和上面value的内容对比。 }; signature { timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage";--通过mkimage工具进行签名。 value = <...>;--签名结果。 algo = "sha256,rsa2048";--签名所使用到的算法组合。 key-name-hint = "dev";--指向名称为dev的公钥。 }; }; kernel-fdt { description = "FDT blob"; data = [...]; type = "flat_dt"; arch = "arm"; compression = "none"; load = <0x86f00000>; hash { value = <0xba237511>; algo = "crc32"; }; signature { timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage"; value = <...>; algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk { description = "Linux ramdisk"; data = [...]; type = "ramdisk"; arch = "arm"; os = "linux"; compression = "none"; hash { value = <0x19670d2>; algo = "crc32"; }; signature { timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage"; value = <...>; algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk-fdt { description = "ramdisk FDT blob"; data = [...]; type = "flat_dt"; arch = "arm"; compression = "none"; load = <0x86f00000>; hash { value = <0x1611959f>; algo = "crc32"; }; signature { timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage"; value = <...>; algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; }; configurations { default = "kernel"; kernel { description = "Boot Linux kernel with FDT blob"; kernel = "kernel"; fdt = "kernel-fdt"; signature {--更多参考《Flattened Image Tree (FIT) Format Image-signature nodes》。 hashed-strings = <0x00 0x9d>;--取多少字节哈希结果作为签名一部分。 hashed-nodes = "/\0/configurations/kernel\0/images/kernel\0/images/kernel/hash\0/images/kernel-fdt\0/images/kernel-fdt/hash";--被哈希的镜像节点。 timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage"; value = <...>;--签名。 algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; ramdisk { description = "Boot Linux kernel with FDT blob and ramdisk"; kernel = "kernel"; ramdisk = "ramdisk"; fdt = "ramdisk-fdt"; signature { hashed-strings = <0x00 0xb9>; hashed-nodes = "/\0/configurations/ramdisk\0/images/kernel\0/images/kernel/hash\0/images/ramdisk-fdt\0/images/ramdisk-fdt/hash"; timestamp = <...>; signer-version = "2021.01+dfsg-3ubuntu0~20.04.6"; signer-name = "mkimage"; value = <...>; algo = "sha256,rsa2048"; key-name-hint = "dev"; }; }; }; };
1.2.1 关于kernel的load和entry地址
在bootm_load_os中,对load进行了检查:
bootm_load_os
->image_decomp--如果需要uboot解压则根据压缩算法进行解压。
->根据os.start/os.end/os.image_start/os.image_len/os.load计算是否存在overlap。
了解bootm_load_os中的不同地址前,先了解struct image_info内容:
typedef struct image_info { ulong start, end;--此处表示整个FIT镜像的起始和结束地址。 ulong image_start, image_len;--kernel镜像的起始地址和大小。 ulong load;--通过fit_image_get_load()从load属性获取值。 uint8_t comp, type, os; /* compression, type of image, os type */ uint8_t arch; /* CPU architecture */ } image_info_t;
所这对load的设置就有要求,如果load不等于os.image_start,就需要判断加载镜像范围[load, load + image_len]是否和原始FIT镜像范围[os.start, os.end]存在重叠。
关于kernel的load/entry地址设置,《Solved: Kernel load address when using FIT images》讲述了3中方式:
- 不需要解压的zImage,在FIT之外设置load地址:需要一次拷贝。
- 需要解压的Image.gz,直接解压到load地址:不需要拷贝,也不需要精确指定load地址。
- 不需要解压的zImage,精确设置load地址为os.image_start:这需要预先计算kernel镜像在FIT中的偏移,load地址=FIT基地址+kernel镜像偏移。
1.2.2 如何确定kernel load地址,避免搬移
对于使用zImaage不需要uboot解压的kernel镜像,如果不想多一次拷贝,那么如何精确指定load地址?
- 在boot_get_kernel()中通过debug打印FIT中kernel镜像的地址和大小:
debug(" kernel data at 0x%08lx, len = 0x%08lx (%ld)\n", *os_data, *os_len, *os_len);
- 通过hexedit打开itb和zImage文件,根据zImage二进制查找itb内容。匹配后即可找到zImage相对于FIT头的偏移地址。
重新编译itb文件。在its不变动的情况下,kernel的load地址不变。
1.3 生成签名itb文件
首先创建一个空白的pub-key.dts文件:
/dts-v1/;
/ {
model = "";
compatible = "";
};
然后编译dts生成pub-key.dtb文件:
dtc -I dts -O dtb pub-key.dts -o pub-key.dtb
对镜像签名并打包成FIT镜像的命令如下:
mkimage -k . -f multi.its kernel-fdt.itb -K pub-key.dtb -r
mkimage借助dtc从-f指定its中解析参数,读取镜像,使用-k指定目录下的带x509证书的私钥对镜像进行签名,并且将公钥内容写入-K指定的dtb中,-r表示如果验签不通过则中止启动流程。
1.4 提取RSA2048公钥
反编译pub-key.dtb,从中提取RSA2048验签所需要的signature特性:
dtc -I dtb -O dts pub-key.dtb -o for-uboot-builtin.dts
具体内容如下:
/dts-v1/; / { model = [00]; compatible = [00]; signature { key-dev {--私钥dev的公钥部分信息。 required = "conf";--有效值为image或conf。如果设置,则必须验签通过才会被认为有效。image则表示必须验证所有的镜像;conf表示必须验证所选的configuration,此时需要镜像提供hash值。 algo = "sha256,rsa2048";--签名验签算法。 rsa,r-squared = <0x6e8560d5 0xe9614a03 0x4d4586c8 0xa185956d 0xcf6bc6b 0x6d030ef0 0x85a4ebd9 0x20de7d3a 0xa11239c 0xcf5c3c13 0x52916d2c 0x423a3082 0x540d2eae 0xba9f28d0 0xa0e56b51 0x7b5eac9f 0x770fb9e0 0x65da3d3d 0x589869de 0xb5089999 0x765611da 0xe7bef4a6 0x7310b36c 0x841ff751 0xa3b1cf07 0x8e57a00c 0xc5190293 0xa794a15f 0x8e3e86c 0x90465fca 0x9a191b8 0xc62a94b2 0x7413c7ea 0xe8110e31 0x5f158df9 0x7cb11475 0xd1afb8a0 0xe38ee2cd 0x494d83f0 0x1c458d5 0x3d4d83c1 0x7462ce5a 0xad6bdd2d 0x5b47c4b0 0x7b06de30 0xe088eb9 0xef61a962 0x743a3ed6 0x81664e16 0x6bf59742 0x822d578 0x553c7ac3 0x59fcdec6 0x2f29aa8c 0x1115286b 0xe9c49c75 0x80632a51 0x994e42fd 0xcf0ed021 0x1ac4bff3 0x82e4fffe 0x62e89db1 0x3378285d 0x1da480d4>;
--(2^num-bits)^2 as a big-endian multi-word integer rsa,modulus = <0xdbc2dfbf 0xbe0da0c9 0xbe49989b 0xa3a4ac48 0x6f6b1ea0 0x3d2392b6 0x868a5012 0x9b0fc9c6 0xc41dfcde 0x916d7a82 0xdc3846ab 0x32a2c85b 0x54410836 0x8f307afc 0x52e7fdea 0xccad1499 0x6e72699c 0xa925b5b7 0x34f71ec6 0xd55b4ee5 0x43e2ab32 0x9ceeee56 0xcddc8708 0xecd2667d 0xa984c300 0x35bffb71 0xe610993d 0xc51f146 0x56f507bf 0xede9895d 0x8b43c0b1 0x6fee1098 0xe9c46245 0x15160d37 0x93e8cd10 0x6d29bed1 0x4489bb8e 0x343269ad 0x84121816 0x9d74d76e 0xe0475631 0x92f2ba62 0x7d3a1ac 0x8b86c917 0x8dcd7c49 0x930ea4ba 0xf264b4ca 0x55e24e32 0xdefde628 0x3649bc9b 0xb707781f 0xeb8cde45 0x28e17ca6 0xde607c0b 0x173df952 0xd897bfef 0x5027fb07 0x61ef057f 0x36474880 0xdca2189c 0x9ee3a38e 0x31d40afa 0xd83c9000 0xff74cb7>;
--Modulus (N) as a big-endian multi-word integer rsa,exponent = <0x00 0x10001>;--Public exponent (E) as a 64 bit unsigned integer rsa,n0-inverse = <0xa23eaef9>;-- -1 / modulus[0] mod 2^32 rsa,num-bits = <0x800>;--Number of key bits (e.g. 2048) key-name-hint = "dev";--用于签名验签的秘钥名称。 }; }; };
1.5 uboot中内嵌RSA2048公钥以及验签所需参数
将pub-uboot-builtin.dts中的signature节点需要拷贝到uboot的dts中进行编译。
在uboot进行验签时需要使用其中参数。
1.6 手动验证签名和验签流程
uboot提供工具fit_check_sign在Host上校验生成的itb文件和dtb中的signature是否匹配:
tools/fit_check_sign -f kernel-fdt.itb -k uboot.dtb
2 uboot关于FIT验签启动流程
2.1 uboot支持FIT验签
uboot要支持FIT验签需要打开如下Feature:
Boot images ->Support Flattened Image Tree--支持FIT格式镜像解析启动。
->Enable signature verfication of FIT uImages--支持FIT子镜像验签。
当选择CONFIG_FIT_SIGNATURE后,也会打开CONFIG_HASH和CONFIG_RSA。
2.2 uboot FIT验签流程
关于FIT签名和在uboot阶段的验签流程,可以参考《doc/uImage.FIT/verified-boot.txt》。
uboot安全启动流程:
fit_image_load
fit_get_image_type_property--根据image_type获取对应名称。
fit_image_get_node--如果指定子镜像名称,则直接在FIT里面找到node offset。
fit_conf_get_node--如果没有指定子镜像名称,则通过configuration加载。如果没有指定configuration,则使用default。
fit_config_verify--校验configuration。
fit_config_verify_required_sigs
fit_config_verify_sig--如果定义了required并且为conf则进行configuration验签。
fit_config_check_sig
fit_image_setup_verfify--初始化struct image_sign_info结构体。由checksum_algos和crypto_algos可知,仅支持sha1/sha256+rsa2048/rsa4096组合。由padding_algos可知支持pkcs-1.5/pss两种padding算法。
fit_image_hash_get_value--获取当前子节点value值,即签名。
info.crypto->verify--对应verify函数,即rsa_verify()。
info->checksum->calculate--对应hash_calculate函数。对于验签的哈希,sha1和sha256都是对应此函数。对区域内内容进行哈希计算。
rsa_verify_with_keynote--获取key-dev类似的公钥节点,然后进行验签。
fdtdec_get_int--获取rsa,num-bits、rsa,n0-inverse、rsa,exponent、rsa,modulus、rsa,r-squared。
rsa_verify_key--prop即是RSA public key内容。使用prop对签名进行解密得到hash。
rsa_mod_exp--使用prop得到的公钥对签名内容进行解密得到pkcs1.5 padding的hash。
mod_exp_sw
rsa_mod_exp_sw
padding->verify--可能是padding_pkcs_15_verify()。
padding_pkcs_15_verify--先检查padding,然后比较hash是否一致。
->memcmp--将计算得到的hash值和对签名进行解密的得到的hash进行比较,一致则通过。
fit_image_select
fit_image_print--打印FIT镜像的详细信息。
fit_image_verify--验证镜像的一致性。
fit_image_get_data_and_size
fit_image_verify_with_data
fit_image_verify_required_sigs--处理FIT中signature节点,进行验签。
fit_image_verify_sig
fit_image_check_sig
fit_image_setup_verify
fit_image_hash_get_value
info.crypto->verify--对应crypto_algo的verify函数,比如rsa_verify。
fit_image_check_hash--处理FIT中hash节点。计算hash值,并和从FIT中获取的hash进行比较。
fit_image_hash_get_algo--获取哈希算法名称。
fit_image_hash_get_value--获取hash值。
calculate_hash--计算hash值,。
fit_image_check_sig--类似于fit_config_check_sig()。
fit_image_get_data_and_size--获取当前镜像的buf地址和size大小。
fit_image_get_load--获取当前镜像的load地址。
image_decomp--如果镜像需要解压,则调用image_decomp将输入数据data解压到load地址。
memcpy--如果load和当前data地址不一致,则需要memcpy一次。
通过fit_image_print()输出的打印如下:
Description: Linux kernel Type: Kernel Image Compression: uncompressed Data Start: 0x830000e0 Data Size: 5971920 Bytes = 5.7 MiB Architecture: ARM OS: Linux Load Address: 0x830000e0 Entry Point: 0x830000e0 Hash algo: crc32 Hash value: 35223e04
2.2.1 hash计算
单独hash校验支持:crc32、sha1、sha256、md5。
int calculate_hash(const void *data, int data_len, const char *algo, uint8_t *value, int *value_len) { if (IMAGE_ENABLE_CRC32 && strcmp(algo, "crc32") == 0) { *((uint32_t *)value) = crc32_wd(0, data, data_len, CHUNKSZ_CRC32); *((uint32_t *)value) = cpu_to_uimage(*((uint32_t *)value)); *value_len = 4; } else if (IMAGE_ENABLE_SHA1 && strcmp(algo, "sha1") == 0) { sha1_csum_wd((unsigned char *)data, data_len, (unsigned char *)value, CHUNKSZ_SHA1); *value_len = 20; } else if (IMAGE_ENABLE_SHA256 && strcmp(algo, "sha256") == 0) { sha256_csum_wd((unsigned char *)data, data_len, (unsigned char *)value, CHUNKSZ_SHA256); *value_len = SHA256_SUM_LEN; } else if (IMAGE_ENABLE_MD5 && strcmp(algo, "md5") == 0) { md5_wd((unsigned char *)data, data_len, value, CHUNKSZ_MD5); *value_len = 16; } else { debug("Unsupported hash alogrithm\n"); return -1; } return 0; }
2.2.2 验签算法
验签算法支持(sha1/sha256)+(rsa2048/rsa4096)组合,结果通过pkcs-1.5填充。
struct checksum_algo checksum_algos[] = { { .name = "sha1", .checksum_len = SHA1_SUM_LEN, .der_len = SHA1_DER_LEN, .der_prefix = sha1_der_prefix, #if IMAGE_ENABLE_SIGN .calculate_sign = EVP_sha1, #endif .calculate = hash_calculate, }, { .name = "sha256", .checksum_len = SHA256_SUM_LEN, .der_len = SHA256_DER_LEN, .der_prefix = sha256_der_prefix, #if IMAGE_ENABLE_SIGN .calculate_sign = EVP_sha256, #endif .calculate = hash_calculate, } }; struct crypto_algo crypto_algos[] = { { .name = "rsa2048", .key_len = RSA2048_BYTES, .sign = rsa_sign, .add_verify_data = rsa_add_verify_data, .verify = rsa_verify, }, { .name = "rsa4096", .key_len = RSA4096_BYTES, .sign = rsa_sign, .add_verify_data = rsa_add_verify_data, .verify = rsa_verify, } }; struct padding_algo padding_algos[] = { { .name = "pkcs-1.5", .verify = padding_pkcs_15_verify, }, #ifdef CONFIG_FIT_ENABLE_RSASSA_PSS_SUPPORT { .name = "pss", .verify = padding_pss_verify, } #endif /* CONFIG_FIT_ENABLE_RSASSA_PSS_SUPPORT */ };
参考文档:
基于FIT的SecureBoot系列文档:《secure boot (一)FIT Image》《secure boot (二)基本概念和框架》《secure boot(三)secure boot的签名和验签方案》。