i.MX6ULL HAB安全启动实战:从PKI构建到镜像签名与熔丝烧录
1. 项目缘起:为什么imx6ull的Secure Boot让我折腾了这么久
最近在给一个基于NXP i.MX6ULL的工控设备做固件升级方案,客户提了一个硬性要求:必须启用Secure Boot,防止产线或现场被刷入未经授权的固件。这个要求合情合理,毕竟设备一旦部署出去,物理安全就很难完全保障。Secure Boot(安全启动)是嵌入式领域,尤其是工业物联网设备的一道重要安全防线。它的核心逻辑很简单:芯片上电后,最先运行的BootROM会去验证接下来要加载的镜像(比如U-Boot)的数字签名。只有用芯片厂商(这里是NXP)授权的私钥签过名的镜像,才能被信任并执行;否则,芯片会直接进入下载模式或者干脆“变砖”,拒绝启动。
听起来很美好,对吧?但真动手在i.MX6ULL上搞HAB(High Assurance Boot,NXP对其Secure Boot的实现)签名时,我才发现这里面的水比想象中深。网上的资料要么是NXP官方晦涩难懂的PDF,满篇的寄存器描述和命令行参数;要么是些零散的博客,只讲了某一步,缺了上下文,照着做十有八九会卡住。最常见的结局就是,你满怀信心地生成了签名,烧录进去,结果设备亮都不亮,串口只输出一句“HAB failure: 0xXX”,然后就没了下文,让人一头雾水。
所以,我决定把这次从零开始,在i.MX6ULL上成功实现HAB签名的完整过程、踩过的所有坑、以及最终让设备“欢快”跑起来的核心要点,系统地梳理出来。这不是一篇照搬官方手册的翻译,而是一个一线工程师的实战复盘。你会看到我为什么选择某些工具,某个参数填错会导致什么后果,以及当遇到那个令人抓狂的“HAB failure”时,应该如何一步步抽丝剥茧找到问题根源。无论你是正在面临同样的需求,还是想提前了解i.MX系列芯片的安全启动机制,这篇长文都应该能给你提供一条清晰的路径。
2. 理解HAB的基石:公钥基础设施(PKI)与SRK表
在动手敲任何命令之前,我们必须先搞清楚HAB签名的底层逻辑,否则后面的所有步骤都将是盲人摸象。HAB本质上构建了一套基于公钥密码学的信任链,而这条链的起点,就是SRK(Super Root Key)表。
你可以把SRK想象成设备出厂时内置的一把“万能信任锚”。在i.MX6ULL的OTP(One-Time Programmable)熔丝中,可以烧录一组SRK哈希值(Hash)。这组哈希值对应着一组公钥。芯片的BootROM在验证镜像时,首先会用熔丝里的SRK哈希,去核对镜像中附带的SRK表(里面包含了完整的公钥)是否可信。只有SRK表被信任了,才能用SRK表中的公钥,去验证后续的镜像签名。
那么,这套密钥从哪里来?答案是我们需要自己搭建一个微型的PKI(公钥基础设施)。通常,我们会生成一个自签名的根证书(CA),然后用这个CA去签发一系列子证书,最终用于签名的私钥就保护在这些证书里。NXP的流程推荐使用OpenSSL命令行工具来生成这些密钥对和证书。
注意:这里有一个关键选择。NXP的HAB支持两种签名算法:RSA2048和RSA4096。i.MX6ULL完全支持这两种。选择RSA2048,签名和验证速度更快,生成的签名块体积更小;选择RSA4096,理论上更安全,但会占用更多的镜像空间,验证时间也稍长。对于大多数工控场景,RSA2048已经足够安全。我本次项目就选择了RSA2048。
生成密钥和证书的步骤大致如下:
生成根密钥和证书:首先,我们生成一个RSA2048的根私钥(
ca_key.pem),然后用它创建一个自签名的根证书(ca_crt.pem)。这个根证书就代表了我们的“私有CA机构”。# 生成CA私钥 openssl genrsa -out ca_key.pem 2048 # 生成CA自签名证书 openssl req -new -x509 -days 3650 -key ca_key.pem -out ca_crt.pem在执行证书生成命令时,会交互式地让你输入国家、组织等信息。这些信息会体现在证书中,但对于HAB验证本身不是关键,可以按实际情况填写。
生成SRK密钥和证书:SRK通常不止一个,NXP建议使用4个SRK密钥来增强安全性(即使其中一个密钥泄露,系统仍可运行)。我们需要用上面的CA来签发SRK证书。
# 为每个SRK生成私钥和证书签名请求(CSR) for i in {1..4}; do openssl genrsa -out srk${i}_key.pem 2048 openssl req -new -key srk${i}_key.pem -out srk${i}.csr done # 使用CA证书为每个CSR签发证书 for i in {1..4}; do openssl x509 -req -in srk${i}.csr -CA ca_crt.pem -CAkey ca_key.pem -CAcreateserial -out srk${i}_crt.pem -days 3650 done生成SRK表并提取哈希:有了4个SRK证书后,我们需要用NXP提供的
srktool(通常位于cst工具包中)将它们打包成一个SRK表(SRK_1_2_3_4_table.bin),并从这个表中提取出SRK哈希。# 假设cst工具在../cst目录下 ../cst/srktool -h 4 -t SRK_1_2_3_4_table.bin -e SRK_1_2_3_4_fuse.bin -d sha256 -c srk1_crt.pem,srk2_crt.pem,srk3_crt.pem,srk4_crt.pem这个命令做了几件事:
-h 4指定SRK数量;-t输出SRK表文件;-e输出包含SRK哈希的熔丝文件;-d指定哈希算法为SHA256;-c指定输入的证书列表。生成的SRK_1_2_3_4_fuse.bin文件里,就包含了要烧录到OTP熔丝中的那组哈希值。
至此,我们完成了HAB信任链的“原材料”准备。接下来,就需要处理我们要签名的目标——U-Boot镜像了。
3. 镜像改造:为“裸镜像”穿上签名的“外衣”
直接从编译得到的U-Boot二进制文件(比如u-boot.imx)是不能直接用于HAB签名的。BootROM对镜像的格式有严格的要求,它期待一个带有特定头部信息的“封装镜像”。这个头部信息里包含了镜像长度、加载地址、入口地址以及最重要的——签名块和证书链的插入位置描述。
我们需要使用NXP提供的imx-mkimage工具套件(或者在一些旧SDK里叫mkimage)来生成这个符合要求的镜像。这个过程通常被称为“生成IVT/DCD/Bo ot Data”并打包。
准备配置文件:首先,你需要一个
.cfg文件,用来描述芯片类型、镜像组件等。对于i.MX6ULL,通常可以参考imx-mkimage/iMX6ULL/目录下的示例。一个简化的my_uboot.cfg可能长这样:[BOOT_FROM sd] [BOOT_OFFSET 0x400] [IVT_OFFSET 0x400] [CSF_OFFSET 0x2000]这里
BOOT_OFFSET和IVT_OFFSET通常设为相同值,表示IVT在镜像内的偏移。CSF_OFFSET则预留了给签名块(CSF)插入的位置,这个值非常关键,必须大于IVT、DCD、Boot Data和应用程序(即U-Boot)本身的总大小,并留有足够余量。生成初始镜像:使用
mkimage工具(注意,此mkimage非U-Boot的mkimage,是NXP专门的工具)处理。# 假设工具在imx-mkimage目录下 cd imx-mkimage ./mkimage -n my_uboot.cfg -T imx6ull -e 0x87800000 -d ../u-boot.bin u-boot-ivt.imx-n指定配置文件,-T指定芯片类型,-e指定U-Boot的入口地址(需要和U-Boot链接脚本一致,i.MX6ULL通常为0x87800000),-d指定输入的U-Boot二进制文件,输出为u-boot-ivt.imx。这个文件已经包含了IVT、DCD等结构,但还没有签名块,所以还不能用于Secure Boot。
现在,我们得到了一个“等待签名”的裸镜像。下一步就是为它注入数字签名。
4. 核心签名流程:使用CST工具生成并注入CSF
签名和证书注入是通过NXP的封闭源代码工具cst(Code Signing Tool)完成的。你需要从NXP官网申请并下载这个工具包(通常需要签署NDA)。得到cst后,最关键的一步是编写一个csf描述文件(例如hab4_pki.csf)。这个文件是指令脚本,告诉cst如何对镜像进行签名。
这个文件内容较多,但结构清晰,主要包含以下几个部分:
- 版本与引擎选择:指定HAB版本和加密引擎。
[Header] Version = 4.1 Hash Algorithm = sha256 Engine = SW - 安装SRK:指定之前生成的SRK表文件,并声明其哈希将被烧录到熔丝。
[Install SRK] File = “../keys/SRK_1_2_3_4_table.bin” Source index = 0 - 安装CSFK:CSFK(CSF Key)是用于保护CSF(命令序列文件,即这个文件本身)完整性的密钥。这里我们用第一个SRK证书对应的私钥来签名CSFK证书。
[Install CSFK] File = “../keys/csfk1_crt.pem” - 认证CSF:用CSFK私钥对整个CSF文件内容进行签名,确保CSF指令本身没有被篡改。
[Authenticate CSF] - 安装密钥:指定用于签名镜像的密钥证书(这里我们使用第二个SRK证书对应的私钥作为签名密钥)。
[Install Key] Verification index = 0 Target index = 2 File = “../keys/srk2_crt.pem” - 认证数据:这是最核心的指令,告诉HAB验证哪段镜像数据。需要指定镜像文件的路径、起始地址(即IVT的绝对地址)、长度。
Blocks字段用于指定要验证的连续数据块。
这里的[Authenticate Data] Verification index = 2 Blocks = 0x877ff400 0x0 0x20000 “u-boot-ivt.imx”Blocks参数是最大的坑点之一。第一个值是Image vector table (IVT)的绝对地址。这个地址怎么算?对于从SD卡启动,IVT通常位于偏移0x400处,而i.MX6ULL的内部RAM起始地址是0x00900000,但BootROM在加载时会进行一个重定位。实际上,更可靠的计算方法是:IVT绝对地址 = 镜像加载地址 + IVT偏移量。而镜像加载地址对于不同的启动设备(SD, NAND, eMMC)是不同的。我强烈建议你直接参考你所用BSP包中已有的csf示例文件,并仔细阅读NXP应用笔记AN4581。填错这个地址,百分百会导致“HAB failure”。
编写好csf文件后,就可以运行cst进行签名了:
../cst/cst -i hab4_pki.csf -o u-boot-ivt_signed.imx-i指定输入csf文件,-o指定输出已签名的镜像文件。如果一切顺利,cst会成功运行,并生成最终的u-boot-ivt_signed.imx。这个文件就是可以用于Secure Boot的完整镜像。
5. 熔丝烧录:将信任锚刻进芯片
生成了签名镜像,并不意味着设备就能自动启用Secure Boot。我们之前生成的SRK哈希还只是躺在电脑里的一个文件(SRK_1_2_3_4_fuse.bin)。必须将这个哈希值烧录到芯片的OTP熔丝中,BootROM才会在启动时去读取并使用它。
i.MX6ULL有一组专用的熔丝位用于HAB。烧录熔丝是一个不可逆的操作!一旦烧写,对应的熔丝位就无法再改回0。如果烧错了SRK哈希,那么所有用之前那套密钥签名的镜像都将无法启动,芯片很可能就“变砖”了,只能通过JTAG等底层方式尝试恢复(非常麻烦)。
因此,烧录熔丝前必须进行充分的测试。标准的测试流程是:
- 测试签名镜像:在不烧写熔丝的情况下,将签名镜像(
u-boot-ivt_signed.imx)烧录到设备的启动介质(如SD卡)。上电后,通过串口观察输出。如果签名验证通过,U-Boot应该能正常启动,并且在U-Boot启动日志的最开始,你会看到类似“HAB Configuration: 0xf0, HAB State: 0x66”的信息。HAB State: 0x66表示“HAB enabled and active (security configuration fused)”的模拟状态,即芯片认为熔丝已烧,但实际上我们还没烧。这是一个好迹象,说明签名本身是正确的,镜像格式也是对的。 - 确认测试通过:确保设备能稳定地用签名镜像启动多次。
- 备份原始镜像和密钥:务必保存好未签名的原始镜像、全套密钥对和证书。这是你未来更新固件的唯一凭证。
- 执行熔丝烧写:在U-Boot命令行下,使用
fuse命令进行烧写。你需要根据SRK_1_2_3_4_fuse.bin文件的内容,计算出对应的熔丝地址和值。例如,SRK_HASH可能分布在0x6-0x9的多个熔丝字(每个字32位)上。
再次警告:请逐字核对烧写的值和地址。一个错误的比特都可能导致灾难性后果。建议先烧写一个SRK_HASH(比如4个中的1个)进行测试,确认系统仍能启动,再烧写剩余的。# 在U-Boot中,示例命令格式 => fuse prog -y 0 6 0x12345678 => fuse prog -y 0 7 0x9abcdef0 ...
烧录完成后,芯片的Secure Boot功能才真正被激活。此时,只有用你那套私钥签名的镜像才能启动。尝试烧录一个未签名或错误签名的镜像,你会看到启动失败,串口输出HAB错误事件。
6. 故障排查:当遇到“HAB failure”时该怎么办
即使你万分小心,在第一次尝试HAB时,大概率还是会遇到启动失败和“HAB failure”的事件。别慌,这是常态。HAB提供了一套相对详细的事件(Event)报告机制,我们需要学会解读它。
当启动失败时,BootROM会在芯片的某个寄存器(对于i.MX6ULL,通常是SRC寄存器)中留下错误状态,并且如果串口初始化得足够早,你可能会在串口上看到错误码输出。更通用的方法是,在U-Boot中(如果你还能进入一个未启用安全启动的U-Boot),使用hab_status命令来获取详细的HAB事件日志。
HAB事件有一个类型和数值。你需要查阅NXP的官方文档(如AN4581的附录),来解码这些事件。常见的失败原因和排查思路如下:
- HAB Failure: 0x69:这通常是“CSF parse error”或“CSF authentication failed”。意味着CSF命令序列文件本身有问题。检查你的
csf文件:- 语法是否有错误?括号、引号是否匹配?
- 文件路径是否正确?
cst工具能否找到所有引用的密钥和镜像文件? [Authenticate Data]中的地址和长度是否正确计算?这是最高频的错误点。
- HAB Failure: 0x66:在未烧熔丝时看到这个,是正常的模拟状态。在已烧熔丝后还看到启动失败,则可能是SRK哈希不匹配。检查烧录到熔丝的值是否与
SRK_1_2_3_4_fuse.bin文件完全一致。检查生成SRK表时使用的证书是否与签名时使用的证书链匹配。 - 签名验证失败:如果事件指向具体的签名验证错误,检查:
- 签名用的私钥是否与证书链匹配?
- 镜像在签名后是否被意外修改?例如,某些烧录工具可能会在镜像头部添加额外信息。
- 是否使用了错误的哈希算法(如配置了sha256但实际用了sha1)?
我的一个实际踩坑案例:我最初在[Authenticate Data]的Blocks参数中,直接使用了从.imx文件头里读出来的IVT自包含的地址,结果一直报错。后来才发现,对于SD卡启动,BootROM在加载镜像到内存时,有一个固定的加载地址(例如0x877ff400),我的IVT地址需要基于这个加载地址来计算,而不是镜像内部的相对偏移。修正了这个地址后,签名立刻验证通过。
7. 构建自动化与持续集成:将签名流程脚本化
手动执行以上步骤不仅容易出错,也无法适应产品迭代的需求。我们必须将整个流程脚本化,集成到固件的构建系统(如Yocto, Buildroot)或CI/CD管道(如Jenkins, GitLab CI)中。
一个基本的自动化脚本流程如下:
- 编译U-Boot:由构建系统完成,输出
u-boot.bin。 - 生成IVT镜像:在编译后步骤中,调用
imx-mkimage工具,将u-boot.bin封装成u-boot-ivt.imx。这个步骤可以写成一个Makefile规则或Shell脚本。 - 签名镜像:调用
cst工具,传入预先准备好的csf模板文件和密钥路径,对u-boot-ivt.imx进行签名,生成u-boot-ivt_signed.imx。 - 打包最终镜像:将签名后的镜像与其他组件(如Linux内核、设备树、根文件系统)一起,打包成最终可供烧录的工厂镜像(如
.sdcard或.ubi映像)。
这里的关键点是csf文件的模板化。csf文件中的镜像文件名、路径等应该是变量。在CI环境中,可以通过脚本替换这些变量。同时,私钥必须被安全地管理,绝不能硬编码在脚本或仓库中。应该使用CI系统的秘密存储功能(如GitLab CI的Variables with Masking, Jenkins的Credentials Binding)来在运行时注入密钥文件或密码。
例如,一个简化的CI脚本片段:
#!/bin/bash # 1. 编译 make u-boot # 2. 生成IVT镜像 ./mkimage -n board/imx6ull/my_uboot.cfg -T imx6ull -e 0x87800000 -d u-boot.bin u-boot-ivt.imx # 3. 签名 (密钥从环境变量或安全存储中获取) cp $CST_CSF_TEMPLATE ./temp.csf sed -i “s|IMAGE_PATH|$(pwd)/u-boot-ivt.imx|g” ./temp.csf $CST_PATH/cst -i ./temp.csf -o u-boot-ivt_signed.imx # 4. 清理临时文件 rm ./temp.csf # 5. 后续打包...通过这样的自动化,每次代码提交后,CI系统都能自动产生一个已签名的、可直接用于生产的固件镜像,极大地提高了开发效率和安全性。
8. 进阶考量:密钥管理、吊销与固件更新
实现了一次性签名和烧录只是开始。在产品生命周期中,更严峻的挑战在于密钥的安全管理和固件的后续更新。
密钥管理:生成密钥的CA根私钥必须离线保存,最好放在硬件安全模块(HSM)或完全断网的电脑上。用于签名的私钥(即SRK对应的私钥)也需要严格保护。在团队中,应遵循最小权限原则,只有少数授权人员能访问签名密钥。CI系统中的签名步骤,也应配置为仅在发布正式版本时触发,并由授权人员审批。
密钥吊销:如果某个SRK私钥疑似泄露怎么办?HAB支持密钥吊销。在OTP熔丝中,除了SRK哈希,还有对应的吊销位(Revoke Fuse)。烧写对应的吊销熔丝,可以使该SRK失效。这就是为什么建议使用多个SRK(如4个)的原因。你可以预先烧录4个SRK哈希,但只使用其中1个进行当前版本的签名。如果这个密钥泄露,你可以通过更新固件(使用另一个SRK签名),并烧录吊销熔丝来吊销泄露的密钥,而无需召回所有设备。
安全固件更新:启用Secure Boot后,U-Boot和内核的更新也必须经过签名验证。通常有两种模式:
- 完全信任U-Boot模式:U-Boot验证后续的kernel和dtb的签名。你需要扩展你的PKI,为内核和设备树创建独立的签名证书,并在U-Boot中实现相应的验证逻辑(例如通过FIT Image机制)。
- 通过U-Boot的
ums或fastboot命令更新:这些命令在接收新镜像时,可以调用相同的HAB库进行验证,只有验证通过的镜像才会被写入存储介质。你需要确保更新工具链也集成签名功能。
无论哪种方式,都意味着你的产品OTA(空中下载)更新系统必须集成签名生成和验证的能力。这通常需要在服务器端用保护良好的私钥对更新包进行签名,设备端的更新程序(在U-Boot或内核中)用对应的公钥进行验证。
最后,我个人最大的体会是:测试,测试,再测试。在烧录熔丝之前,建立一个完整的测试流程。使用开发板,反复测试签名镜像在各种异常情况下的行为(如断电、损坏的镜像)。准备好一个“逃生”机制,比如在板上预留一个测试点或开关,可以在必要时强制进入串行下载模式,以便在密钥丢失或镜像错误时还能恢复。HAB是一把强大的安全锁,但钥匙的管理和应急方案同样重要。
