如果你在 STM32H5 上同时碰过安全启动和 PKA,大概率能体会我下面要讲的崩溃场景:代码在 OPEN 状态跑得稳稳当当,PKA 一使能,SR.INITOK 很快就拉起来;结果把设备切到 iRoT-Provisioned 状态、通过 STiRoT 引导启动之后,同样的初始化代码,SR.INITOK 就是一直停在 0,后面所有依赖 PKA 的运算全部卡死。为这个问题我熬了好几个晚上,最终发现根子不在 PKA 外设本身,而是安全生命周期和启动路径改变了外设的访问环境。这篇文章把完整背景、分析过程、调试方法和最终解决方案整理出来,给同样在安全启动场景下做密码加速的朋友一个参考。如果你现在正处于“OPEN 正常,provision 就翻车”的阶段,这篇文章应该能帮你省下不少排查时间。
1. 问题复现:同一个 PKA 初始化,两种生命周期两种结局
1.1 STiRoT 和生命周期状态到底改变了什么
STiRoT 是 ST 提供的一套不可变信任根方案,它固定烧录在芯片内部,职责是上电后先于用户代码执行,完成固件完整性校验、安全启动和生命周期状态管理。在 STM32H5 这类带 TrustZone 的芯片上,设备不是简单的“开锁/关锁”二态,而是一条完整的状态链:OPEN、PROVISIONING、iRoT-Provisioned、CLOSED、LOCKED 等。每一个状态对调试端口、外设访问、Flash 读写都有不同的权限约束。
我在实际项目里用的是一块 STM32H573 开发板,带原生 STiRoT。最初阶段芯片处于出厂 OPEN 状态,这时候所有调试口、所有安全属性基本都是默认宽松的,内存和外设的 Secure/Non-Secure 划分可以由用户代码随意配置,也随时可以切回去。所以我最初写 PKA 初始化代码、验签逻辑、加速运算,全部在 OPEN 状态下完成,所有功能测试全过,完全没有意识到后面还有坑。
等我把设备通过 STM32CubeProgrammer 执行 provisioning 流程,把 STiRoT 的配置写进 OTP,并把生命周期切到 iRoT-Provisioned 之后,问题就出现了。此时芯片每次上电都会先走 STiRoT 启动流程,校验用户固件,通过后再跳转到用户应用。代码流程没有任何改动,但 PKA 的状态就往奇怪的方向走了。可以这么理解:OPEN 状态相当于你在一间门窗全开的实验室里调设备,任何条件都能手动满足;而 iRoT-Provisioned 状态相当于房间已经按图纸完成了安防部署,部分区域开始限制进入,但你可能没拿到最新版图纸。
1.2 PKA 和 SR.INITOK 在系统里扮演什么角色
PKA 全称 Public Key Accelerator,是芯片上的公钥运算加速器,专门处理 RSA、ECC 这类大整数模运算。和软件实现相比,PKA 硬件加速在性能和功耗上都有明显优势,尤其在安全启动验签、固件签名校验、TLS 握手这些场景里几乎是刚需。
但 PKA 不是上电就能直接用的。它内部有一块用于大数运算的 RAM 和一套状态机,开始工作之前必须先完成初始化。初始化完成的标志就是状态寄存器 SR 里的 INITOK 位。正常情况下,软件使能 PKA、等待 SR.INITOK 置 1,然后才能往命令寄存器里写入要执行的运算命令。如果 INITOK 一直为 0,后续所有命令都会无效,可能表现为命令不执行、中断不来、结果寄存器没更新,甚至整个调用超时卡死。
在实际代码里,我最初的 PKA 初始化逻辑很简单:先使能时钟,然后写控制寄存器打开 PKA,轮询 SR 等待 INITOK。在 OPEN 状态下,这个流程几十个周期就能完成;在 iRoT-Provisioned 状态下,轮询走到超时也等不到 INITOK。当时我打印出来的 SR 值非常直观:INITOK 始终是 0,BUSY 位也在 0,看起来 PKA 就像根本没被激活。
1.3 精确的复现步骤和现场现象
为了让问题可复现,我记录了完整步骤。首先在 OPEN 状态烧录一个带 PKA 自检的用户固件,固件启动后依次执行 PKA 使能、INITOK 等待、一次 ECC 点乘计算,并把每步结果写到串口。这个固件在 OPEN 状态下输出全绿:INITOK 置位,点乘结果和软件参考值一致。然后把固件签名、配置 STiRoT、切到 iRoT-Provisioned,重新上电走安全启动。跳转到用户代码后,串口输出停在“PKA init timeout”,SR 寄存器的打印值固定为 0x00000000,INITOK 和 BUSY 全都不置位。
有意思的是,并不是整个芯片都出了问题,其他外设如 GPIO、UART、Flash 读写都正常,唯独 PKA 初始化不过。这说明问题不是系统级死锁,而是 PKA 相关的某条链路在 iRoT-Provisioned 状态下被改了状态。这个时候如果你只盯着 PKA 的寄存器看,会很迷茫,因为你看到的寄存器根本没有响应;必须跳到 RCC 时钟配置、安全属性配置、启动路径这三个维度去找差异。
2. 根因排查:从 SR.INITOK 的置位条件一路挖到安全边界
2.1 SR.INITOK 的置位依赖哪些前置条件
我排查问题从来不直接猜结论,而是先把“INITOK 置位到底依赖什么”捋清楚。从芯片设计角度看,PKA 要完成初始化并置位 INITOK,至少需要三个条件同时满足:第一,PKA 的时钟必须正常使能,并且时钟频率在规格范围内;第二,PKA 内部 RAM 必须对当前总线主设备可访问,PKA RAM 的初始化读写不能被打断;第三,PKA 不能处于复位状态,也不能处在某个被外部锁定的状态。
这三个条件在 OPEN 状态下默认都满足,所以代码不出问题。但在 STiRoT 启动路径下,条件可能被逐一破坏。尤其是第二点,在带 TrustZone 的芯片上,“可访问”不再只是物理上能否读写,还包含安全属性是否匹配。如果 PKA 被配置成 Secure 外设,而用户代码跑在 Non-Secure 世界,写进去的寄存器指令会被硬件过滤掉,看起来就是“写了个寂寞”。
我当时对照参考手册检查了 RCC 里的 PKA 安全属性配置位,发现一个关键现象:在 OPEN 状态下,该位默认值是 0,也就是 PKA 属于 Non-Secure 外设,Non-Secure 用户代码随便访问;而完成 STiRoT provisioning 并切到 iRoT-Provisioned 之后,同一个位被 STiRoT 的配置值改成了 1,PKA 被划到 Secure 域。而我的用户代码恰好运行在 Non-Secure 世界。这一下就把 PKA 的寄存器访问全部挡在门外了,INITOK 自然永远不会置位。
2.2 安全属性和生命周期状态如何影响外设访问
这里要展开讲一下 TrustZone 对普通外设的影响。STM32H5 的外设并非全都默认分配到 Non-Secure 域,很多外设可以通过 RCC 的安全配置寄存器或者 GTZC 的配置来决定它属于 Secure 还是 Non-Secure。访问者必须和该外设处于相同安全世界,才能正常操作。一旦不匹配,有两种常见表现:一种是比较直接的总线错误或者 HardFault;另一种更隐蔽,就是总线接口直接丢弃写操作,寄存器读出来总是复位值,代码不会崩溃但逻辑跑不通。PKA 遇到的问题就是后者。
另外,生命周期状态会锁存一部分安全配置。在 OPEN 状态,你可以随便翻转安全属性位,调试器也能读写一切;到了 iRoT-Provisioned 状态,STiRoT 会按照你在 provisioning 阶段烧进去的配置,在跳转用户代码之前就把外设安全分组设好。理论上这是让你提前规划好安全边界。但如果你的用户代码没有检查这些配置,它就会默认沿用开发时的假设,最终出现“OPEN 正常、provision 翻车”的经典局面。这不是 STiRoT 的 bug,更多是开发阶段没有把安全配置和软件初始化对齐。
2.3 时钟域和复位过程也不容忽视
除了安全属性,我还排查了时钟和复位链路。在 STiRoT 引导阶段,芯片会先跑一套由 STiRoT 配置决定的时钟树,跳转到用户代码后,用户代码有责任重新配置 RCC。如果你的工程直接用 CubeMX 生成的 SystemClock_Config,理论上会把 PKA 时钟重新配好。但我遇到的情况是,用户代码重新配置了时钟树,可是 RCC 里 PKA 的时钟使能位仍然是关闭状态。原因很可能是 CubeMX 的安全视角里默认认为 PKA 归 Secure 域管理,而我的 Non-Secure 工程生成的初始化代码里没有包含 PKA 时钟使能。
对于这类“时钟使能没执行”的问题,直接读 RCC 寄存器就能发现。我当时打开调试器看了 RCC 的 PKA 使能位,发现和 OPEN 状态下的 1 不同,provision 状态下这个位是 0。即便我在应用初始化里尝试打开,如果当前代码处于 Non-Secure 世界而该时钟控制位被安全锁定,写操作也无效。也就是说,安全属性问题可能同时影响外设本身和外设的时钟控制逻辑。
我还在调试中将 PKA 的复位控制位来了一次强制复位和释放复位,确认 PKA 能从这个操作中恢复。这个动作在 OPEN 下完全 OK,但在 iRoT-Provisioned 下,复位控制寄存器的写入同样受到安全属性限制,需要先打开对应时钟和访问权限。所以排查时要按“时钟 -> 复位 -> 外设安全位 -> 外设寄存器”的层次逐个确认,不要一上来就盯着 PKA 的 SR.
2.4 另一个隐形因素:STiRoT 可能已经用过 PKA 验签
还有一个容易被忽略的点:STiRoT 在安全启动过程中要做固件镜像的签名校验,而签名校验完全可能使用 PKA 硬件加速。也就是说,在你的用户代码接管 CPU 之前,PKA 已经被 STiRoT 初始化并使用过一轮。如果 STiRoT 在跳转前没有把 PKA 恢复到复位状态,那么 PKA 内部状态机可能停在某个中间状态,用户代码再次执行“初始化”时,PKA 并不会重新走一遍 RAM 初始化流程,INITOK 的置位条件就和冷启动时不太一样。
这和 OPEN 状态下的区别在于:OPEN 模式下芯片不会先走 STiRoT 安全引导,PKA 在用户代码执行前从未被碰过,所以一次干净初始化就能正常置位 INITOK。而在 iRoT-Provisioned 模式下,PKA 很可能带着上一手的状态。如果你不做 RCC 复位就重复初始化,就等于想在一个已经点着的发动机上再点一次火,当然不会有反应。
我在确认这个因素时做了个实验:在用户代码初始化 PKA 之前,先强制把 PKA 时钟关闭、复位拉起来再释放,彻底清掉 STiRoT 留下的状态,然后再使能 PKA。结果在同样 iRoT-Provisioned 状态下,INITOK 又能够正常置位了。这说明“STiRoT 预占用”确实是原因之一,但不一定是唯一原因。等到我再把安全属性也按正确配置调整后,问题才算彻底稳定解决。
2.5 把几个疑点汇总成一张问题定位图
把上面的排查思路整理一下,可以归纳成四个疑点:安全属性不匹配、时钟未使能、复位状态未清、PKA 被预占用。这四者不是互斥的,很可能是叠加出现。对于我这个项目,最终确认的根因是安全属性不匹配加上 PKA 处于被 STiRoT 用过的脏状态。时钟使能属于次生问题,因为 Non-Secure 代码写不了 Secure 侧的时钟控制位。
定位之后你会明白,这类问题的本质不是 PKA 初始化代码写得不合格,而是安全环境下“初始化”的含义变了。它不再只是简单操作外设寄存器,而是要先确认代码所在的安全世界、外设所属的安全世界、以及外设从启动到现在经历了什么。想清楚这一点,排查方向就不会跑偏。
3. 调试手法:在 iRoT-Provisioned 状态下怎么把现场查清楚
3.1 第一步:确认当前生命周期和调试端口权限
在 iRoT-Provisioned 状态下做调试,首先要确认你还能不能通过调试器看寄存器。不同 provisioning 配置对调试端口的策略不一样,有些允许 Non-Secure 调试,有些允许 Secure 调试,有些干脆把调试口关了。如果调试口被关,你需要提前在固件里预留串口打印和状态上报逻辑,否则切到受限状态后就没有任何观察手段。
我建议在 provisioning 之前先确认 STM32CubeProgrammer 里显示的当前生命周期,并且在固件里加一个启动信息打印:把芯片当前的调试配置、安全世界、关键 RCC 位都打印出来。这样即使切到 iRoT-Provisioned 后调试器连不上,你至少还能通过串口判断运行到哪个函数、哪一个寄存器没有置位。
如果你还能连上调试器,那么读取生命周期状态寄存器或者直接在 CubeProgrammer 的 Option Bytes 页面看状态是最快的。我那次是 Non-Secure 调试仍然可用,所以能直接在线看寄存器,这是排查顺利的关键。如果连不上,就得靠串口打印和最小化复现来缩小范围,会慢很多。
3.2 第二步:对比 OPEN 和 iRoT-Provisioned 下的关键寄存器
调试这类问题最有用的操作,是在两种状态下分别跑同一段代码,然后把 PKA 相关的所有寄存器都打印出来做对比。我对比的核心寄存器包括:RCC 的 PKA 时钟使能位、PKA 安全属性位、PKA 控制寄存器 CR、PKA 状态寄存器 SR。把两组值并排放到一起,差异一目了然。
以我的现场记录为例:OPEN 下 RCC 的 PKA 使能位是 1,安全属性位是 0,PKA 的 SR 最终变成 0x00000002 也就是 INITOK 置位;iRoT-Provisioned 下 RCC 的 PKA 使能位是 0,安全属性位是 1,PKA 的 SR 恒为 0x00000000。这组对照数据基本就把方向指到了“安全属性导致 Non-Secure 代码访问 PKA 被屏蔽”。如果没有做这种对比,你很容易在 PKA 的初始化函数里反复改超时时间、改等待顺序,却始终碰不到真正原因。
| 调试项 | OPEN 状态 | iRoT-Provisioned 状态 |
|---|---|---|
| RCC PKA 时钟使能位 | 1 | 0 |
| PKA 安全属性位 | 0(Non-Secure) | 1(Secure) |
| PKA CR 寄存器写入 | 正常 | 写入不生效 |
| PKA SR 寄存器 | INITOK=1 | INITOK=0 |
| 用户代码安全世界 | Non-Secure | Non-Secure |
3.3 第三步:用“写读回”判断访问权限是否被阻断
有时候安全属性配置位在调试器里直接读不一定方便,我习惯用“写读回”来验证访问权限:向 PKA 的控制寄存器写入一个非零且可恢复的测试值,然后立刻读回来,如果读回来还是复位值或者完全不变,说明写入操作根本没到达 PKA。这也解释了为什么初始化代码看起来在执行,但寄存器永远不变化。
具体做法是在初始化函数入口处加一段临时逻辑:先读一次 CR,然后把 CR 当前值或上 0x01 写回去,再读一次 CR,把两次值都通过串口打印出来。OPEN 状态下,读回值会包含你写进去的位;iRoT-Provisioned 状态下,读回值会原封不动。这个小实验能非常直接地证明是否存在安全屏蔽。做完之后记得把测试代码删掉,避免影响后续逻辑。
另外一个判断技巧是注意 HardFault 是否发生。如果 Non-Secure 代码访问 Secure 外设直接触发 HardFault,问题会好查一些;但 PKA 这种外设往往是被总线级别的过滤器静默丢弃写操作,不产生任何异常。所以“没有报错但寄存器不动”反而更要警惕安全属性问题。
3.4 第四步:最小化复现,把环境和代码变量分开
定位到安全属性后,我还做了一步最小化复现来确认结论:在 OPEN 状态下,先用 CubeMX 把 PKA 手动配置成 Secure 外设,再编译一个 Non-Secure 用户代码访问 PKA。结果复现了同样的现象:SR.INITOK 不置位,寄存器写入无效。这个过程本质上是在 OPEN 环境下模拟了 iRoT-Provisioned 的安全边界,不用反复重新烧 provisioning 配置,调试效率高很多。
最小化复现的思路很实用。当你觉得“切到 provision 才出问题”时,不要一直用 provisioning 来复现,因为切换生命周期本身有一定成本,而且改了配置后想回退可能很麻烦。先在 OPEN 下通过配置寄存器模拟受限环境,快速确认是否和安全属性有关,再回到真实场景验证,能省掉大量重复烧录等待的时间。当然模拟环境无法完全覆盖 STiRoT 预占用 PKA 的脏状态,所以最终还是要回到真实 iRoT-Provisioned 状态做回归。
4. 解决方案:让 PKA 在 STiRoT 启动后也能正常初始化
4.1 方案一:把 PKA 安全属性调整到和用户代码一致
最简单直接的方案,是在 provisioning 配置阶段就把 PKA 分配给你实际运行的安全世界。如果用户应用在 Non-Secure 世界,那么把 PKA 的 RCC 安全属性位配成 Non-Secure。这样 STiRoT 跳转前设置的安全边界就不会挡住用户代码对 PKA 的访问。
具体操作可以在 STM32CubeProgrammer 的 STiRoT 配置页面里找外设安全分配的选项,或者在 CubeMX 的工程配置里把 PKA 的安全属性设为 Non-Secure,再重新生成、重新 provisioning。需要注意的是,把密码外设放在 Non-Secure 世界会降低整体的安全强度,如果产品对密钥保护要求很高,不建议把所有密码相关资源全部开放给 Non-Secure 代码。安全性和便利性之间需要你根据实际威胁模型权衡。
如果你的用户代码运行在 Secure 世界,那就简单了,确保 PKA 安全属性是 Secure 即可。但要注意,很多应用的主逻辑都在 Non-Secure 世界,Secure 世界只放信任根和关键服务,所以 PKA 归 Non-Secure 更常见。还有一种做法是把 PKA 留在 Secure 世界,然后在 Secure 侧封装一组可信 API,Non-Secure 代码通过安全调用间接使用 PKA,这样既解决访问限制,又保住安全强度。代价是实现复杂度高一些,函数调用也要走安全边界。
4.2 方案二:用户代码里先做时钟和复位清理
如果你的产品安全策略不允许调整 PKA 的安全属性,或者你没法重新 provisioning,那么只能在用户代码里做补救。核心思路是:在访问 PKA 之前,确保时钟使能并处于可复位状态,然后强制一次复位,清除 STiRoT 可能留下的脏状态,最后再重新初始化 PKA。
参考代码如下:
static int pka_safe_init(void) { /* 1. 使能 PKA 时钟,如果访问被安全属性阻挡,这里会无效 */ __HAL_RCC_PKA_CLK_ENABLE(); /* 2. 强制复位 PKA,清掉 STiRoT 或其它代码留下的状态 */ __HAL_RCC_PKA_FORCE_RESET(); __HAL_RCC_PKA_RELEASE_RESET(); /* 3. 再次使能时钟,确保复位后时钟稳定 */ __HAL_RCC_PKA_CLK_ENABLE(); /* 4. 根据实际芯片手册,配置必要的控制位 */ PKA->CR &= ~PKA_CR_ENABLE; PKA->CR |= PKA_CR_ENABLE; /* 5. 等待 INITOK */ uint32_t timeout = 10000; while ((PKA->SR & PKA_SR_INITOK) == 0U) { if (--timeout == 0U) { return -1; } } return 0; }这段代码在 OPEN 下不会有问题,因为 PKA 时钟和安全属性都正常。在 iRoT-Provisioned 下,只有当 PKA 安全属性允许当前代码访问时,RCC 复位操作才能真正生效。如果安全属性被锁死为 Secure,而你运行在 Non-Secure,那这段代码里的时钟使能和复位操作同样会被静默丢弃,所以方案二成立的前提是安全属性不能是排斥性配置。如果确实被锁死成 Secure,只能走方案一或 Secure 侧封装接口。
4.3 方案三:如果 PKA 被 STiRoT 占用,用复位强行清场
刚才提到 STiRoT 可能已经用 PKA 做过验签,所以即便时钟和安全属性都正常,PKA 也可能处于一个“初始化过但没复位”的状态。这个时候最干脆的办法就是强制复位 PKA,让它彻底回到上电状态,再走一遍初始化流程。方案二代码里的 FORCE_RESET 和 RELEASE_RESET 就是做这件事。
需要提醒的是,PKA 复位操作必须在 PKA 空闲时进行,不要在它正在执行运算的中间插一脚。在安全启动场景下,STiRoT 已经把 PKA 运算做完了,理论上跳转时 PKA 已经空闲,所以这时复位是安全的。如果用户代码里还有其它线程可能同时访问 PKA,复位前需要加锁或关中断,避免状态错乱。
另外,不一定所有 STM32 系列都允许 RCC 级复位 PKA,具体要看参考手册的复位控制章节。如果不支持 RCC 复位,可以尝试通过关闭 PKA 时钟再重新打开来完成类似效果,但要注意关闭时钟也可能受安全边界影响,所以先确认复位和时钟控制寄存器是否可写。
4.4 验证清单和回归测试
改完代码后,不能只在 iRoT-Provisioned 下跑一次就算完事。我把验证分成三个层次。第一层,确认 PKA 初始化函数返回 0,SR.INITOK 置位,CR 写入读回正常。第二层,跑一次完整的 ECC 点乘或 RSA 验签,和已知正确结果比对,确保 PKA 不是“假活”。第三层,分别在 OPEN、iRoT-Provisioned、CLOSED 环境各跑一遍相同的启动与运算流程,记录串口日志。CLOSED 状态下调试口可能已经关闭,所以要在 provisioning 前就把日志输出逻辑准备好,或者用 GPIO 状态灯代替串口。
我还建议做一次“断电冷启动”测试,因为热复位和冷上电时 STiRoT 的启动路径可能略有差异,PKA 的脏状态不一定每次都在。实测中有时候热复位能复现、冷启动就正常,或者反过来。这类问题非常依赖启动时序,所以冷热都测才放心。最后再跑一遍长期稳定性测试,比如连续执行 1000 次 PKA 验签,确认稳定通过。
5. 避坑清单:安全生命周期下使用 PKA 的实战建议
5.1 常见问题速查表
我把这次调试中遇到的各种表现整理成一张速查表,后续项目里再碰到类似问题可以快速定位方向:
| 现象 | 可能原因 | 排查手段 | 解决方向 |
|---|---|---|---|
| SR.INITOK 一直为 0,写入寄存器无效 | PKA 安全属性与当前代码世界不匹配 | 写读回测试、检查 RCC 安全属性位 | 调整安全属性或在 Secure 侧封装接口 |
| SR.INITOK 为 0,且 RCC 使能位也是 0 | 时钟未使能或时钟控制被安全锁定 | 读 RCC 使能位,确认能否写入 | 在正确安全世界使能 PKA 时钟 |
| 热复位正常、冷启动异常 | STiRoT 对 PKA 的初始化路径不同 | 分别热冷启动看日志 | 初始化流程增加 RCC 强制复位 |
| 卡在 PKA 命令等待,SR 有 INITOK 但命令不执行 | PKA 从脏状态恢复但参数错乱 | 检查命令寄存器写入是否生效 | 强制复位 PKA 后重新初始化 |
| Non-Secure 代码触发 HardFault | 访问了 Secure 外设或 Secure 内存 | 查看 HardFault 位置 | 调整访问路径或安全边界 |
这张表不可能覆盖所有情况,但能帮你快速建立排查顺序:先看安全属性,再看时钟,再看复位,最后才是算法和参数问题。很多人一上来就怀疑算法实现,反复检查大数运算和字节序,完全绕过了真正的安全边界问题,浪费时间。
5.2 开发期就要把安全生命周期纳入测试范围
我最大的教训是:不要在 OPEN 状态做完全部验证后再切到生产生命周期。正确的做法是从项目一开始,就按最终产品的安全策略配置好 STiRoT 和生命周期状态,哪怕开发调试中保留部分调试口能力,也要让外设安全属性和最终产品保持一致。这样你在开发阶段就能暴露 PKA 访问被屏蔽之类的问题,而不是等到快要量产了才被迫处理。
具体操作上,我会在开发板上一开始就执行 provisioning,但把调试口策略调成允许 Secure 调试,这样既能观察 Secure 世界寄存器,也不会偏离最终运行环境太多。用户代码的启动日志里固定打印关键外设的时钟使能和安全属性状态,任何异常都能第一时间发现。等到所有功能都稳定,再把调试口关闭,切到 CLOSED 或 LOCKED 做最终回归。这样可以避免“开发一切正常,上线立刻翻车”的尴尬。
5.3 安全世界和外设分配要提前设计,别临时补丁
如果项目里需要同时使用 Secure 和 Non-Secure 代码,建议在系统设计阶段就画一张“安全资源分配表”,把哪个外设属于 Secure、哪个属于 Non-Secure、哪些外设需要跨世界访问固定下来。PKA、AES、RNG 这类密码相关外设是重点决策对象。不要把 PKA 放在 Non-Secure,又把密钥放在 Secure 内存里,导致密钥流转路径绕来绕去。
我经历过一个反面例子:为了快速解决问题,直接在 provisioning 里把 PKA 改成 Non-Secure,结果后面做安全评估时发现,Non-Secure 代码可以直接操作 PKA,等于把核心密码能力完全暴露给了被攻破的非安全环境。后来还是回头增加了 Secure 侧封装接口,整体架构做了一次不小的调整。如果一开始就规划好 PKA 的安全归属,这个返工完全可以避免。安全功能不像普通功能那样可以无痛追加,越早定型越好。
5.4 多留一手:初始化函数要做成可重入、可恢复
在安全启动场景下,PKA 的初始化函数最好设计成可以被重复调用,并且在失败时能把状态清干净。我在最终代码里把初始化拆成了三步:关闭时钟、复位、重新使能。这样即使上层调用方传入的参数有问题,或者外部安全边界发生了变化,我也能在下一次调用时通过强制复位恢复 PKA 到一个已知状态。
同时,超时处理和错误上报要做得明确。不要用死等,而是加一个带计数的超时循环,超时后打印错误码和当前 SR 值。错误码至少包含“初始化超时”“写读回失败”“运算结果校验失败”三类。这样现场出现问题,光靠日志就能判断是访问被阻断,还是算法本身出错,不用再把调试器连上去复现。在调试口可能被关闭的生产环境里,这些日志就是你的第二只眼睛。
我个人在实际操作中的体会是,调试这类生命周期相关的问题,最怕的是想当然。只要经历过一次“OPEN 正常、provision 翻车”,后续我就会在项目的每个关键节点问自己:这个外设到底归哪个安全世界管?它在到达用户代码之前被谁碰过?如果这两个问题答不上来,再往后写多少功能代码都可能在真正上线时踩坑。最后再分享一个小技巧:改完初始化逻辑后,记得把 PKA 的测试向量也一起固化到工程里,每次烧录后自动跑一遍自检,这样任何安全边界变化都能被第一时间探测到。