ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32H573安全启动provisioning排查:STiROT与OEMuROT信任链解析

STM32H573安全启动provisioning排查:STiROT与OEMuROT信任链解析 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 是否已被写入过如果板子之前跑过一遍 provisioningOTP 已经有值了第二次再烧的时候因为 OTP 是一次性可编程的写不进去就会报错。ST 的 OTP 虽然部分字节支持回读为 0但如果配置字里某些位已经写入后续擦除和重写是没办法的。这种场景下普通用户跑 provisioning 脚本会得到一个无头无尾的错误具体表现为卡在写 OTP 那一步。解决办法是在 provisioning 前先用 CubeProgrammer 检查 OTP 状态。命令行下可以这样查STM32_Programmer_CLI -c portSWD modeUR -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 模式进行 provisioningUSB 转串口的电平适配、线路延迟问题可以全部绕开。只有产品量产阶段才建议用 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 公钥哈希写入 OTPwps设置写保护ob写入选项字节。# 擦除用户 flash 和系统 flash注意会清掉所有用户数据 STM32_Programmer_CLI -c portSWD modeUR -e all # 烧录 STiROT 固件系统 flash 区域 STM32_Programmer_CLI -c portSWD modeUR -w stirot.bin 0x0FF00000 # 烧录 OEMuROT 固件用户 flash 区域 STM32_Programmer_CLI -c portSWD modeUR -w oemurot.bin 0x08000000 # 写入 OEM 公钥哈希到 OTP STM32_Programmer_CLI -c portSWD modeUR -obk oem_key_256bit.obk # 设置 RDP 等级和保护属性 STM32_Programmer_CLI -c portSWD modeUR -ob RDP0xBE这些命令看起来很简单但要注意区分 STiROT 的烧录地址和 OEMuROT 的烧录地址。不同版本的示例工程地址可能不同你务必以实际生成的.stirot_cfg.xml文件为准不要照抄网上的教程。如果在烧 STiROT 时写错了地址可能直接导致后续无法启动。5.3 配置 DA 证书避免变成砖的后悔药我在执行上面的步骤之后紧接着做了 DA 证书的烧录。这一步在官方脚本里是包含的但如果你手动执行很容易漏掉。DA 证书的作用是以后可以通过它来解除保护回到 Level 0便于开发调试。DA 证书生成也在 TrustedPackageCreator 里需要关联芯片的 unique ID。烧录命令大致如下STM32_Programmer_CLI -c portSWD modeUR -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 foundRDP 已成 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 21这样即使出错也能回看完整日志不会因为控制台滚动太快错过关键信息。这次我就是靠日志里的一行OTP not empty判断出芯片已经被写过。总的体会是STiROT OEMuROT 这套机制设计得足够细留给用户按需配置的空间大但代价就是任何一环不匹配都会导致 provisioning 失败。遇到问题别慌先把钥匙链理清楚再按排查清单逐项检查最终都能解决。希望这篇能帮你少走几天弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进