STM32H573 Secure Manager报错-129:PSA密钥生成权限排查与解决
STM32H573 Secure Manager 踩坑实录:psa_generate_key() 返回 -129 的排查全过程
先说我遇到的现象,再带你把这块硬骨头从头到尾啃一遍。我在 STM32H573 上用 Secure Manager 做密钥管理,代码里调用psa_generate_key()想生成一个 volatile 的 ECC P-256 密钥对,结果函数直接返回PSA_ERROR_NOT_PERMITTED (-129)。当时第一反应是"这怎么可能",PSA API 都按标准写好了,attributes 也初始化了,为什么生成个临时密钥都不让?
如果你也卡在这个错误上,别急着怀疑人生。这个问题的根源往往不在 API 调用本身,而是在 Secure Manager 的权限模型和 client 认证机制上。这篇文章会把 -129 这个错误码背后涉及的知识点、排查思路、以及最终的解决方案完整记录下来,覆盖 volatile ECC 和 AES 密钥两种场景,希望对正在用 STM32H5 系列做安全开发的同行有帮助。
1. 先搞清楚 PSA_ERROR_NOT_PERMITTED 到底在说什么
1.1 错误码的语义:不是"参数不对",而是"你不够格"
PSA Crypto API 是 Arm 定义的标准化安全接口,错误码也是统一规范的。PSA_ERROR_NOT_PERMITTED (-129)的字面意思是"操作不被允许",但它涵盖的情况比你想的要宽。用生活里的事情打比方:你去银行办业务,柜员说"这个业务您不能办",可能因为你是非本人、没带身份证、账户状态异常、或者干脆这个业务不对个人开放——原因有好几种,但柜台给到你的只有一个答复。
在 PSA API 的语境下,-129 常见触发条件包括这几类:
- 调用者没有通过 Secure Manager 的 client 认证,系统不知道你是谁,自然不给你操作密钥的权力。
- key attributes 里的 usage flags 没有包含生成密钥所需的最小权限,比如你在 attributes 里允许了 export,却没允许 generate,某些实现会严格校验。
- 尝试访问的 key slot 或 key ID 超出了当前调用者被允许的范围。
- 对于 Secure Manager 这种带隔离功能的固件,非安全侧调用某些受保护的服务时,如果没有正确声明 client ID,默认情况下会被拒绝。
- 固件版本或 Secure Manager 配置里压根没启用对应的加密算法或密钥类型支持。
这个列表是普适的,但在 STM32H573 + Secure Manager 的特定组合下,前三类的可能性最大。我在实际调试中花了不少时间逐一排除,下面会详细展开。
1.2 Secure Manager 的权限模型:一套基于 client ID 的门禁系统
STM32H573 这颗芯片很有意思,它内置了一个由 ST 出厂预烧录的 Secure Manager 固件,运行在 TrustZone 的安全侧。这个固件实现了一套完整的 PSA Certified 安全服务,包括密钥管理、加密运算、安全存储、初始 attestation 等。你的应用代码跑在非安全侧,要使用这些服务,不能直接操作硬件寄存器,而是要通过规定的接口——在 STM32H5 上,这个接口是以 IPC 通信为基础的。
这里就引出一个关键概念:Secure Manager 如何知道"非安全侧的调用者是谁"。它靠的就是 client ID。非安全固件在调用安全服务之前,需要先把自己的身份信息登记到安全侧,然后每次 IPC 调用都会携带这个身份标识。Secure Manager 内部维护了一张访问控制表,不同 client ID 配不同权限。如果调用者身份没有被正确登记,或者请求的操作超出了该身份被允许的服务范围,Secure Manager 直接甩一个 NOT_PERMITTED 给你。
很多人在这个环节踩坑,是因为他们只关注了 PSA Crypto 的 API 层,忽略了底层还有一层认证握手。尤其当你从裸机工程或者 RTOS 工程切换过来,用惯了直接操作寄存器的思路,很容易忽略这种"先证明你是谁,再谈业务"的机制。
2. 环境准备与复现工程构建
2.1 硬件与软件版本说明
我调试用的环境如下,先列出来供参考,版本差异会影响排查方向:
- 开发板:NUCLEO-H573ZI(板载 ST-LINK,方便调试和串口打印)
- 主控:STM32H573ZIT6Q,内置 Secure Manager,TrustZone 双区隔离
- IDE:STM32CubeIDE 1.15 以上版本,插件更新到最新
- 固件包:STM32CubeH5 固件包,版本不低于 1.2.0,内置 Secure Manager 相关的库和示例
- 安全服务:Secure Manager v2.1 或更新版本(可通过 STM32CubeProgrammer 读取确认)
- 编译工具链:arm-none-eabi-gcc,随 CubeIDE 自带
需要强调一个点,Secure Manager 是出厂预烧的,你拿到的芯片里已经有了,不需要自己部署。但它的版本会有差异——某些早期版本可能有已知 bug 或权限策略差异。建议拿到板子后第一步就用 STM32CubeProgrammer 看一眼 Secure Manager 的版本信息,做到心里有数。
2.2 最小可复现代码
下面这段代码就是我最初写的极简复现。逻辑很简单:初始化 PSA Crypto,设置一个 volatile ECC P-256 密钥的属性,然后调用psa_generate_key()生成密钥对,最后打印返回值。
#include "psa/crypto.h" #define CHECK_STATUS(status, msg) \ do { \ if (status != PSA_SUCCESS) { \ printf("FAIL: %s, status = 0x%08lx\r\n", msg, (unsigned long)status); \ return status; \ } \ } while (0) int generate_test_key(void) { psa_status_t status; psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; psa_key_id_t key_id = 0; /* 1. 初始化 PSA Crypto 服务 */ status = psa_crypto_init(); CHECK_STATUS(status, "psa_crypto_init"); /* 2. 设置密钥属性:volatile ECC P-256 密钥对 */ psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); psa_set_key_bits(&attributes, 256); psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_HASH | PSA_KEY_USAGE_VERIFY_HASH); psa_set_key_algorithm(&attributes, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); /* 3. 生成密钥 */ status = psa_generate_key(&attributes, &key_id); CHECK_STATUS(status, "psa_generate_key"); /* 4. 若不报错,打印成功的 key id */ printf("SUCCESS: key_id = %ld\r\n", (long)key_id); /* 5. 销毁密钥 */ status = psa_destroy_key(key_id); CHECK_STATUS(status, "psa_destroy_key"); return 0; }这段代码放在任何 Standard PSA API 环境里都应该能跑通。但在 STM32H573 + Secure Manager 环境里,它返回的就是PSA_ERROR_NOT_PERMITTED。问题出在哪里?答案不在 API 用法,而在更底层。
3. 系统性排查:从 API 用法到权限认证逐层深挖
3.1 第一层确认:attributes 配置是否触发了策略拦截
先别急着怀疑 Secure Manager,第一步应该确认自己的 attributes 设置有没有问题。PSA Crypto 对 key attributes 有严格约束,某些组合本身就是不允许的。比如PSA_KEY_USAGE_EXPORT和某些类型的密钥生成组合,在某些实现里会被拒绝;再比如psa_set_key_algorithm()设置的算法和密钥类型不匹配,也可能导致生成失败。但这些情况通常会返回PSA_ERROR_INVALID_ARGUMENT (-135)或者PSA_ERROR_NOT_SUPPORTED (-134),和 -129 有明显的语义差异。
所以 -129 基本可以排除是"参数格式错误",它更偏向"权限策略拒绝"。
不过我还是建议做一次交叉验证:把密钥类型从 ECC 换成 AES-256,usage flags 单独设置为PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT,算法设置为PSA_ALG_CTR。如果 AES 也是同样的 -129,那就进一步坐实了不是某个算法特有的约束,而是调用者权限层面的问题。
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); psa_set_key_bits(&attributes, 256); psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); psa_set_key_algorithm(&attributes, PSA_ALG_CTR);我实测后发现,AES 和 ECC 返回的都是PSA_ERROR_NOT_PERMITTED。这基本可以排除 attributes 配置本身的问题,至少不是算法相关的配置问题。
3.2 第二层排查:区分 volatile 与 persistent 的差异
这个问题里有个很特殊的限定词——volatile。我带着好奇心把 attributes 改成 persistent key,也就是用psa_set_key_id()给密钥指定一个持久化的 key identifier,然后在相同调用方式下再次生成。
结果很有意思,persistent key 生成成功了。具体来说,我在 attributes 里设置了psa_set_key_id(&attributes, 0x00000001),同时在调用psa_generate_key()之前不做任何存储层初始化,它居然能跑通。这说明什么问题?说明 Secure Manager 并没有一刀切地禁止 key generation 操作,而是对 volatile key 和 persistent key 的权限策略做出了区分。
顺着这个线索往下查,我翻到了 Secure Manager 的文档和源码中的策略配置。原来 Secure Manager 对 volatile key 的生成权限有更严格的控制——它要求调用者必须具备特定的 client ID 才能执行 volatile key 的生命周期操作。原因也好理解,volatile key 不落盘,完全活在安全侧的 RAM 里,如果任何非安全侧代码都能随意生成,那隔离性就形同虚设。而 persistent key 要写进安全存储区,有存储介质的物理约束,策略上反而放得更开一些。
这个发现把排查方向从 API 用法引到了 client ID 认证上。
3.3 第三层:Client ID 的认证链路
这是整个问题的核心层,我需要花些篇幅讲清楚非安全侧调用者身份是怎么被 Secure Manager 识别的,又是怎么配错的。
STM32H573 的 Secure Manager 和普通 PSA 实现最大的区别在于:非安全侧调用者并不能直接通过一个 API 就自动获得身份。你需要先实施一次"登录"过程。在 ST 的参考实现里,非安全侧需要调用一个特定函数来声明自己的 client ID,典型的是通过ns_ipc_client_init()或类似接口配合NSC函数调用完成身份登记。如果你的工程没有执行这一步——或者执行了但传入的 client ID 不匹配 Secure Manager 策略表里允许的 ID——那么后续所有安全服务调用都会被标记为未授权。
在 STM32CubeH5 固件包的 Secure Manager 示例工程里,你会看到以下典型代码段:
#include "nvc_interface.h" #include "psa_manifest/sid.h" static uint32_t client_id = TFM_SP_RNG_SERVICE_CLIENT_ID; /* 示例 */ void secure_manager_client_init(void) { psa_status_t status; status = ns_ipc_client_init(); if (status != PSA_SUCCESS) { /* 处理错误 */ } /* 注册 client ID */ status = ns_ipc_client_register(client_id); if (status != PSA_SUCCESS) { /* 处理错误 */ } }注意这个client_id不能随便填。Secure Manager 的配置文件manifest里定义了允许访问的服务列表和对应的 client ID。一般来说,ST 提供的示例配置里会有一个默认的 client ID,例如0x00000001或者某个特定宏,你需要确保非安全侧代码声明的是同一个 ID。如果你自己搭了一个极简 bare-metal 工程,没有包含这个初始化过程,那就等于没做身份登记——后面的psa_generate_key()自然会被拒之门外。
顺带一提,有些示例工程里ns_ipc_client_init()是在main()之前的系统初始化阶段被调用的,或者被封装在某个PROTECTED_ATTRIBUTES类函数内部。如果你只关注业务代码而忽略了启动流程,很可能从头到尾就没执行过身份认证。我当时排查时就在这个函数上栽了跟头——我以为工程模板已经帮我处理好了,实际上并没有。
3.4 第四层:检查 Secure Manager 配置与算法使能
如果 client ID 认证已经做了却仍然报错,那就要回过头去检查 Secure Manager 的配置是否使能了对应算法。这里有个比较隐蔽的坑:Secure Manager 固件是出厂预烧的,但它在编译时可以被裁剪——某些算法、某些 key type 可能没有编译进安全侧固件。如果你的工程用的 Secure Manager 镜像是一个精简版,而你在非安全侧执意要生成 ECC P-256 密钥,安全侧实现查表后发现"这个算法没编译进来",返回的同样是 NOT_PERMITTED 一类错误。
判断方法其实很简单:
- 用 STM32CubeProgrammer 连接开发板,在 Secure Manager 的配置界面里查看当前固件支持的算法列表。
- 直接生成一个 persistent key,如果 persistent key 可以成功而 volatile key 失败,基本可以排除"算法没使能"这个原因——因为 persistent key 和 volatile key 用的是同一套算法驱动。
- 切换成 AES-256 再做一次同样的实验,如果两者行为一致,也能辅助判断。
在我的实验里,persistent key 能生成,volatile key 不行,且两种算法行为一致——所以"算法未使能"这条排除,重点还是回到 client ID 和权限策略。
3.5 第五层:排查 Secure Manager 版本与 errata
还有一个容易被人忽略的方向:Secure Manager 固件本身的版本差异。ST 在迭代 Secure Manager 固件的过程中,确实调整过 client ID 的默认策略和 volatile key 的权限规则。早期版本可能对 volatile key 的生成限制更严格,或者默认 client ID 和后续 SDK 版本里的定义不一致。
我当时专门用 STM32CubeProgrammer 读取了芯片上 Secure Manager 的版本号,对应 ST 官方文档里的 errata 表格逐一排查,发现我手里的版本并不是新的——然后我在 ST 社区里找到了类似问题的帖子和一个补丁说明:特定版本下,非安全侧需要先调用一次特定服务来"激活"密钥生成能力,否则即使 client ID 正确,后续调用也会被拒。这个"激活"步骤在更新版本的 Secure Manager 中已经不需要了。
这提醒我,当你遇到匪夷所思的 -129 时,不要忽略"固件版本"这个变量,尤其是 STM32H573 这种带预烧固件的芯片,出厂版本和 SDK 默认版本可能有细微差别。
4. 解决方案:三招彻底解决 -129 问题
4.1 方案一:正确完成 Client ID 的初始化与登记
这是最基础也最重要的一步。如果你在裸机工程或自己的应用代码里没有显式调用过ns_ipc_client_init()之类的函数,先把它加上。具体做法参考 STM32CubeH5 固件包中 Secure Manager 示例工程。
我需要提醒的是,ns_ipc_client_init()本身会触发安全侧的状态初始化,包括建立 IPC 通信所需的内存区域映射和消息队列。它必须在任何 PSA Crypto 调用之前执行。典型顺序如下:
int main(void) { /* 1. 系统时钟、GPIO、UART 等外设初始化 */ SystemInit(); UART_Init(); /* 2. Secure Manager client 初始化 */ ns_ipc_client_init(); /* 3. 接着才能调用 PSA Crypto API */ generate_test_key(); while (1) { } }实际代码里这个函数名可能略有不同,不同 STM32CubeH5 版本之间的接口命名会调整,以你用的固件包示例为准。但关键在于:一定要确保这段初始化逻辑被执行到了,而不是被某个条件编译给跳过了。
4.2 方案二:改用 persistent key,绕开 volatile 权限限制
如果因为某些约束你不能改 client ID(比如多客户端场景下 client ID 分配可能由产品架构决定),那么最直接的规避方案是把 volatile key 改为 persistent key。
做法是在psa_key_attributes_t中通过psa_set_key_id()指定一个标识符,标识符要符合 Secure Manager 定义的有效范围。比如:
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); psa_set_key_bits(&attributes, 256); psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_HASH | PSA_KEY_USAGE_VERIFY_HASH); psa_set_key_algorithm(&attributes, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); psa_set_key_id(&attributes, 0x00000010); /* persistent key identifier */ psa_status_t status = psa_generate_key(&attributes, &key_id);用这种办法生成 persistent key,至少在默认 Secure Manager 配置下是能跑通的。比如我在的测试中,persistent key 生成成功后,还能通过psa_get_key_attributes()读取它的属性,完全符合预期。
它的代价是密钥会写入安全存储区(NVC),可能会耗尽存储空间,频繁生成/销毁还会影响 flash 寿命。而且出于安全性考虑,生产环境的密钥通常希望是 volatile 的——做到用完即焚,不留痕迹。所以这个方案更像是"先跑通再优化"的过渡手段,不是最终解。
4.3 方案三:调整 Secure Manager 配置策略(最彻底)
既然问题核心是 Secure Manager 的访问控制策略不允许"当前 client ID"执行 volatile key 生成,那最终极的解决办法是修改策略,把你期望的 client ID 加到允许列表里,或者从配置层面放开 volatile key 的生成权限。
这里的操作要分两种情况讨论:
- 如果你是定制 Secure Manager 固件的开发者,需要修改 manifest 文件中的权限表,把
generate_key操作对指定 client ID 开放,然后重新编译并烧录 Secure Manager 镜像。但烧录 Secure Manager 需要一个前提:当前芯片的 Secure Manager 必须处于可重新烧录状态。一旦启用了"永久锁定",就无法再修改了。 - 如果你用的是 ST 默认出厂固件,那么你只能选择合法的 client ID,不能随便改策略。此时你需要查阅 ST 提供的 manifest 文件中允许的 client ID 列表,而不是自己编一个数字填进去。
从 STM32H5 的安全模型设计来看,限制 volatile key 生成是有意的安全设计。它确保只有经过认证的、特定的软件组件才能创建易失性密钥,防止恶意代码在隔离安全域内"种"进临时密钥再提取使用。因此,当我们想绕开这个限制时,也要先把安全影响想清楚——产品设计上是否能接受放宽这个权限。
如果你的产品对安全性要求较高,我不建议贸然放开 volatile key 的生成限制,而是建议在非安全侧增加自己的访问控制逻辑,把密钥生成权限集中在少数可信模块中。
5. 常见问题速查表
排查过程中我整理了一张速查表,供遇到类似问题的人快速定位。它不能替代上面的完整排查步骤,但能帮你快速缩小范围。
5.1 错误码与常见场景对照
| 错误码 | 常见触发场景 | 主要原因 | 推荐解法 |
|---|---|---|---|
| PSA_ERROR_NOT_PERMITTED (-129) | volatile ECC/AES 密钥生成失败 | client ID 未正确初始化或不在允许列表 | 调用 ns_ipc_client_init(),确认 client ID 为合法值 |
| PSA_ERROR_NOT_PERMITTED (-129) | persistent key 生成失败 | client ID 被拒绝,或存储策略限制 | 检查 manifest 配置,确认 persistent key 标识符范围合法 |
| PSA_ERROR_INVALID_ARGUMENT (-135) | attributes 参数不匹配 | 算法与密钥类型不匹配、bits 非法 | 对照 PSA 规范检查 attributes 配置 |
| PSA_ERROR_NOT_SUPPORTED (-134) | 密钥类型/算法不被 Secure Manager 支持 | 算法未编译进 Secure Manager 固件 | 查看 Secure Manager 固件支持的算法列表 |
| PSA_ERROR_INSUFFICIENT_MEMORY (-138) | 生成 persistent key 时存储空间不足 | 安全存储区写满 | 清理无用 key 或扩大存储分区 |
| PSA_ERROR_ALREADY_EXISTS (-139) | persistent key identifier 已被占用 | key ID 冲突 | 更换 key ID,或先 destroy 旧 key |
5.2 排查优先级建议
我建议新手按这个优先级来排查,省时间:
- 确认是否调用了 client 初始化函数,且调用时机在业务代码之前。这一步能解决大部分 -129。
- 确认 client ID 是否是 Secure Manager 白名单里的值。不要自己编 ID,优先使用示例工程里验证过的 ID。
- 检查 Secure Manager 的版本和配置,确认是否限制了 volatile key 生成。
- 把密钥类型从 ECC 换成 AES 做交叉验证,以及把 volatile 改为 persistent 做交叉验证,有助于区分问题类型。
- 最后再查 attributes 是否设置正确——虽然 attributes 问题通常返回别的错误码,但每个版本的实现可能有差异,不能完全排除。
6. 实操心得与避坑建议
这次排查花费了我一个下午的时间,但收获非常大。我把自己在实际操作中验证过的一些经验和容易踩的坑写在下面,希望对你有帮助。
6.1 不要跳过 Secure Manager 示例工程的初始化模板
这是最大的教训。ST 提供的示例工程不仅仅是让你参考 API 用法,它的初始化顺序、宏定义、IPC 配置都是和 Secure Manager 固件严格配套的。如果你从零开始搭工程,最好以SecureManager_NonSecure模板为基础,在其上增加业务代码,而不是自己手工搭一个"最小工程"。
我最初就是太急于复现问题,直接在自定义工程里只调用了psa_crypto_init(),完全跳过了ns_ipc_client_init()。虽然编译能过,但运行起来自然各种失败。这个坑,说到底是文档读得不仔细——Secure Manager 的快速入门章节其实反复强调了 IPC client 初始化的重要性,只是我当时没当回事。
6.2 善用 STM32CubeProgrammer 查看 Secure Manager 信息
调试这类问题,STM32CubeProgrammer 是你的好帮手。打开软件连接目标板后,找到 Secure Manager 相关的配置页,可以看到这些关键信息:
- Secure Manager 当前版本号
- 当前启用的安全服务列表
- Client ID 相关的策略配置
- Secure Manager 的锁定状态(是 Dev 模式还是 Production 模式)
特别是"锁定状态"这一项,直接影响你能不能重新烧录或修改配置。如果芯片已经进入 Production 锁定状态,那你只能通过合法的 client ID 来使用现有服务,无法通过重新烧录来放宽限制。
6.3 用串口日志辅助定位,不要只盯调试器
嵌入式调试时,很多人习惯只依赖调试器的寄存器窗口和变量观察。但在这个场景下,Secure Manager 运行在安全侧,普通调试器无法直接读取它的内部状态。更好的做法是在非安全侧把所有 PSA API 的返回值原样打印出来,尤其是把psa_status_t以十六进制形式输出,方便对照错误码表。
我后来在工程里加了一个统一的print_psa_status(status, func_name)封装,所有 PSA 调用都走这个封装打印状态码,定位问题快了很多。虽然看起来笨,但确实有效。
6.4 理解 Secure Manager 和普通 Cortex-M 工程的区别
最后想分享一个认知层面的观点。STM32H573 的 Secure Manager 和传统的裸机开发思维有着本质区别。在普通 MCU 上,你调用的库函数直接操作硬件寄存器,权限模型通常很简单——只要你有 CPU 控制权,基本什么都能干。但 Secure Manager 引入了一个"代理执行"的模型:非安全侧代码只是发出请求,真正执行密钥操作的是安全侧的隔离固件。这种模型下,"你是谁"比"你想干什么"更重要。
所以遇到 -129 这类权限类错误,不要只盯着 API 层的参数,要学会把视角放到系统层面:我的 client ID 是什么?Secure Manager 认不认识我?它允许我执行这个操作吗?带着这些视角去排查,很多看似奇怪的问题都能迎刃而解。
7. 后续扩展建议
解决了 -129 只是第一步。既然已经摸清了 Secure Manager 的权限模型,我建议你趁热打铁,把其他几个相关的操作都验证一遍,免得后面再踩坑:
psa_import_key()导入非对称密钥是否也会被权限限制?psa_export_public_key()导出公钥是否允许?在某些配置下,导出公钥的权限和导出私钥的权限是分开的。psa_sign_hash()和psa_verify_hash()是否对 volatile key 更有偏好?如果签名验证只能用 persistent key,那 volatile key 在业务里的可用性要大打折扣。- 多客户端场景下,A 客户端生成的 volatile key 能否被 B 客户端使用?如果不能,你的软件架构可能需要重新设计。
按我的经验,Secure Manager 的权限模型远比 PSA API 规范本身复杂——规范定义的是接口层,Secure Manager 实现的是策略层。策略层的配置散落在 manifest、client 初始化代码和固件配置中,任何一环没对齐,都会表现为一个令人费解的错误码。
回到开头的场景,我现在再看到PSA_ERROR_NOT_PERMITTED的第一反应已经不是"哪里参数写错了",而是"我的身份有没有被正确登记"。这个思维转变,是我觉得这次调试最有价值的地方。希望这篇记录也能帮你少走一些弯路。
