news 2026/9/12 2:02:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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一直读回0

1.1 项目背景和复现路径

我这边是一个基于STM32H5系列的固件安全升级方案。整个链路需要满足:芯片由STiRoT(ST immutable Root of Trust)做一级启动校验,应用固件中再用PKA(Public 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。只有这个位为1,PKA才会接受运算命令。

这三个条件里,前两个看着简单,但在带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标记为secure,CPU随后跳到非安全应用,非安全应用尝试初始化PKA时,由于目标外设是secure,所有写控制寄存器的操作都被丢弃,PKA模块的状态机根本没有启动,SR.INITOK自然不可能置位。

4. 根因定性:安全属性把PKA的初始化机会掐死了

4.1 为什么secure属性会让SR.INITOK永远为0

PKA模块的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方案适合安全要求不高的产品。

路径B:PKA保持secure,在安全侧初始化并封装PKA操作。优点是把密钥材料、签名计算过程都留在安全世界,非安全侧可以请求“计算这个哈希的签名验证”,但看不到中间数据,也无法篡改计算流程。缺点是需要在安全侧写一套PKA服务代码,包括初始化、任务分发、结果回传,通过NSC(Non-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状态只是给了你一个比较舒服的开发环境,真正的战场永远是那些安全策略完全生效的状态。希望这篇记录能帮你少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 12:38:57

SSH框架CRM客户关系管理系统课设开发全解析

简介:本资源是一套基于Java技术栈开发的客户关系管理系统(CRM)完整实现方案,面向高校计算机专业课程设计、毕业设计及Java Web开发初学者,解决企业客户信息管理、销售流程跟踪与服务响应等典型业务场景建模与系统落地问…

作者头像 李华
网站建设 2026/9/2 3:42:48

基于Hadoop的房价数据分析系统:从爬虫到可视化的完整毕设指南

每年毕业季,计算机专业的同学都会面对同一个问题:毕业设计到底选什么题目。选得太简单,答辩时容易被老师追问到无话可说;选得太复杂,又可能做到一半发现时间根本不够。如果你正在为这个问题发愁,同时希望毕…

作者头像 李华
网站建设 2026/9/12 19:27:41

浙江研究生自主招生学校盘点:报考细节梳理,浙江万里学院报考要点解析

摘要在浙江研究生自主招生报考阶段,不少考生会把目光投向具备相关招生资质的民办院校,浙江万里学院是其中关注度较高的一所。研究生自主招生区别于全国统考,报考条件、考核形式、录取规则都有着自身特点。本文梳理浙江地区开展研究生自主招生…

作者头像 李华
网站建设 2026/9/12 13:36:21

postgresql数据库中~和like和ilike的区别

~(暂且叫他波浪号吧) 和 LIKE 和 ILIKE 操作符可以模糊匹配字符串,LIKE是一般用法,ILIKE匹配时则不区分字符串的大小写,~ 波浪号则可以使用正则匹配。LIKE和 ILIKE它们需要结合通配符使用,下面介绍两种常用…

作者头像 李华
网站建设 2026/9/12 16:15:01

企业级Agent记忆系统设计:从Context管理到LangGraph长期记忆

当你的 Agent 在生产环境跑了一个月,用户开始问出这样的问题:“你上次不是说帮我处理过工单吗?”“我不记得你是老客户了?”这时候你才会意识到,Agent 不是模型不够聪明,而是没有记忆。很多团队把 Agent 做…

作者头像 李华