STM32H573 Secure Manager密钥生成-129错误排查与修复 最近在 STM32H573 上调一个安全存储相关的功能卡在了一个非常不起眼的地方psa_generate_key()对易失性 ECC/AES 密钥一直返回PSA_ERROR_NOT_PERMITTED也就是 -129。这个错误对做嵌入式固件的人来说太有迷惑性了——你的第一反应肯定是密钥策略写错了于是翻来覆去检查 usage flags、algorithm、lifetime发现全都符合 PSA 规范代码在别的平台上也能跑通偏偏在 H573 的 Secure Manager 环境里就是不行。这篇文章把我整个排查过程、最终定位的根因、以及正确的密钥生成配置完整记录一遍。如果你正在 STM32H573 上做安全启动、安全存储、TLS 密钥管理或者准备把应用从裸 PSA Crypto 迁移到 Secure Manager 环境这篇可以直接当避坑参考。1. 问题现场Secure Manager 环境下的 -129 报错全貌1.1 硬件与软件背景先说环境。我用的主控是 STM32H573这芯片最大的卖点之一就是出厂内置了 Secure Manager跑在 Cortex-M33 的安全世界里对外提供 PSA Certified 级别的安全服务安全存储、密码学运算、密钥管理、设备认证等。非安全侧的应用代码通过 Secure Manager 提供的 veneer 接口调用 PSA Crypto API密钥的实际生成和管理都发生在安全侧隔离环境里。软件侧我使用 STM32CubeH5 固件包里的 Secure Manager 相关组件编译链接了官方提供的 NSNon-Secure侧库。应用本身跑在非安全世界业务逻辑很简单系统启动后动态生成一把 ECC P-256 密钥对用于 TLS 客户端证书协商再生成一把 AES-128 密钥用于通信数据加密。问题就出在密钥生成这一步。无论 ECC 还是 AES只要密钥生命周期设置为易失性volatilepsa_generate_key()就稳定返回 -129。改成持久性密钥persistent有些场景能过去有些场景仍然报错但报错方式又不一样。1.2 最小复现代码当时复现问题的代码非常标准基本就是从 PSA Crypto 标准示例里抄过来的#include psa/crypto.h static psa_status_t generate_volatile_ecc_key(psa_key_id_t *out_key_id) { psa_status_t status psa_crypto_init(); if (status ! PSA_SUCCESS) { return status; } psa_key_attributes_t attr PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); psa_set_key_bits(attr, 256); psa_set_key_lifetime(attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(attr, PSA_KEY_USAGE_SIGN | PSA_KEY_USAGE_VERIFY); psa_set_key_algorithm(attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); status psa_generate_key(attr, out_key_id); psa_reset_key_attributes(attr); return status; }psa_generate_key()返回后我把它打印出来psa_status_t status generate_volatile_ecc_key(key_id); printf(psa_generate_key status 0x%08x (%d)\r\n, (uint32_t)status, (int32_t)status);输出是psa_generate_key status 0xffffff7f (-129)0xffffff7f强转成int32_t就是 -129对应PSA_ERROR_NOT_PERMITTED。AES 密钥复现方式一模一样psa_key_attributes_t attr PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(attr, PSA_KEY_TYPE_AES); psa_set_key_bits(attr, 128); psa_set_key_lifetime(attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); psa_set_key_algorithm(attr, PSA_ALG_CCM); status psa_generate_key(attr, key_id);结果一样是 -129。1.3 错误码怎么读-129 不是一个常规参数错误这里先提一个排查时容易忽略的细节。PSA Crypto 规范里的错误码是负数但在不同的编译环境下打印出来长得不一样以int32_t打印-129 就是-129以uint32_t或者psa_status_t输出看到的是0xFFFFFF7F如果代码里不小心把psa_status_t截断成int8_t你会看到0x7F也就是 127完全没法判断。所以排查的第一步先确认你的打印逻辑没有把错误码截断。我见过不少人在论坛上问为什么返回 127其实就是 -129 被int8_t截断了。建议统一写成printf(status %d (0x%08x)\r\n, (int32_t)status, (uint32_t)status);这样有符号、无符号两种形态都看得明白。2. PSA_ERROR_NOT_PERMITTED 到底在拒绝什么2.1 PSA 标准语义按 PSA Crypto API 规范psa_generate_key()返回PSA_ERROR_NOT_PERMITTED的场景主要有两类调用者没有执行该操作的权限密钥属性里配置的策略不允许该操作典型情况是 usage flags 与算法不匹配或者试图对一个策略受限的密钥做超出策略范围的事。这两类语义一个是你是谁一个是你想干的事合不合规。注意它跟PSA_ERROR_INVALID_ARGUMENT的区别参数错误是说你传的东西本身就非法而 NOT_PERMITTED 是说东西合法但你没权限或者策略不允许。2.2 Secure Manager 的双重校验STM32H573 的 Secure Manager 不是一个简单的密码学函数库它底层是一个基于 ARM TrustZone 安全分区管理的完整安全服务框架。非安全侧调用psa_generate_key()时实际路径是NS 应用 → veneer 接口 → SPM 安全分区管理 → Secure Manager Crypto 服务 → 密钥策略检查 → 生成密钥这条链路里至少有两层策略判断第一层是 SPM 层面对调用者身份的检查。非安全侧应用必须先建立合法的 PSA Client 连接SPM 才会把请求路由到 Crypto 安全分区。如果这一步没通过返回的基本就是PSA_ERROR_NOT_PERMITTED。第二层才是 Crypto 分区内部的密钥策略检查。Secure Manager 里的安全策略可能比通用 TF-M 更严格或者说它会把某些在标准 PSA 里属于不支持的情况也归并成不允许。所以同样是 -129在普通 MCU 上跑 TF-M 和在 H573 上跑 Secure Manager根因可能完全不同。这也是我最开始被误导的原因我一直在改密钥属性结果真正的问题在服务访问层。2.3 为什么这个问题容易误判我复盘下来误判有三个原因第一PSA_ERROR_NOT_PERMITTED这个名字太容易让人联想到权限位。大家习惯性去看 usage flags很少有人第一时间怀疑是底层服务连接的问题。第二AES 和 ECC 同时失败反而让我觉得不是密钥类型的问题于是专注在共同点上——易失性生命周期、usage flags、algorithm。这三个恰恰是最容易出问题的点结果绕了一圈。第三Secure Manager 的报错不是特别细。很多内部校验失败都被统一折叠成 -129你没法从错误码本身猜出是哪一层拒的。这时候只能靠逐层剥离验证来定位而不是盯着错误码猜。3. 排查链路从密钥策略到服务访问逐层剥离3.1 第一层属性策略逐字段核对先把最基础的 PSA 属性检查一遍排除策略本身就不合规的情况。我当时做了一轮代码走查几个容易出问题的字段逐一确认字段正确要求我当时的配置结论key typeECC 密钥对要带曲线族PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)正确key bitsP-256 对应 256psa_set_key_bits(attr, 256)正确lifetime易失性密钥用PSA_KEY_LIFETIME_VOLATILEPSA_KEY_LIFETIME_VOLATILE正确usage flagsECC 签名需要PSA_KEY_USAGE_SIGNPSA_KEY_USAGE_SIGN | PSA_KEY_USAGE_VERIFY正确algorithmECDSA 需要指定哈希PSA_ALG_ECDSA(PSA_ALG_SHA_256)正确肉眼看着全对。但我还是做了两件事第一把psa_key_attributes_t结构体在调用前后完整 dump 出来确认每个字段真是我们设置的值第二把 usage flags 精简到最小集合只留PSA_KEY_USAGE_SIGN排除flags 过多导致的策略冲突。结果仍然 -129。这一层基本可以排除代码里属性设置错误。这里还有个细节值得提醒psa_key_attributes_t attr PSA_KEY_ATTRIBUTES_INIT;这条初始化语句不能省。如果像裸机代码那样声明一个局部变量不初始化结构体里的 lifetime、usage、algorithm 字段全是栈上的随机值Secure Manager 解析策略时可能直接判定为策略非法/不允许而且报的很可能就是 NOT_PERMITTED 而不是 INVALID_ARGUMENT。我当时确认了初始化没写错才敢往下走。3.2 第二层易失性与持久性分治既然易失性密钥失败那就试试持久性密钥用来判断问题是不是出在易失性这个属性上。psa_set_key_lifetime(attr, PSA_KEY_LIFETIME_PERSISTENT); psa_set_key_id(attr, 0x00000001); // 持久性密钥需要显式指定 key id结果很有意思持久性 AES 密钥能生成成功持久性 ECC 密钥也能生成成功。这意味着 Secure Manager 的 Crypto 服务本身是可用的密钥类型也支持问题被进一步压缩到易失性密钥路径这一个分支。我当时的第一个猜测是Secure Manager 的默认配置里非安全侧可能不允许创建易失性密钥或者对易失性密钥有额外的使用限制。为了验证这个猜测我把密钥生成函数搬到安全侧示例工程里跑——结果安全侧生成易失性密钥完全正常。这就很奇怪了。同一把 Secure Manager安全侧能生成易失性密钥非安全侧不行。说明问题不在 Crypto 服务本身而在非安全侧调用链路。3.3 第三层SM 服务初始化与 veneer 库回头看调用链路的起点非安全侧应用是通过 Secure Manager 的 veneer 接口发起调用的。Secure Manager 在非安全侧有一个配套的接口库里面包含 PSA API 到 SPM IPC 的转封装以及启动时与安全侧握手初始化的逻辑。我检查工程配置时发现一个问题我的工程链接了 Secure Manager 的 NS 接口库但代码里完全没有调用 Secure Manager 的启动/握手接口直接上来就psa_crypto_init()然后psa_generate_key()。psa_crypto_init()在 H573 的 Secure Manager 环境里并不是一个纯软件初始化它底层要跟安全侧的 Crypto 服务建立 PSA Client 连接。如果 Secure Manager 的服务没有先就绪或者 NS 侧没有完成必要的初始化步骤psa_generate_key()请求到了 SPM 层就会被判定为非授权调用。对照 ST 官方例程后确认H573 的 Secure Manager 非安全侧例程里main 函数最开始有一段 Secure Manager 初始化流程把安全侧服务准备好之后才会进入业务逻辑。我把这段初始化补上再跑/* 伪代码Secure Manager NS 侧初始化 */ SM_Init(); // 建立与安全侧通信的基础环境 psa_crypto_init(); // 初始化 PSA Crypto 客户端补完初始化之后AES 易失性密钥生成立刻正常了。但 ECC 仍然报 -129。也就是说问题分成了两段第一段是服务初始化/握手导致的整体拒绝第二段是 ECC 密钥策略在 Secure Manager 侧的额外限制。3.4 第四层ECC 的 export 策略与算法歧义把 ECC 属性逐个字段做二分排除后最终定位到PSA_KEY_USAGE_EXPORT这个 flag 上。我当时在 ECC 密钥的 usage flags 里加上了PSA_KEY_USAGE_EXPORT理由很朴素TLS 握手阶段需要导出公钥发给对端。在通用 PSA 实现里公钥导出通常被PSA_KEY_USAGE_EXPORT覆盖加上没毛病。但 Secure Manager 对 ECC 密钥对的管理更严格由安全侧生成的 ECC 私钥默认不允许导出即使你在 usage flags 里显式要求 EXPORTSecure Manager 的策略引擎也会直接拒绝整个密钥生成请求返回PSA_ERROR_NOT_PERMITTED。这在语义上是说得通的Secure Manager 的核心卖点就是私钥永远不离开安全世界如果你在策略里声明要导出私钥它干脆连生成都不让你生成。PSA 规范里并没有强制要求实现方必须允许所有 usage 组合Secure Manager 选择了更保守的策略。把PSA_KEY_USAGE_EXPORT从 ECC 密钥的 usage flags 里去掉保留PSA_KEY_USAGE_SIGN | PSA_KEY_USAGE_VERIFY再次调用psa_generate_key status 0 (0)ECC 易失性密钥生成通过。至此两个问题都定位到了。4. 根因确认与修复ECC/AES 易失性密钥生成的正解4.1 修复后的 ECC 密钥生成最终确认可用的 ECC P-256 易失性密钥生成代码如下static psa_status_t generate_volatile_ecc_key(psa_key_id_t *out_key_id) { psa_status_t status psa_crypto_init(); if (status ! PSA_SUCCESS) { return status; } psa_key_attributes_t attr PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); psa_set_key_bits(attr, 256); psa_set_key_lifetime(attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(attr, PSA_KEY_USAGE_SIGN | PSA_KEY_USAGE_VERIFY); psa_set_key_algorithm(attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); status psa_generate_key(attr, out_key_id); psa_reset_key_attributes(attr); return status; }两个关键点不要加PSA_KEY_USAGE_EXPORT。如果后续业务需要在网络报文中携带公钥用psa_export_public_key()单独导出公钥而不是通过PSA_KEY_USAGE_EXPORT去解锁私钥的导出权限。PSA_ALG_ECDSA(PSA_ALG_SHA_256)这种算法写法是推荐做法。有些代码里写PSA_ALG_ECDSA_BASE或PSA_ALG_ECDSA_ANY在 Secure Manager 某些版本里会被当成策略不完整而拒绝。显式指定哈希算法策略最清晰踩坑最少。4.2 修复后的 AES 密钥生成AES 部分相对简单修复后的代码static psa_status_t generate_volatile_aes_key(psa_key_id_t *out_key_id) { psa_status_t status psa_crypto_init(); if (status ! PSA_SUCCESS) { return status; } psa_key_attributes_t attr PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(attr, PSA_KEY_TYPE_AES); psa_set_key_bits(attr, 128); psa_set_key_lifetime(attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); psa_set_key_algorithm(attr, PSA_ALG_CCM); status psa_generate_key(attr, out_key_id); psa_reset_key_attributes(attr); return status; }如果 AES 密钥后续要用于 GCM、CTR 或者 CBCpsa_set_key_algorithm()里改成对应的PSA_ALG_GCM、PSA_ALG_CTR、PSA_ALG_CBC_NO_PADDING即可。但注意usage flags 要跟算法匹配。比如使用PSA_ALG_AEAD系的算法既要ENCRYPT又要DECRYPT就都要写上如果只做加密只写PSA_KEY_USAGE_ENCRYPT反而更安全。4.3 生成后的验证闭环密钥生成之后不能只看返回值必须做一次闭环验证确认这把密钥真的能用。推荐做法是立刻用生成的密钥跑一次与该密钥算法匹配的操作比如 ECC 密钥做一次psa_sign_message()/psa_verify_message()AES 密钥做一次对称加解密static psa_status_t verify_ecc_key_works(psa_key_id_t key_id) { uint8_t message[32] {0}; uint8_t signature[64] {0}; size_t sig_len 0; psa_status_t status; status psa_sign_message(key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), message, sizeof(message), signature, sizeof(signature), sig_len); if (status ! PSA_SUCCESS) { return status; } status psa_verify_message(key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), message, sizeof(message), signature, sig_len); return status; }AES 的验证类似用psa_cipher_encrypt()/psa_cipher_decrypt()或者直接跑一个 AEAD 加解密。这一步能帮你确认生成的密钥不仅在 Secure Manager 里存在而且真正能参与业务运算。5. 工程复盘Secure Manager 开发值得注意的几个点5.1 例程先行别直接照抄通用 PSA 代码STM32H573 的 Secure Manager 虽然对外暴露的是标准 PSA Crypto API但它毕竟是一个独立的安全服务产品不是所有标准 PSA 行为都被 100% 继承。开发前最好先跑一遍 ST 官方提供的 Secure Manager 例程尤其是密钥生成 安全存储 认证那个组合例程确认你手上的 Secure Manager 固件版本和 NS 接口库版本再动自己的业务代码。我当时如果先跑例程就不会在 EXPORT 这个 flag 上浪费那么多时间——例程里的 ECC 密钥生成几乎没有加 EXPORT 的。这属于看了文档但没看例程的典型教训。5.2 密钥策略要够用就好经过这次踩坑我把密钥策略的设计原则改成了最小授权不用的 usage flag 一律不设不导出的私钥坚决不写PSA_KEY_USAGE_EXPORT算法尽量显式指定少用_ANY、_BASE这类宽松写法易失性密钥用途明确后及时psa_destroy_key()不让密钥生命周期拖到系统重启。尤其是 ECC 私钥Secure Manager 的设计目标就是保护私钥不离开安全世界。你在策略里声明 EXPORT等于跟安全模型对着干被拒绝是合理的。搞清楚这个设计意图之后很多看似不合理的返回码其实都能解释通。5.3 调试时把错误码打印做扎实这次排查里最花时间的不是代码逻辑而是错误码打印不完整。Secure Manager 的错误码链路长返回值在不同层会被折叠、透传光看一个 -129 很难判断是哪一层的问题。建议在应用里封装一个统一的 PSA 状态打印函数const char *psa_status_str(psa_status_t status) { switch (status) { case PSA_SUCCESS: return PSA_SUCCESS; case PSA_ERROR_NOT_PERMITTED: return PSA_ERROR_NOT_PERMITTED; case PSA_ERROR_INVALID_ARGUMENT: return PSA_ERROR_INVALID_ARGUMENT; case PSA_ERROR_NOT_SUPPORTED: return PSA_ERROR_NOT_SUPPORTED; case PSA_ERROR_BAD_STATE: return PSA_ERROR_BAD_STATE; case PSA_ERROR_COMMUNICATION_FAILURE: return PSA_ERROR_COMMUNICATION_FAILURE; default: return UNKNOWN; } } #define PSA_CHECK(expr) do { \ psa_status_t s_ (expr); \ if (s_ ! PSA_SUCCESS) { \ printf(%s failed: %s (%d)\r\n, #expr, psa_status_str(s_), (int32_t)s_); \ return s_; \ } \ } while (0)排查时可以只关注状态字符串不用每次对着表格查 -129 到底是谁。5.4 Secure Manager 开发的方向性建议最后说点工程上的体会。Secure Manager 这类方案的价值在于安全能力是出厂预置的应用开发只需要关注非安全侧业务不用自己维护安全固件和密钥分区。但它也带来了新的约束——你失去了对安全侧实现的完全控制必须接受它定义的策略边界。比如私钥不可导出、某些 usage 组合被拒绝、易失性密钥受调用上下文限制等都属于这类边界。我的建议是在项目早期把所有要用的 PSA API 都列出来逐个在目标 Secure Manager 版本上跑一遍冒烟测试把哪些写法会被拒绝提前摸清楚。这比写到一半再来查 -129 高效得多。另外不同版本的 Secure Manager 固件行为可能有差异。如果碰到文档和实测不一致的情况优先看 STM32CubeH5 固件包里的 Release Notes 和 Secure Manager 集成指南里面的已知限制列表往往藏着答案。这次 ECC 私钥导出限制在最新版文档的安全策略章节里就有明确描述只是平时不容易注意到。