尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H5安全启动后PKA初始化失败?TRUSTZONE外设隔离排查与修复

STM32H5安全启动后PKA初始化失败?TRUSTZONE外设隔离排查与修复 从板子切到正式安全启动的那一刻开始我的签名验证代码就死了。OPEN状态跑得好好的固件在iRoT-Provisioned下调用PKA做ECDSA校验永远卡在初始化检查那一行——读回来的SR.INITOK始终是0。这个问题的诡异之处在于代码一行没改唯一的区别就是设备状态从OPEN切到了iRoT-Provisioned。如果你也在ST新一代带安全启动的MCU上做固件签名校验或者正在和STiRoT、PKA、产品状态字这些东西打交道我这篇排查记录应该能帮你省下好几个晚上。这是一个典型的“安全策略生效后外设可用性发生跳变”的问题。表面看是PKA不干活实际是启动链路上某个环节把PKA的初始化条件给掐了。下面我按排查顺序把整件事拆开讲包括现象复现、STiRoT启动链的关键机制、从软件到硬件的完整排除过程以及最终的修复方案和几条工程建议。1. 现象签名校验在iRoT-Provisioned下失灵SR.INITOK一直读回01.1 项目背景和复现路径我这边是一个基于STM32H5系列的固件安全升级方案。整个链路需要满足芯片由STiRoTST immutable Root of Trust做一级启动校验应用固件中再用PKAPublic Key Accelerator对升级包做ECDSA P-256签名验证。这样既能保证启动链条可信又能在运行时验证每一包升级数据。硬件平台是一块自研板主控选了带TrustZone和安全启动功能的型号调试器用的ST-LINK开发环境是STM32CubeIDE 最新版的STM32CubeH5固件包。软件这边PKA的驱动直接用HAL层接口初始化流程大致是/* PKA 初始化开时钟、释放复位、等待 SR.INITOK */ __HAL_RCC_PKA_CLK_ENABLE(); __HAL_RCC_PKA_FORCE_RESET(); __HAL_RCC_PKA_RELEASE_RESET(); PKA_HandleTypeDef hpka {0}; hpka.Instance PKA; if (HAL_PKA_Init(hpka) ! HAL_OK) { /* 卡在这附近 */ }在OPEN状态下这段代码执行得非常干净HAL_PKA_Init正常返回后续HAL_PKA_ECDSA_Verify跑完一轮签名验证大概也就几十毫秒。可是把设备状态切到iRoT-Provisioned之后程序走到HAL_PKA_Init里面就出问题了。HAL库在初始化PKA时会等待内部SRAM初始化完成具体看PKA-SR寄存器中的SR.INITOK标志位。在iRoT-Provisioned下这个标志位永远不置1导致HAL库一直超时最终返回HAL_TIMEOUT。1.2 直观现象和表面排查我一开始以为是自己代码里漏了什么于是做了几轮常规检查第一确认PKA时钟是不是开了。从RCC寄存器读回来PKAEN位确实已经是1时钟门控没问题。第二确认PKA是否被强制复位。复位寄存器读回来PKA的复位释放位也是正常的模块应该不在复位状态。第三确认代码是否跑在异常状态。整个验证流程没有触发HardFault也没有总线错误这一点很关键——说明非安全代码对PKA的访问没有被硬件直接拦下来。这三轮检查全部正常但SR.INITOK就是不置位。后来我在调试器里直接读PKA的任一控制寄存器发现一个有意思的现象写入操作似乎没有生效。比如往PKA的某个控制位写1再读回来还是复位值。这种现象和“外设根本没收到时钟”很像但时钟明明开了。这里就引出一个很重要的怀疑方向PKA模块虽然时钟门控打开了但它是否被允许接收来自当前执行环境的访问或者说它在TrustZone里到底被划到了安全域还是非安全域1.3 为什么不能用软件软算法绕过可能有人会问PKA初始化不了直接用软件实现ECDSA不就行了这个思路在开发阶段可以但产品上完全走不通。首先我们的安全方案要求在安全启动信任链里必须使用硬件密码引擎软件实现无法通过功能安全审计。其次PKA执行点乘运算比软件实现快一个数量级以上升级包校验如果走软件算法整个交互超时可能扛不住。更重要的是这个问题本质上是外设分配问题今天能绕过PKA明天可能连Flash、OTP都绕不过去。所以必须从根上解决。2. STiRoT启动链和PKA的初始化前提2.1 STiRoT到底做了什么STiRoT是ST提供的不可变信任根固化在芯片ROM里。上电后CPU先执行STiRoT它会检查当前设备生命周期状态Life Cycle然后根据状态决定执行策略。简单来说OPEN状态属于开发调试状态安全策略极其宽松iRoT-Provisioned状态是正式启用安全启动校验的中间状态此时STiRoT会严格校验第一级固件镜像的签名和身份同时也会按预设的安全配置把芯片内的各种隔离、访问控制、时钟状态初始化好。这里需要特别注意的是STiRoT在iRoT-Provisioned状态下做的事情不只是校验签名。它还会根据烧录在OTP里的配置对TrustZone隔离边界、外设安全属性、系统总线权限做一轮初始化。这轮初始化完成后用户固件接手时的硬件环境和OPEN状态下随手reset出来的硬件环境是完全不同的。很多“OPEN好好的安全状态就出问题”的bug根源就在这一步。2.2 PKA模块要工作依赖哪几件事PKA是一个专用非对称密码加速器工作依赖三个前提条件时钟PKA的kernel clock和总线接口时钟都由RCC统一管理必须在正常工作频率下使能。复位状态PKA需要从复位中释放否则寄存器无法访问操作根本无法启动。内部RAM初始化PKA内部有用于存放运算中间结果的专用SRAM模块上电后会自动执行RAM初始化初始化完成会把SR.INITOK位置1。只有这个位为1PKA才会接受运算命令。这三个条件里前两个看着简单但在带TrustZone的芯片上会引入一个额外变量——访问权限。即使时钟和复位都正常如果当前执行环境没有权限访问PKA对应的寄存器和内存映射区域那么写控制寄存器就会像写空气一样毫无动静。PKA模块的RAM初始化又是靠模块自身状态机完成的如果模块连“被启动/被配置”的机会都没有它当然永远不会去设置SR.INITOK。2.3 OPEN和iRoT-Provisioned的核心差异把两种状态下的安全配置摆在一起看差异非常明显维度OPENiRoT-Provisioned调试器访问完全开放全地址可读写受安全策略限制只允许在规定的安全上下文访问TrustZone隔离基本处于bypass或宽松模式强制生效外设按安全/非安全属性分配外设访问权限用户代码可直接操作绝大多数外设只有被配置为non-secure属性的外设非安全代码才能直接操作时钟/复位默认状态启动到用户代码时大部分外设已放开部分外设可能被STiRoT保持复位或时钟门控固件校验不做强制校验或者仅做标记强制校验校验失败拒绝启动从这张表很容易得到一个判断SR.INITOK不置位不是PKA本身坏了而是它在iRoT-Provisioned状态下根本没有获得“被非安全代码启动”的资格。换句话说PKA大概率被划到了安全域而非安全应用代码的写操作被硬件静默忽略了。3. 一条完整的排查链路从调用、寄存器、时钟再到隔离域3.1 第一步先排除调用方式的问题定位这类问题我习惯从最外层往最里层剥。先拉出完整调用栈确认当前CPU确实运行在非安全模式。如果代码在非安全世界跑那么访问安全外设时硬件可能做两件事要么触发总线错误要么静默返回失败。STM32H5系列在多数总线互连场景下非安全写安全地址会直接产生总线错误但我们这里没有进HardFault所以得考虑第二种可能——有些外设的寄存器访问本身就是“写后读回无效”这和外设实现有关系。为了排除HAL库封装对问题的干扰我直接用寄存器操作/* 直接读 SR 看 INITOK读 CR 看模块状态 */ uint32_t sr PKA-SR; uint32_t cr PKA-CR; /* 尝试写一个控制位然后读回 */ PKA-CR | 0x1; uint32_t cr_after PKA-CR;在OPEN状态下写控制位后读回是能读到变更的。在iRoT-Provisioned状态写1再读回来还是复位值。这说明问题出在“访问能不能真正落到PKA内部”而不是HAL库的时序问题。3.2 第二步时钟和复位状态再确认最初检查时钟是看RCC的使能位但这里有个陷阱带安全属性的时钟门控可能由安全域控制。非安全代码虽然读RCC寄存器能看到某个使能位是1但如果实际的时钟门控信号被安全策略拉住了模块照样收不到时钟。具体表现就是寄存器访问像“假死”——读得到默认值写不进去。复位的检查也一样。RCC里的复位释放位如果由安全侧管理非安全代码写这个位不会真正生效。我后来用调试器对比了OPEN和iRoT-Provisioned两种状态下PKA模块的复位状态。发现切到iRoT-Provisioned后PKA一直处于外部复位状态无论非安全代码怎么释放它都保持复位。这个观察基本锁定了方向PKA的复位控制被安全隔离策略接管了。3.3 第三步查TrustZone隔离配置接下来就是确认PKA到底被划到了哪个安全域。在这类芯片上外设安全属性由安全外设分配控制器不同系列叫GTZC、TZSC、ETZPC但作用类似管理。正常流程下安全侧代码可以通过该控制器把某个外设标记为non-secure或者标记为secure。我直接在调试器里读了PKA对应的安全配置位结果确实不出所料PKA被标记为secure属性。这就解释了为什么PKA不启动也解释了为什么寄存器写操作被忽略——非安全代码对一个secure外设的控制寄存器写入会被总线矩阵当作非法访问丢弃。芯片没有触发HardFault是因为这次访问没有跨到安全地址空间而是在安全属性墙上被静默消化了。3.4 第四步确认设备状态字和启动配置来源最后还要确认一件事PKA被划到secure是“STiRoT默认行为”还是“我的配置镜像里写死的”。我用OTP读取工具看了烧录在OTP里的启动配置表确认里面确实有“PKA分配为secure”这一项。同时确认当前设备生命周期状态就是iRoT-Provisioned而不是更激进的CLOSED或LOCKED。如果是CLOSED那安全配置在OTP里被锁定只能通过回退流程重新配置而iRoT-Provisioned本身还保留了重新配置安全属性的弹性这也为修复留了空间。到这里整个链路已经完整了STiRoT启动后根据OTP配置把PKA标记为secureCPU随后跳到非安全应用非安全应用尝试初始化PKA时由于目标外设是secure所有写控制寄存器的操作都被丢弃PKA模块的状态机根本没有启动SR.INITOK自然不可能置位。4. 根因定性安全属性把PKA的初始化机会掐死了4.1 为什么secure属性会让SR.INITOK永远为0PKA模块的SR.INITOK置位不是软件直接写的而是PKA内部状态机在完成RAM初始化后自动置的。状态机启动的前提是模块退出复位并且时钟稳定。复位释放和时钟门控由RCC控制而RCC对外设的控制信号又受TrustZone属性约束。当PKA被设为secure外设时非安全代码访问PKA控制寄存器会被总线矩阵拦截。被拦截的写操作不会进入PKA模块内部的复位释放信号来自安全侧的配置非安全代码无法改变。模块始终处于复位或被禁止状态内部状态机没有运行条件。SR.INITOK作为一个只读状态位自然永远停留在复位默认值0。这就像一台机器电源开关握在别人手里你在操作面板上怎么按启动按钮都没用。面板上的指示灯永远不亮不是灯坏了是机器压根没拿到电。4.2 OPEN下为什么能跑OPEN状态下STiRoT的启动策略不同。在OPEN里安全策略基本不生效外设隔离属性默认是开放的PKA要么被配置为non-secure要么安全隔离本身就不拦截非法访问。此时非安全代码能直接操作PKA模块能正常退出复位、收到时钟、启动RAM初始化SR.INITOK自然就能置位。这也是很多开发者的第一反应“代码又没错”的原因。代码确实没错问题出在运行环境的访问策略变了。OPEN状态像你在自己家里怎么摆弄工具都行iRoT-Provisioned像进了需要权限的房间你没钥匙工具放在玻璃柜里看得见摸不着。4.3 两条修复路径怎么选既然根因是PKA被划到了secure那修复思路就两条要么把PKA改成non-secure让非安全应用能直接操作要么保持PKA secure但把PKA的初始化和调用放到安全侧通过安全调用接口提供给非安全应用。这两条路径在产品形态上有明显取舍。路径A把PKA改为non-secure。优点是改动最小非安全应用代码保持原样HAL调用直接可用。缺点是PKA的配置寄存器暴露给非安全世界安全性差一些。对于“运行时验证外部升级包签名”这个场景如果攻击者能够拿到改签名的能力或者干扰PKA计算流程那整个升级校验体系就会被攻破。所以A方案适合安全要求不高的产品。路径BPKA保持secure在安全侧初始化并封装PKA操作。优点是把密钥材料、签名计算过程都留在安全世界非安全侧可以请求“计算这个哈希的签名验证”但看不到中间数据也无法篡改计算流程。缺点是需要在安全侧写一套PKA服务代码包括初始化、任务分发、结果回传通过NSCNon-Secure Callable接口暴露给非安全侧。这个方案改动量更大但作为安全启动方案更严谨。我最后选的是路径B理由很简单我们已经上了一整套安全启动就是在对抗代码篡改和固件伪造如果因为图省事把PKA改成non-secure等于把整个信任链的“签名验证”环节敞开给攻击者那前面做的STiRoT、固件签名、防回滚全都白费。5. 修复落地和验证结果5.1 安全侧PKA服务实现我实现的思路是在安全侧启动早期由secure代码完成PKA的时钟使能、复位释放和RAM初始化等待。然后把PKA封装成一个小型服务通过NSC接口对外提供“签名校验”能力。安全侧初始化/* 安全侧执行此时运行在secure privilege级别 */ void Secure_PKA_Init(void) { __HAL_RCC_PKA_CLK_ENABLE(); __HAL_RCC_PKA_FORCE_RESET(); __HAL_RCC_PKA_RELEASE_RESET(); /* 等待 PKA 内部 RAM 初始化完成 */ while ((PKA-SR PKA_SR_INITOK) 0U) { /* 如果超时说明 PKA 时钟或复位异常 */ } }对外暴露的NSC接口只做三件事接收输入摘要和签名、调用PKA做验签、返回结果。中间计算不经过非安全内存不暴露PKA寄存器操作。/* Non-Secure Callable 接口非安全侧通过它请求PKA验签 */ uint32_t SECURE_PKA_Verify(uint8_t *hash, uint8_t *signature, size_t len) { /* 安全检查输入指针必须在非安全内存范围内 */ /* 执行 PKA ECDSA Verify */ /* 返回验证结果 */ }非安全侧的原始代码几乎不用改只需要把原来的HAL_PKA_ECDSA_Verify调用替换成SECURE_PKA_Verify调用。5.2 处理非安全内存访问的安全校验这里有个容易被忽略的安全细节。NSC接口接收的指针来自非安全侧安全侧代码在处理这些指针前必须确认它们指向有效内存否则容易引入内存泄漏或者被恶意调用者传入伪造缓冲区。我这里是先调用TrustZone提供的检查函数确认地址在非安全SRAM范围内、长度没过界然后才拷贝进安全侧工作缓冲区。这一步不做的话即使PKA保持secure整体安全性也有缺口。攻击者可以构造一个看似合理的输入让安全侧读越界数据或写坏内存污染PKA运算结果。5.3 验证结果和性能对比修复后我在iRoT-Provisioned状态下跑了完整验证PKA初始化安全侧启动时完成SR.INITOK正常置位。非安全侧调用通过NSC接口调用验签能够正常返回通过/失败。升级包验证完整升级流程可以跑通。异常输入测试故意构造错误签名和越界指针安全侧正确拒绝没有崩溃。性能上由于多了一层安全调用和内存拷贝单次PKA验签比直接用HAL调用多了几微秒对整个升级流程来说可以忽略。更关键的是现在PKA的关键寄存器只有安全侧能碰非安全侧即使被攻破也拿不到任何中间密钥和计算数据。5.4 给做安全启动固件的人几条排查建议第一遇到“OPEN下好好的安全状态下坏了”的问题先别急着翻业务代码直接看外设的安全属性分配。这是这类问题最常见的根因检查顺序应该是外设安全属性 - 时钟/复位门控 - 访问权限。第二调试早期尽量保留一个串口或日志通道把安全侧初始化的关键步骤打印出来哪怕只有一个状态码。这次如果安全侧初始化时能输出SR.INITOK的状态我根本不用翻那么多寄存器。第三如果使用安全侧封装方案NSC接口的参数校验一定要做扎实。安全世界和不安全世界的边界最容易出漏洞的地方就是跨边界传递指针。第四预算允许的话尽早切到iRoT-Provisioned状态联调不要等到开发末期才切状态。安全启动相关的bug越晚暴露越难排查因为此时你已经默认代码是好的会绕很多弯路。6. 最后说几句实在话我这次的问题从现象产生到定位根因前后折腾了两天。回头复盘最核心的教训只有一个在带TrustZone和安全启动的平台上任何一个外设的可用性都不是“代码里开了时钟就能用”这么简单。安全属性、隔离策略、设备生命周期状态这三样东西叠加在一起会把很多在普通MCU上不成问题的问题放大成疑难杂症。如果你也遇到了类似情况建议第一步先查安全外设分配表确认目标外设在当前状态下归属哪个域。这一步比翻调用栈、比测时钟都优先。把“它是谁的”搞清楚了再谈“它能不能跑”。另外一个建议是安全启动的固件架构最好从一开始就把安全侧服务和非安全侧业务分清楚。不要让业务代码直接操作所有外设尤其是密码引擎、OTP、时钟安全相关的关键外设。该封装的安全调用越早封装越好。等出了问题再来拆代码耦合度会让你改得很痛苦。这次幸好PKA只是个独立外设如果后面有更复杂的多外设联动事情会更麻烦。在安全启动这条路上OPEN状态只是给了你一个比较舒服的开发环境真正的战场永远是那些安全策略完全生效的状态。希望这篇记录能帮你少踩几个坑。
返回列表