2. 不要急着换板子,先拆明白 STiROT 和 OEMuROT 的安全模型
几个月前我在调一块 STM32H573 的开发板,跑的是 ST 官方提供的 STiROT + OEMuROT 示例工程,卡在 provisioning 阶段死活过不去。日志来回就那么几行,定位到具体流程才弄清楚问题出在哪。这篇把整个排查过程和大伙儿捋一遍,尤其适合第一次碰 STM32H5 安全启动、对 STiROT/OEMuROT 关系还不太清楚的工程师。
先说结论:STiROT 和 OEMuROT 是两个层级不同的信任根,前者是 ST 出厂固化在系统 flash 的只读代码,后者是你在 OTP 里烧录自己的公钥哈希之后形成的第二级信任根。provisioning 失败大多数时候不是芯片坏了,是你在两个层级之间把密钥链、地址、选项字节搞错了。
为了能讲明白排查思路,先把这两层信任模型梳理一下,毕竟很多配置报错都是从概念混淆开始的。
2.1 STiROT:芯片出厂就有的不可变信任根
STM32H573 这代产品把安全启动分成了两层,STiROT 是第一层。它类似一个 ROM 里的引导程序,代码在系统 flash 里,出厂时就已经被 ST 烧录好,用户没法改,也没法擦。它做的事很简单:上电之后先校验第二级引导(OEMuROT)的完整性和真实性。
校验用的密钥不是 ST 的私钥,而是你自己生成的 OEM 密钥对中的公钥。也就是说,STiROT 里只是烧了 ST 自己的根密钥,用于验证固件包中的 OEM 公钥是否被合法发布;真正验证你应用固件的,是 OEMuROT 这一层。这个设计的好处是 ST 不需要把你的私钥拿走,也不会在出厂时就固化你的产品密钥。
很多人有个误区,以为 STiROT 校验 OEMuROT 的启动镜像就是用 ST 的公钥验签. 这句话只说对了一半。STiROT 里面那套机制确实包含 根公钥,但固件包合成时,OEMuROT 的头部信息中会携带你自己的 OEM 公钥哈希、密钥索引、启动地址等信息。STiROT 先用它自己的密钥验证固件包的签名,然后检查 OEM 公钥是否匹配你在 OTP 里烧录的哈希。全部通过,才把执行权交给 OEMuROT。
2.2 OEMuROT:你的产品信任根入口
OEMuROT 是你的第二级引导,它才是真正和你产品绑定的那一层。你需要用 STM32TrustedPackageCreator 生成自己的密钥对,把公钥哈希烧进 STM32H573 的 OTP,然后用私钥给 OEMuROT 固件签名。后续 OEMuROT 校验用户应用固件,用的就是这把私钥对应的公钥,形成一个独立于 ST 的信任链。
这样做的好处非常实际:STiROT 只负责"芯片出厂后第一个信任锚"的角色,以后产品的固件升级、防回滚、安全启动全部由你自己的密钥体系掌控。你在 provisioning 阶段做的所有操作,本质上就是把这套自有信任体系写入芯片。
所以 provisioning 操作的对象不是一个文件,而是一整套安全状态。具体包括:
- OTP 区域写入 OEM 公钥哈希、配置字、启动地址;
- 烧录 STiROT 和 OEMuROT 的代码镜像到系统 flash 和用户 flash;
- 设置读保护等级 RDP、写保护 WRP、安全属性配置;
- 烧录 Debug Authentication 证书,用于后续调试解锁。
我这次失败就卡在第三步和第四步的衔接处,第一轮只配置了 RDP 没有处理 DA 证书文件,结果后续回读、验证全被拒。
3. provisioning 的标准流程和配置项解析
为了定位问题,我把 ST 官方示例的 provisioning 流程重新完整跑了一遍。这里先给一份基于官方 TrustZone 示例工程的标准步骤,后续排查都会基于这个流程展开。
3.1 需要用到的软件和版本
我这次用的是 STM32CubeIDE 1.15 + STM32CubeProgrammer 2.16,官方示例包是 STM32Cube_FW_H5_V1.2.0。这里强烈建议不要用太老的 CubeProgrammer,早期版本对 H573 的 OTP 写入支持不全,尤其是不支持-obk相关命令时你可能连错误日志都拿不到。
3.2 完整 provisioning 流程
整理过的核心步骤如下,为了便于排查,我在每一步后面都加了检查点:
- 生成密钥对:用 STM32TrustedPackageCreator 的 OEMiROT/STiROT 配置工具生成 OEM 密钥对,输出为
.pem文件。检查点:确认是 RSA 或 ECDSA 算法,密钥长度和工程配置一致,不要混用。 - 生成固件包:把 OEMuROT 工程编译出的
.elf或者.hex转成带签名的.bin和头部描述文件。检查点:固件包里填写的启动地址必须和链接脚本一致。 - 配置 STM32TrustedPackageCreator 的 provisioning 脚本:指定目标芯片型号、烧录文件路径、密钥文件路径、OTP 配置字。
- 连接开发板执行 provisioning:通常用 ST-Link 连接,通过 STM32CubeProgrammer CLI 执行脚本。检查点:连接选项里选择的是"Under reset"模式还是"Hot plug"模式,前者更稳。
- 配置 RDP 等级:provisioning 过程中会把 RDP 提到 1 级或者 2 级。检查点:如果之前芯片是全新的 Level 0,这一步不会报错;如果是反复使用的板子,要先用 full erase 清回 Level 0。
- 烧录 DA 证书:如果后续还想调试,这一步不能省。检查点:证书文件路径是否正确,是否和芯片的 unique ID 绑定。
我这边第一遍只执行到第 5 步就停了,没有做第 6 步,导致后面想通过调试器回读校验时,直接被拒。如果你只是跑通 demo,也许觉得不烧 DA 也行,但作为产品化流程,这个必须一开始就养成习惯,否则后面变成砖的代价更高。
3.3 最容易混淆的配置项:启动地址和加载地址
接着上面说的"启动地址必须一致",这是 provisioning 失败的高频原因,必须单独拎出来讲。STiROT 在验证完 OEMuROT 之后,会把 PC 指针跳到 OEMuROT 的起始地址。这个地址在 TrustZone 工程里是写在链接脚本和工程配置里的,在生成固件包时也要填。如果两个地方不一致,轻则启动到未知区域,重则直接进入 HardFault。
我见过有人在 CubeIDE 里把链接脚本改到 0x08010000,但在 TrustedPackageCreator 的配置界面忘了同步,结果固件包头部写入的地址还是 0x08020000。由于 STiROT 校验的是整个镜像的签名和头部信息,地址不匹配时签名校验会失败,直接表现为 provisioning 失败。
所以排查 provisioning 问题,第一步永远是核对"链接脚本地址 == 固件包头部起始地址 == 工程宏定义里的地址"这个三角关系。
4. 失败现象和系统排查路径
终于到了正题。我在实验中碰到的失败日志长这样:
[STM32_Programmer] Error: STiROT_IMAGE_SIGNATURE_INVALID [STM32_Programmer] Error: Provisioning failed.这种报错只告诉你签名无效,没有告诉你具体哪个环节不对。我最初一度怀疑是密钥文件生成错了,后来发现问题不止一个,前前后后踩了三个坑。
4.1 排查步骤一:确认密钥类型和长度匹配
STiROT 示例工程默认支持 ECDSA P-256 和 RSA 2048。但你在 TrustedPackageCreator 里生成的密钥对,必须和 OTP 中烧录的公钥哈希使用的算法一致。如果工程配的是 ECDSA,你却在生成固件包时选了 RSA,签名校验必然失败。
我当时遇到的情况更隐蔽:密钥对是用老版本工具生成的,算法显示 P-256 没错,但后来重新安装新版本工具后,生成证书时的 DER 编码格式变了。工具升级后编码方式有差异,导致烧进去的公钥哈希和固件包里的公钥不一致。这种问题你光盯日志很难发现,必须把生成的公钥和固件包里的公钥逐一比对。
排查建议:直接把生成好的.pem文件用openssl ec -in xxx.pem -pubout -text打印出来,然后将固件包中嵌入的公钥哈希和 OTP 烧录的十六进制值对比,不一致就说明源头就错了。
目前业界对这类问题的描述总是一笔带过,我建议你在自己的笔记里记录下每次生成密钥的工具版本号,别小看这一点,排查时能省半天时间。
4.2 排查步骤二:检查 OTP 是否已被写入过
如果板子之前跑过一遍 provisioning,OTP 已经有值了,第二次再烧的时候因为 OTP 是一次性可编程的,写不进去就会报错。ST 的 OTP 虽然部分字节支持回读为 0,但如果配置字里某些位已经写入,后续擦除和重写是没办法的。这种场景下,普通用户跑 provisioning 脚本会得到一个无头无尾的错误,具体表现为卡在写 OTP 那一步。
解决办法是在 provisioning 前先用 CubeProgrammer 检查 OTP 状态。命令行下可以这样查:
STM32_Programmer_CLI -c port=SWD mode=UR -otp read如果看到0xFFFF这类值说明还是干净的,如果已经有非全 F 的数值并且和你的配置预期不一致,就要认真判断是继续覆盖还是直接换一片。因为 OTP 是不可逆的,与其在这里折腾,不如换板子或者换芯片,时间成本更低。
4.3 排查步骤三:RDP 等级导致的连不上问题
前两种失败都还能连上芯片,最麻烦的是 RDP 等级被改成 Level 2 之后,调试接口彻底锁死。Level 2 下 SWD 和 JTAG 都被禁,ST-Link 无法连接,此时你看到的现象就是 provisioning 脚本中途报错,然后板子完全失去响应。
我在调试中一度把 RDP 设成 Level 2 测试安全等级,结果发现一个配置遗漏导致后续烧录无法继续。这种情况下唯一能做的恢复操作是使用 STM32CubeProgrammer 的"Remove protection"功能,通过 DA 证书认证后降级到 Level 0 或 Level 1。如果你没有提前烧录 DA 证书,就只能换芯片或者使用 ST 的特殊恢复流程,工程开发期建议不要轻易尝试 Level 2。
4.4 排查步骤四:串口模式下的时序问题
如果你用的是 UART 烧录方式而不是 SWD,还容易出现时序问题。provisioning 脚本里如果启用了 握手等待,芯片复位后必须在指定时间内发送握手命令,否则命令超时会导致流程中断。这不是配置错误,但对新手很像配置错误,因为日志里会报一个TIMEOUT后缀的错误,容易误导排查方向。
排查建议:优先使用 SWD 模式进行 provisioning,USB 转串口的电平适配、线路延迟问题可以全部绕开。只有产品量产阶段才建议用 UART 烧录,并且需要针对产线环境调整时序参数。
5. 逐步复现与解决方案:我的完整执行过程
为了让你有个可以直接参考的对照,我把我最终成功 provisioning 的完整过程写出来,每一步的操作和结果都描述清楚。这个过程其实只用了一次就成功,原因是我提前把所有准备工作做足了。
5.1 准备阶段:从零生成密钥和固件
首先在 STM32TrustedPackageCreator 的 OEMiROT 配置界面,选择工程对应的.ioc文件,工具会自动识别芯片型号和工程配置。生成密钥对时选ECDSA_P256,输出路径放到工程目录下新建的Keys文件夹里。
固件编译我用的是 STM32CubeIDE,编译出 OEMuROT 的.elf文件后,在 TrustedPackageCreator 的 "Images" 界面添加固件包,软件会自动解析出镜像的加载地址。这里我特意和.icf链接脚本做了对比,确认无误。然后在 "Output" 中生成 provisioning 脚本,工具会生成一个.bat和.sh脚本,以及一个.xml描述文件。
5.2 执行阶段:把一切先用命令行走一遍
第一次跑脚本失败后,我没再冲动地去试图形化界面,而是改用命令行逐步执行,这样每一行命令的返回值都能明确看到。以下是关键命令,其中obk用于把 OEM 公钥哈希写入 OTP,wps设置写保护,ob写入选项字节。
# 擦除用户 flash 和系统 flash(注意:会清掉所有用户数据) STM32_Programmer_CLI -c port=SWD mode=UR -e all # 烧录 STiROT 固件(系统 flash 区域) STM32_Programmer_CLI -c port=SWD mode=UR -w stirot.bin 0x0FF00000 # 烧录 OEMuROT 固件(用户 flash 区域) STM32_Programmer_CLI -c port=SWD mode=UR -w oemurot.bin 0x08000000 # 写入 OEM 公钥哈希到 OTP STM32_Programmer_CLI -c port=SWD mode=UR -obk oem_key_256bit.obk # 设置 RDP 等级和保护属性 STM32_Programmer_CLI -c port=SWD mode=UR -ob RDP=0xBE这些命令看起来很简单,但要注意区分 STiROT 的烧录地址和 OEMuROT 的烧录地址。不同版本的示例工程地址可能不同,你务必以实际生成的.stirot_cfg.xml文件为准,不要照抄网上的教程。如果在烧 STiROT 时写错了地址,可能直接导致后续无法启动。
5.3 配置 DA 证书:避免变成砖的后悔药
我在执行上面的步骤之后,紧接着做了 DA 证书的烧录。这一步在官方脚本里是包含的,但如果你手动执行,很容易漏掉。DA 证书的作用是以后可以通过它来解除保护,回到 Level 0,便于开发调试。
DA 证书生成也在 TrustedPackageCreator 里,需要关联芯片的 unique ID。烧录命令大致如下:
STM32_Programmer_CLI -c port=SWD mode=UR -da da_cert.der我的建议是:不要只在量产前才烧 DA,开发阶段就把它烧好。因为你在调试过程中可能会反复修改密钥、反复调整配置,一旦把 RDP 等级设置得过高,没有 DA 就只能换芯片,每一片 H573 价格不低,这成本没必要自己扛。
5.4 成功判定:如何确认 provisioning 真的成功了
provisioning 成功之后,我在 CubeProgrammer 里读取了 OTP 和选项字节,确认 RDP 等级、WRP 区域和公钥哈希都和配置一致。然后给板子重新上电,观察串口输出。示例工程里 OLEDuROT 阶段的串口会打印一条启动日志,看到这个日志就说明 STiROT 校验通过了,OEMuROT 启动成功。
第一次正确配置后,我实测的串口输出如下(这是示例工程默认输出,具体内容因工程而异):
== STiROT Boot success == == OEMuROT Boot success == User App start...如果在调试器里还看到DBGMCU相关的异常,说明配置的 debug 权限和 DA 证书不一致,需要回到上一节检查调试接口设置。
6. 常见问题速查:给后来者的一份 checklist
这一节内容是我这次踩坑之后整理的速查表,适合贴在工位旁边,也可以直接写进项目文档里。遇到 provisioning 失败时,按顺序逐条排查比漫无目的地改配置要高效得多。
| 现象 | 大概率原因 | 解决方式 |
|---|---|---|
STiROT_IMAGE_SIGNATURE_INVALID | 密钥算法不匹配或公钥哈希不一致 | 核对密钥类型、比较公钥哈希 |
| 卡在 OTP 写入步骤 | OTP 已被写过或芯片非全新 | 读取 OTP 状态,考虑换芯片 |
连接报错No STM32 target found | RDP 已成 Level 2 或接线异常 | 换一个芯片,或通过 DA 恢复 |
UART 模式报TIMEOUT | 握手命令时序不匹配 | 使用 SWD 模式或调整时序参数 |
| 上电后无输出 | 启动地址错误或镜像链接地址不匹配 | 核对链接脚本和固件包头部地址 |
| 烧录后无法二次烧录 | 写保护配置过严 | 检查 WRP 设置,必要时回 Level 0 |
在这个基础上,我再额外补充几条经验:
- 不要用同一块板子反复做 provisioning 实验。OTP 是不可逆的,最好的做法是准备 3-5 块板子,分为"开发随意烧板"和"验证正式 provisioning 板"两类。
- 每完成一步就立刻读取状态确认,不要攒到最后一起验证。这样可以帮你定位到具体哪一步失败。
- 保持固定的软件版本组合。有时候你升级了 CubeProgrammer,但工程里旧的证书工具生成的配置格式不兼容,就会产生上面提到的"版本坑"。
7. 批量生产视角下的 provisioning 注意点
说完了开发阶段的排障,最后补充一个从产品化角度容易忽略的点。ST 官方示例里的 provisioning 脚本,本质上是一个开发流程,并不适合直接搬到产线。如果你的产品要量产,建议关注以下几个方面:
7.1 产线中的 provisioning 顺序
量产时,一般建议在贴片完成后先做 provisioning,再进行功能测试,因为 provisioning 会改变 RDP 等级和调试权限。如果先做功能测试后再 provisioning,可能会因为 RDP 升级导致部分测试项无法完成,比如边界扫描测试时调试接口被锁死。
注意,量产设备通常不烧录 DA 证书到 OTP 中,而是把 DA 证书保存在产线服务器里,只有返修时才通过临时烧录 DA 的方式解锁。如果每台设备都写入 DA 证书,等于给产品留了一个后门,安全等级会降低。
7.2 密钥管理策略
在开发阶段,你可以用工具随便生成一对密钥,但量产阶段,密钥管理必须严肃对待。完整的策略包括:
- 私钥不出产线服务器,加密存储;
- 每批次使用不同的 OEM 密钥对,并不定期轮换;
- 密钥文件严格按权限管控,不进入代码仓库;
- 公钥哈希可以预埋在固件和产线脚本里,但私钥绝对不能出现在产线工人的电脑上。
这里强调一下,provisioning 过程中用的密钥和固件包,必须通过安全的文件传输通道发送给产线设备,不能在工厂内网里传输时随意暴露。ST 有配套的密钥管理方案,但具体落地仍然取决于你们公司的安全规范。
7.3 Provisioning 失败后的产线处理策略
产线环境比实验室残酷得多,可能遇到芯片个体差异、电压波动、连接器接触不良等问题。所以产线 provisioning 一定要设自动重试和失败隔离机制,失败的产品自动进入返修通道,而不是人工反复重新烧录,以免把问题扩大。
返修通道通常需要用 DA 证书解锁,解锁后重新 provisioning。这也解释了为什么前面说"开发阶段也要烧 DA",因为你不光要在实验室调试,产线的返修流程同样需要它。
8. 最后分享一个排查小技巧
这次调试过程中对我帮助最大的一个技巧,是用STiROT_CFG.xml文件中的校验值来反向核对固件包。TrustedPackageCreator 生成固件包时,会在配置 xml 里附带一串镜像哈希和密钥哈希,你可以用 Python 快速解析并和 OTP 里实际读出的值对比,确认在哪个环节出了问题。
import hashlib import sys # 读取固件包二进制 firmware = open(sys.argv[1], 'rb').read() hash_value = hashlib.sha256(firmware).hexdigest() # 和 OTP 中读到的公钥哈希进行对比 expected_hash = sys.argv[2].lower() print("firmware hash:", hash_value) print("expected hash:", expected_hash) print("match:", hash_value == expected_hash)我在实际执行时,就这样排查出了公钥哈希不一致的问题。虽然 ST 的日志已经明确指向签名无效,但如果没有这一步对比,我可能还会在密钥长度、算法配置上浪费更多时间。
另外一个针对实操的小建议:在执行 provisioning 脚本时,先把日志输出重定向到文件,比如加上> provision_log.txt 2>&1,这样即使出错,也能回看完整日志,不会因为控制台滚动太快错过关键信息。这次我就是靠日志里的一行OTP not empty判断出芯片已经被写过。
总的体会是,STiROT + OEMuROT 这套机制设计得足够细,留给用户按需配置的空间大,但代价就是任何一环不匹配都会导致 provisioning 失败。遇到问题别慌,先把钥匙链理清楚,再按排查清单逐项检查,最终都能解决。希望这篇能帮你少走几天弯路。