当前位置: 首页 > news >正文

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 相关组件,编译链接了官方提供的 NS(Non-Secure)侧库。应用本身跑在非安全世界,业务逻辑很简单:系统启动后,动态生成一把 ECC P-256 密钥对用于 TLS 客户端证书协商,再生成一把 AES-128 密钥用于通信数据加密。

问题就出在密钥生成这一步。无论 ECC 还是 AES,只要密钥生命周期设置为易失性(volatile),psa_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的场景主要有两类:

  1. 调用者没有执行该操作的权限;
  2. 密钥属性里配置的策略不允许该操作,典型情况是 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 里显式要求 EXPORT,Secure 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; }

两个关键点:

  1. 不要加PSA_KEY_USAGE_EXPORT。如果后续业务需要在网络报文中携带公钥,用psa_export_public_key()单独导出公钥,而不是通过PSA_KEY_USAGE_EXPORT去"解锁"私钥的导出权限。
  2. PSA_ALG_ECDSA(PSA_ALG_SHA_256)这种算法写法是推荐做法。有些代码里写PSA_ALG_ECDSA_BASEPSA_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 或者 CBC,psa_set_key_algorithm()里改成对应的PSA_ALG_GCMPSA_ALG_CTRPSA_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 私钥导出限制,在最新版文档的安全策略章节里就有明确描述,只是平时不容易注意到。

http://www.cnnetsun.cn/news/4305829.html

相关文章:

  • Spring Boot在线考试系统毕设项目深度拆解:从设计到部署
  • Vibe Coding实战:自然语言驱动个人网站设计与迭代
  • GPT4Free LMArenaProvider 报错修复:4 步自查清单
  • 如何用graphify搭建个人第二大脑?从/raw文件夹到可查询图谱
  • drawio-desktop 安装教程:5 分钟跑起来,顺手把批量导出接进流水线
  • Docling 完整指南:5 分钟把 PDF、DOCX 变成 AI 能读懂的结构化数据
  • Penpot快速上手:免费开源的网页端设计协作工具完整实战指南
  • open-code-review团队引入指南:30分钟让团队用起AI代码评审
  • graphify安全模型全解析:10个威胁向量与逐一缓解措施
  • STM32N6570裸机I3C驱动移植:VL53L9 ToF传感器从V4L2到MCU实战
  • 批量文本处理方法对比:脚本、CLI与API接口的选型与最佳实践
  • 腾讯暑期实习生笔试题复盘:构造回文、字符移位与有趣的数字
  • Llmem:为AI编程助手的本地持久化记忆,解决上下文丢失痛点
  • 受限设备的上线配置管理
  • AI语音助手应用开发实战:配额管理、成本控制与免费/收费模式技术实现
  • 并发服务在本地跑通,先搭一个能复现问题的环境
  • oh-my-pi conflict:// 实战:一行 @theirs 搞定所有 Git 合并冲突
  • 10T参数预训练大模型解析:从Scaling Law到工程实践
  • 5分钟跑通drawio-desktop:本地流程图绘制工具新手上手指南
  • Java Lambda表达式:从匿名内部类到函数式编程的实践指南
  • 从零搭建参数服务器架构:分布式深度学习实战与避坑指南
  • Plane 快速上手指南:4 天从零部署开源项目管理工具,跑通你的第一个项目
  • 强化学习(RL)为何是 LLM 绕不开的关键:从 RLHF 到 PPO 与 DPO
  • 实操指南:120 个精选资源,如何快速配好你的 Claude Code
  • k-skill Olive Young 搜索指南:门店、商品、库存三合一查询
  • 语言模型评测不能只看演示
  • STM32L452 USB切换GPIO失效?引脚被USB外设覆盖的根因与解决方案
  • Mole能力边界清单:macOS之外,哪些清理与监控能力能直接用
  • no-mistakes如何把SKILL.md装进Claude Code:agent技能安装原理
  • STEVAL-CTM015V1上SRM电机位置传感器配置实战指南