[toc]
背景
MTK7981 平台在进行了一定量的出货后遇到了与升级有关的诸多缺陷,该文档的目的是分析这些问题出现的原因并介绍解决方案。
问题介绍
升级过程中MTD层 read write 报错
该问题表现为当MTD在读写一个坏块的时候会返回EIO,但是升级代码并没有对这种直接当作升级失败。
一开始我们认为MTD会在读取写入坏块的时候对坏块单独处理,进行类似逻辑块物理块的转换的操作,或者直接跳过坏块的操作,但是后来真的遇到坏块后发现其直接报错,在分析了MTD NAND驱动后发现裸的NAND驱动在MTD层会直接感知到坏块的,这就意味着只要这个分区有一个坏块,那么现有方案无论是写入擦除读取还是挂载都一定会报错。
很多原厂会有自己的轻量级坏块处理方案,也就是在NAND中抽出一块区域用作坏块管理备份。MTK就是在128M的nand flash中抽取了 2M的空间用作坏块管理,然后在MTD驱动中需要选择nmbm这个驱动,这个驱动进行了物理块和逻辑块的转换,对于坏块这个驱动将这个块挪到最后的2M空间中,经过验证只要使用这个驱动,在MTD层就不会在感知到坏块。NMBM驱动细节可以参考 MT7986_NMBM_AN_20221104_v1.1.pdf 。
但是这种原厂提供的中间层MTD驱动,并不能解决所有问题,其主要目的还是保证UBOOT的启动不会因为坏块而出问题。
读取过程中的位翻转问题
这个问题相比与第一个问题更加致命,因为这个很难在启动包括挂载过程中感知的,会体现在对于一个文件的访问出现报错,如果这种位翻转出现在关键文件中将直接导致系统一直处于不可用的状态且无法恢复。处理这种问题唯一的办法就是使用UBI, UBI内部具有处理位翻转的机制,这就以为着基于NAND做存储的系统,必须全部使用UBI卷来避免这种随机的位翻转。
升级过程修改了正在使用的数据导致文件系统报错
app 镜像以squashfs的形式直接挂载,但是升级功能在app镜像中,app镜像需要修改自己,往往会在升级过程中直接导致升级进程崩溃,为了解决这个情况app需要将很多升级有关的依赖库放到内存中,非常繁琐而且容易出错。
升级过程中的内存不足问题
路由器和AC产品的升级逻辑要求在升级失败后,整体系统依然正常,而原有的升级逻辑是将升级包完整的读取到内存中,然而现有业务逻辑经常存在内存不足的情况,不能保证在原有业务不受影响的情况下完成升级,这种情况可以采用先将数据写入到NAND中的一个升级分区,然后重启升级,但是这种方案虽然避免了升级中的内存瓶颈,但是这种情况需要重启两次,而且需要Uboot和Rootfs中增加大量的升级逻辑。同时这种升级方式不再兼容原有的升级逻辑,升级时需要将数据写入ubi卷中,这种代码大规模修改是非常困难而且危险的。
升级过程中断电无法无感恢复
原有升级逻辑是在升级失败后会进入recovery系统,但是原有recovery还不支持萤石云直接恢复,而且并不是所有产品都支持接入萤石云。这个时候需要用户感知的传入升级包进行系统恢复。而且recovery存在误入风险。
升级业务兼容性难以保证
这种情况体现在hikupdate升级逻辑的代码为了兼容不同的平台变得越来越冗余,通用反而变得累赘。
问题分析
上面是我对升级问题的一些总结,不难发现最严重的前两个问题都是由于nand的物理特性直接导致的,原先我们大多使用的是NOR FLASH,所以并没有充分考虑nand的物理特性,nor与nand虽然在软件上都没抽象为mtd设备但是在使用上区别很大,不能无视。
1、 NAND比NOR便宜;NAND的容量比NOR大(指相同成本);NAND的擦写次数是NOR的十倍;NAND的擦除和写入速度比NOR快,读取速度比NOR稍慢;
2、 NAND和NOR的读都可以以字节为单位,但NAND的写以page为单位,而NOR可以随机写每一个字节。NAND和NOR的擦除都以block为单位,但一般NAND的block比NOR的block小。另外,不管是NAND还是NOR,在写入前,都必须先进行擦除操作,但是NOR在擦除前要先写0;;
3、 NAND和NOR都可能发生比特位反转(但NAND反转的几率远大于NOR),因此这两者都必须进行ECC操作;NAND可能会有坏块(出厂时厂家会对坏块做标记),在使用过程中也还有可能会出现新的坏块,因此NAND驱动必须对坏块进行管理。
NAND特性
以W25N01GV这款NAND Flash为例 (size 128MB, page size 2048, block size 128K):
比较通用的结构如下:Nand⇒ 片/Die ⇒ Plane ⇒ Block ⇒ Page + oob,其中obb(Out Of Band)也叫做空闲区域(spare area)/冗余区域(redundant area),这样设计是为了保证数据的正确性,可以在obb区存储校验数据、标记坏块、映射信息等。
每个Nand包含一个或多个片。片是可以独立执行命令并上报状态的最小单元。
每个片包含一个或多个plane。不同的plane间可以并发操作,不过有一些限制。
每个plane包含多block,block是最小擦除单元(擦除后为全1,擦除失败则判定为坏块)。
每个block包含多个page+obb, page是最小的读写单元。
Nand有如下特性:
- Nand在写入前需要先擦除,那么写入就必须要进行块操作,如果是更改数据那么需要先读取block再擦除后写入。
- block的擦写次数是有限的,最大擦写次数称为PECycles。
- 由于物理特性的原因,在读写时会对其他的页产生影响、漂移效应也会产生影响,会导致的出现位反转(bit flip)的现象(原先Nand中的某个位,从1变成0或者从0变成1)(这里写的物理的原因,也可能是软件上的原因,处理方法是一样的)。
基于以上特性nand在使用过程必须要解决一下问题:
- 既然flash的每个单元的擦写次数是有限的,那么如果我们总是频繁的擦写flash上的某些块,而另外一些块很少被擦写,那么这些擦写频繁的块就会很快达到使用寿命成为坏块,这样的坏块多了,如果设计上没有考虑好这个问题,就会影响整个设备的使用寿命。(磨损均衡)
- 既然会发生位翻转,怎么发现存储的数据变了,怎么进行修复。(校验算法)
- 怎么判定一个快为坏块
- 检测到坏块后,如何避免使用坏块?
NAND坏块管理方法可分为如下几类:
基于FTL的坏块管理
它使用一个额外的 FTL (Flash Translation Layer)对 NAND 进行管理,对外部屏蔽了坏块信息,U 盘、SD 卡、MMC 卡以及固态硬盘都使用这种管理方法。这种方式简化了 NAND 操作,但也使坏块信息对外部而言不可见,如果系统中出现了可能和坏块相关的问题,定位和调试变得困难,另外,FTL 也需要额外的硬件成本。
基于NAND文件系统的坏块管理
JFFS2、 YAFFS2、 FlashFx 这些专门针对 NAND 的文件系统可以对坏块进行管理。
NAND管理中间件
有一些中间件(Middleware)专门用于 NAND 管理,比如 UBI。
轻量级NAND坏块管理
轻量级的坏块管理只专注于坏块,并不提供擦写负载平均的支持,而且,它也不依赖于任何第三方的库。因此,轻量级的坏块管理方式降低了系统的复杂度,而且免去了加载文件系统或初始化中间件的时间,NMBM旧属于这种方案。
所以对于采用NAND的嵌入式系统来说,必须使用上述方案中的一种。
UBI介绍
UBI目前是最成熟的用于解决NAND管理问题的中间件,我们需要了解一下UBI
UBI全称Unsorted Block Images,是一种原始flash设备的卷管理系统。这个系统能在一个物理的flash设备上管理多个卷并且能在整个flash上实现损耗均衡。
从某种意义上说,UBI和LVM(Logical Volume Manager)有点相似,LVM将逻辑扇区映射到物理扇区上面,UBI映射逻辑擦除块到物理擦除块。但是除了映射,UBI还实现了全局的损耗均衡和透明的I/O错误处理。
一个UBI卷就是一串连续的逻辑擦除块(LEBs)。每一个逻辑擦除块可以被映射到任何一个物理擦除块(PEB)上面。这个映射是由UBI管理,这种映射对用户是透明的,同时这种映射也是UBI实现全局的损耗均衡的基础。通过每一个物理擦除块记录的擦除计数,可以将数据从损耗严重的物理块转移到损耗较少的擦除块。
UBI的主要功能如下:
- UBI 提供可以动态创建、删除或调整大小的卷;
- UBI 在整个闪存设备上实现磨损均衡(您可能认为您在不断地写入/擦除UBI卷的同一个逻辑擦除块,但实际上UBI 会将其均衡分配到闪存的所有物理擦除块);
- UBI 透明地处理坏物理擦除块;
- UBI会处理坏的擦除块,无需上层软件参与。 UBI 有一个保留的物理擦除块池,当一个物理擦除块变坏时,它会透明地用一个好的物理擦除块替换它。 UBI 将数据从新发现的坏物理擦除块移动到好的物理擦除块。 结果是 UBI 卷的用户不会注意到 I/O 错误,因为 UBI 会透明地处理它们。
UBI 通过清理将丢失数据的机会降至最低。- NAND 闪存容易受到读写操作中发生的位翻转错误的影响。位翻转由ECC校验和纠正,但它们可能会随着时间的推移累积并导致数据丢失。UBI通过将数据从具有位翻转的物理擦除块移动到其他物理擦除块来处理这个问题。这个过程称为scrub。 scrub工作在后台透明地完成,并且对上层隐藏。
一个UBI卷就是一串连续的逻辑擦除块(LEBs)。
UBI卷大小在创建卷时指定,但以后可能会更改(卷可动态调整大小)。UBI提供用户空间工具用于操作UBI卷。
MTD分区和UBI卷的相同点:
两者都由擦除块组成——在UBI卷的情况下是逻辑擦除块,在MTD分区的情况下是物理擦除块;
两者都支持三种基本操作:读、写和擦除。
但与 MTD分区相比,UBI卷具有以下优点:
UBI实现了wear-leveling,用户完全不用关心这个,也就是说上层软件会更简单;
UBI处理坏的擦除块,这也使上层软件更简单;
UBI 卷可以是动态的,它们可以动态创建、删除或调整大小,而MTD分区总是静态的;
UBI 处理位翻转,这再次使上层软件更简单;
UBI提供了卷更新操作,该操作使检测中断的软件更新和恢复变得更容易;
UBI 提供原子逻辑擦除块更改操作,如果在操作期间发生不干净的重启,它允许更改逻辑擦除块的内容而不会丢失数据;这可能对上层软件非常有用(例如,对于文件系统);
UBI有一个un-map 操作,它只是从物理擦除块中取消映射一个逻辑擦除块,调度物理擦除块进行擦除,然后返回;这非常快,并且使上层软件免于实现自己的机制来延迟擦除(例如,JFFS2必须实现这样的机制)。
除此之外,UBI还提供了一个块设备,允许将常规的、面向块的文件系统挂载在UBI卷的顶部。这是可能的,因为UBI透明地处理坏块,这种块设备是只读的只能以只读的方式挂载文件系统。
UBI headers
UBI在每个非坏物理擦除块的开头存储2个小的64 字节headers:
包含物理擦除块(PEB)的擦除计数器和其他信息的擦除计数器头(EC header)
卷标识符头(VID header),存储卷ID 和此PEB所属的逻辑擦除块 (LEB) 编号
这就是逻辑擦除块小于物理擦除块的原因——header占用一些闪存空间
UBI 将一些闪存空间用于自己的目的,从而减少了可供 UBI 用户使用的闪存空间量。 即:
2个PEB用于存储卷表;
1 PEB 保留用于磨损均衡目的;
1个PEB是为原子LEB更改操作保留的;
保留一些 PEB 用于不良的 PEB 处理; 这适用于 NAND 闪存,但不适用于 NOR 闪存; 保留的PEB数量是可配置的,默认等于每1024个区块20个区块;
UBI 将 EC 和 VID header存储在每个 PEB 的开头; 用于这些目的的字节数取决于闪存类型,如下所述。
W -闪存上物理擦除块的总数(NB:不是MTD分区);
P – MTD 分区上的物理擦除块总数;
SP – 物理擦除块大小;
SL – 逻辑擦除块大小;
BB – MTD 分区上的坏块数;
BR – 为坏 PEB 处理保留的 PEB 数量(默认情况下,NAND 为 20 W/1024,NOR 和其他没有坏 PEB 的闪存类型为 0)
B – MAX(BR,BB)
O – 与以字节为单位存储 EC 和 VID header相关的开销,即 O = SP – SL。
UBI 开销为 (B + 4) SP + O * (P – B – 4),即用户无法访问此字节数。 O对于不同的Flash是不同的:
在 NOR flash 的情况下,它具有 1 字节的最小 I/O 单元,O 是 128 字节;
在没有sub-page的 NAND 闪存(例如 MLC NAND)的情况下,O 是 2 个 NAND page,即在 4KiB NAND page的情况下为 8KiB,在 512 字节 NAND page的情况下为 1KiB;
对于具有sub-page的 NAND 闪存,UBI 优化其on-flash 布局并将 EC 和 VID header放在相同的 NAND page,但不同的sub-page中; 在这种情况下,O只是一个 NAND page;
对于其他闪存,如果最小 I/O 单元大小大于或等于 64 字节,则开销应为 2 个最小 I/O 单元,如果最小I/O单元大小小于64字节,则开销应为2倍64字节并与最小I/O单元大小对齐。
注意:上面的公式将坏块计为 UBI 开销。 真正的 UBI 开销是:(B – BB + 4) SP + O (P – B – 4)
实际来看一个45M 的 mtdblock ,在初始化位ubi卷之后大约40M左右,需要大约5M的开销。
方案对比
既然NAND的特性是常见的那么我们可以分析一下开源的方案,看看是否可以吸取一些经验。
上图是海康原有的分区方案,下图是openwrt采用的通用分区方案。通过对比我们不难发现我们原有方案的最大问题是所有的镜像数据都是裸写在mtd层的,而openwrt则使用了一个大的UBI分区里面分了三个小卷,所有的数据都是存在UBI卷中的,交由UBI管理所有的NAND问题,相比与我们的方案可靠性会高很多,此外其将整个系统完整的放在一个ubi分区中也是值的借鉴的。
此外mtk 原厂方案还支持双slot方案,也就是从一个升另外一个。
新方案设计与实现
为了一劳永逸的解决现有的所有遇到的问题,重新设计了一整套分区升级启动方案,可以完美解决现有缺陷。
双DIGICAP分区方案 总体设计
新的分区方案中的SYS0,SYS1都是具有独立功能的分区,是一种互相备份的关系。单个SYS分区是45M , 每个分区都只有一个叫digicap的UBI卷,最大40 M左右,卷中直接写入了一个digicap包,这里意味着digicap包的结构不光是在升级中会用到,启动也需要使用digicap的打包结构,为了让操作系统可以无感的使用digicap包中的镜像文件。在系统启动中需要将两个分区都分别挂载,
需要设计两个文件系统,其中一个可以直接访问digicap打包的所有文件,另外一个需要可以文件的形式直接修改块设备,一个主要功能在于读需要感知digciap包的结构,另外一个主要功能在于写,可以理解为将一个块设备变成一个文件,只要修改了这个文件则同步修改了块设备。
如此一来,因为所有的数据都是经过了ubi层,因为NAND特性导致坏块问题和位翻转问题都得到了解决,同时因为采用了双分区方案在一次升级中并不会修改正在使用或者正在挂载的数据,则不再需要umount掉正在挂载的分区。同时因为数据直接写入flash所以不会升级过程中消耗很多的内存,同时就算写入失败也不会影响正常功能,下一次启动依然使用当前分区可以正常启动,意味着两个系统无论如何总是有一个正常的。
HIKBOOT启动方案
UBOOT 在之前为了可以进行updatek,和uboot命令行保护种种原因,需要有一些体现digicap内部细节的代码,在代码中会暴露加密密钥和digicap打包结构,因为uboot的开源特性所以需要进行Uboot gpl隔离,但是uboot_gpl隔离相关代码的适配是比较困难的需要有一定的适配经验,那么有没有什么方法既不使用uboot_gpl方案又可以保证uboot代码可以不暴露一些不想暴露的细节。为此需要设计一个具有类似uboot功能的可执行程序代替原有Uboot实现updatek,updateb功能,同时需要使用uImage 支持的FIT打包方式。在digicap包中打入fit镜像,则uboot在启动过程中则可以通过直接向后查找查找fit镜像的方式启动,然后彻底关闭uboot命令行也就不再需要在uboot实现一个psh。uboot只需要实现可以启动FIT镜像的功能和一个按键升级的功能即可。
Kernel中引入的Device Trace概念,将配置信息放入dtb中。达到一个kernel,结合不同dtb适配多个平台。
FIT是Flattened Image Tree的意思,即将多个镜像通过dts语法编译生成一个镜像文件。
uboot支持编写its文件,通过mkimage和dtc创建kernel、ramdisk、dtb等等文件的打包镜像。
这里的dts文件通常命名为.its,输出的镜像文件通常命名.itb。
FIT 创建流程
创建FIT镜像的命令:
mkimage -f multi.its fit.itb
查看FIT镜像头:
mkimage -l fit.itb
its(image tree source)是创建FIT镜像的配置脚本,itb(flattened image tree blob)是FIT镜像
FIT解析流程
bootm命令使用
通过bootm命令可以解析FIT镜像并启动:
bootm--后面没有地址则使用CONFIG_SYS_LOAD_ADDR地址作为启动地址。
bootm <addr>--使用default配置启动。
bootm <addr>:<image name>--单独加载FIT中<image name>镜像。
bootm <addr>#<config name>--使用指定<config name>启动。
bootm还支持将整个流程拆解为如下步骤:
start [addr [arg ...]]
loados - load OS image
ramdisk - relocate initrd, set env initrd_start/initrd_end
fdt - relocate flat device tree
cmdline - OS specific command line processing/setup
bdt - OS specific bd_t processing
prep - OS specific prep before relocation or go
go - start OS
bootm流程解析
下面解析bootm关于FIT的处理流程:
do_bootm
->do_bootm_states--根据states调用一系列函数进行处理。
->bootm_start
->bootm_find_os--加载OS镜像。
->boot_get_kernel
->fit_image_load--从指定或默认configuration中加载kernel镜像。
->genimg_get_format--对于IMAGE_FORMAT_FIT类型镜像,从FIT中获取kernel镜像。
->fit_image_get_type--images.os.type。
->fit_image_get_comp--images.os.comp。
->fit_image_get_os--images.os.os。
->fit_image_get_arch--images.os.arch。
->fit_image_get_load--images.os.load。
->fit_image_get_entry--images.ep。
->bootm_find_other--加载OS相关镜像,比如ramdisk和dtb等。
->bootm_find_images
->boot_get_ramdisk--获取images.rd_start和images.rd_end。
->fit_image_load--从FIT镜像中获取ramdisk文件。
->boot_get_fdt--获取images.ft_addr和images.ft_len。
->boot_get_fdt_fit--从FIT中获取dtb文件。
->fit_image_load--根据fdt镜像名获取dtb文件。
->bootm_load_os
->image_decomp--解压缩kernel镜像。至此kernel镜像已经完成加载。
->boot_ramdisk_high--根据initrd_high的值否则分配地址移动initrd内容。自此ramdisk内容就绪。
->boot_relocate_fdt--根据fdt_high的值否则分区地址移动fdt内容。至此fdt内容就绪。
->bootm_os_get_boot_func--根据os类型从boot_os[]获取启动函数,linux对应do_bootm_linux()。
->boot_selected_os
->do_bootm_linux
->boot_prep_linux
->image_setup_linux
->boot_fdt_add_mem_rsv_regions
->boot_relocate_fdt
->image_setup_libfdt--根据需要更新dtb文件内容。
->fdt_root
->fdt_chosen
->arch_fixup_fdt
->fdt_initrd--创建或更新linux,initrd-start和linux,initrd-end节点。
->boot_jump_linux
->kernel_entry(0, machid, r2)--kernel_entry即为kernel镜像的entry属性,machid是从环境变量machid获取,r2为dtb地址。
随后进入Linux kernel启动,dtb从r2获取,initrd已在dtb中指定。kernel启动解析dtb,然后解压initrd内容形成ramfs。
Linux initrd解析
内核从start_kernel开始执行C代码,对initrd的处理如下:
start_kernel
->setup_arch
->setup_arch
->setup_machine_fdt--参数__atags_pointer即为从r2获取的dtb地址。
->early_init_dt_scan_nodes
->early_init_dt_scan_chosen
->early_init_dt_check_for_initrd--从dtb中获取linux,initrd-start和linux,initrd-end的值。
->__early_init_dt_declare_initrd--将initrd的起始和结束地址赋给initrd_start和initrd_end。
->arch_call_rest_init
->rest_init
->kernel_init
->kernel_init_freeable
->do_basic_setup
->do_initcalls
->do_initcall_level
->populate_rootfs
->unpack_to_rootfs(__initramfs_start, __initramfs_size);--优先解压内部ramfs。如果initrd_start为0或者强制使用initramfs,则使用initramfs。
->unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start)--其次解压从dtb传入的外部initrd。
FIT镜像签名与验签功能
我们不难发现其实用FIT镜像直接启动存在致命缺陷,就是可以伪造digicap包写入flash后,只要镜像中包含fit格式的镜像,可以启动自己定制的系统,就可以获取root权限,这是非常危险的,所以这里使用的镜像必须进行签名。签名流程大致如下:
实现方式
关于支持对configuration签名的its,可以参考《doc/uImage.FIT/sign-configs.its》;对镜像签名的its,可以参考《doc/uImage.FIT/sign-images.its》。
编写hikboot.its增加对每个子镜像的签名
/*
* 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";
};
};
};
};
同时需要在Uboot的设备树中放入公钥:
/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";--用于签名验签的秘钥名称。
};
};
};
uboot 安全启动流程
关于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是否一致。检查padding是否符合pkcs1.5规范。
->memcmp--将计算得到的hash值和对签名进行解密的得到的hash进行比较,一致则通过。打印FIT镜像的详细信息。
验证镜像的一致性。处理FIT中signature节点,进行验签。-对应crypto_algo的verify函数,比如rsa_verify。
处理FIT中hash节点。计算hash值,并和从FIT中获取的hash进行比较。
获取哈希算法名称。
获取hash值。
计算hash值,。
类似于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()输出的打印如下:
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 alogrithmn");
return -1;
}
return 0;
}
验签算法
验签算法支持(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 */
};
线上签名
为了保证签名的安全性,必须采用线上签名的方式。需要修改部分mkimage的代码,使用hikpack类似的方式获取签名数据后写入fit镜像。
留言