尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32H573 TLS 1.3客户端HKDF不支持问题与Secure Manager替代方案

STM32H573 TLS 1.3客户端HKDF不支持问题与Secure Manager替代方案 STM32H573 上做 TLS 1.3 客户端卡在hkdf extract not supported这个错误上前前后后折腾了两天。换了不少路子最后发现根子不在 TLS 协议栈上而在 Secure Manager 只暴露了有限 PSA Crypto APIHKDF 这种密钥派生服务根本没被实现。这篇文章把思路、代码、验证方法完整记录下来给后面踩同一个坑的人省点时间。这篇文章适合手头有 STM32H573或者其他带 Secure Manager 的 H5 系列、正在做 TLS 1.3 接入、又不想放弃硬件安全能力的嵌入式工程师看。如果你只是单纯用 mbedTLS 做纯软件 TLS不涉及 Secure Manager那这篇文章的参考价值有限但 HKDF 的原理和测试向量部分依然可以复用。1. 先把背景讲清楚Secure Manager 到底管了什么1.1 STM32H573 和 Secure Manager 的设计逻辑STM32H573 用的是 Cortex-M33 内核带 TrustZone也就是说芯片默认就把世界分成了安全侧Secure World和非安全侧Non-Secure World。Secure Manager 是 ST 在出厂时预烧在安全侧的一个固件特定型号和批次支持也可以自己通过工具烧录它对外提供一套基于 PSA Certified API 的安全服务比如密钥管理、签名验签、哈希计算、AES/ECC 加解密等。应用跑在非安全侧通过psa_xxx系列函数调用安全侧的服务。这套设计对大多数团队来说是好事。原来要自己写安全固件、管理 TrustZone 边界、隔离密钥存储现在只需要在非安全侧调 API 就行。代价也明显你只能用 Secure Manager 暴露出来的服务它没实现的算法你叫破喉咙也没用而且不像普通库函数你可以自己改源码加功能Secure Manager 是二进制黑盒。我在这个项目里就撞上了它的边界——HKDF。1.2 TLS 1.3 密钥调度为什么依赖 HKDFTLS 1.3 和 TLS 1.2 的一个核心区别是TLS 1.3 的密钥调度完全建立在 HKDF 之上。它不再是简单地从预主密钥做 PRF 派生而是通过一系列HKDF-Extract和HKDF-Expand操作把 PSK、ECDHE 共享密钥、随机数等内容一步步“搅拌”成各个阶段的工作密钥。HKDF 本身定义在 RFC 5869分成两个基本步骤Extract提取PRK HMAC-Hash(salt, IKM)输入是初始密钥材料 IKM比如 ECDHE 共享密钥和盐 saltTLS 1.3 里通常是一个全是 0 的派生密钥或者 PSK输出是一段定长的伪随机密钥 PRK。这一步的目标是把 IKM 中可能存在的非均匀熵“提取”成均匀分布的密钥。Expand扩展OKM T(1) || T(2) || ...其中T(i) HMAC-Hash(PRK, T(i-1) || info || byte(i))把 PRK 扩展成长度任意、带有上下文信息 info 的输出密钥材料。在 TLS 1.3 密钥调度里这两个操作被反复使用early_secret HKDF-Extract(0, PSK)无 PSK 时PSK 位置用全 0 替代derived HKDF-Expand-Label(early_secret, derived, , 32)handshake_secret HKDF-Extract(derived, ECDHE_shared_secret)再通过HKDF-Expand-Label派生出client_handshake_traffic_secret、server_handshake_traffic_secret握手结束后继续用同样方式派生出master_secret和应用流量密钥这里的HKDF-Expand-Label是 TLS 1.3 对HKDF-Expand的封装它在 info 里塞了一个特定格式的HkdfLabel结构而且所有 label 前面都固定带一个tls13 前缀。这个前缀是 TLS 1.3 和一般 HKDF 使用方式最大的区别很多人在自己实现的时候容易在这里翻车。你看这一套流程下来HKDF-Extract和HKDF-Expand缺一不可。只要有一个操作得不到底层支持整个 TLS 1.3 密钥调度就断掉了。1.3 为什么 Secure Manager 会不支持 HKDF问题就出在 Secure Manager 暴露的 PSA Crypto API 上。PSA Crypto API 的密钥派生流程通常是这样的psa_key_derivation_operation_t op PSA_KEY_DERIVATION_OPERATION_INIT; psa_key_derivation_setup(op, PSA_ALG_HKDF(PSA_ALG_SHA_256)); psa_key_derivation_input_bytes(op, PSA_KEY_DERIVATION_INPUT_SECRET, ikm, ikm_len); psa_key_derivation_input_bytes(op, PSA_KEY_DERIVATION_INPUT_SALT, salt, salt_len); psa_key_derivation_output_bytes(op, okm, okm_len);这套接口在 PSA 规范里是标准定义但注意接口标准不代表服务端实现完整。Secure Manager 作为一个量产的、需要满足一定内存和性能约束的固件它只会实现 PSA 规范中的一个子集。我手头这个版本的 Secure Managerpsa_key_derivation_setup一旦传入PSA_ALG_HKDF就返回PSA_ERROR_NOT_SUPPORTED。我从调试串口看到的日志就是类似hkdf extract/expand not supported这样的报错。看调用栈错误是从 mbedTLS 的 TLS 1.3 密钥调度模块ssl_tls13_keys.c里传出来的它内部调用了 PSA 的密钥派生接口。也就是说TLS 库本身没问题问题出在底层 PSA 服务根本没实现 HKDF 算法。注意在这里“不支持”分两种情况。一种是 Secure Manager 的 PSA 驱动层直接返回PSA_ERROR_NOT_SUPPORTED另一种是库层面因为宏配置问题整个PSA_WANT_ALG_HKDF都没有被使能。排查时先区分清楚是哪种情况避免白改代码。2. 问题定位别急着改库先确认错误到底从哪来2.1 错误出现的现场我当时的开发环境是 STM32H573 Discovery Kit软件栈是 STM32CubeFW_H5 固件包 mbedTLSTF-PSA-Crypto 分支Secure Manager 通过stm32h5_psa驱动接入。板子上电后TLS 1.2 握手一切正常但把协议版本切到 TLS 1.3客户端发起ClientHello后服务端回应了ServerHello接着在计算握手密钥时直接失败。调试串口打出来的日志是[tls] key schedule failed: -135 (PSA_ERROR_NOT_SUPPORTED) hkdf extract/expand not supported随后客户端直接关闭连接服务端那边看日志是 TLS 握手中断没有收到Finished消息。我第一次看到这个日志还以为是 mbedTLS 配置问题检查了mbedtls_config.h里的MBEDTLS_SSL_PROTO_TLS1_3、PSA_WANT_ALG_HKDF等宏全部正常。后来把问题定位到psa_key_derivation_setup调用才发现是 Secure Manager 返回的错误。2.2 从 PSA Crypto API 的角度去理解这个错误PSA Crypto API 的密钥派生设计是分阶段式的setup时指定算法input时喂入各段材料output时取出结果。整个流程中任何一个环节如果驱动层不支持该算法都会在setup阶段直接失败。这是设计如此不是为了给你添堵而是让你提前知道这个算法能不能用避免在派生中途才发现算不出来。在 STM32H573 上Secure Manager 的 PSA 实现支持比较完善的是哈希类SHA-256/SHA-384、对称加解密类AES-CBC/GCM、非对称类ECDSA/ECDH这些基础原语。但像 HKDF、TLS 1.2 PRF 这类“组合型”算法就属于可实现可不实现的部分。ST 的 Release Notes 里通常有一张 API 支持列表排查时一定要先查这个表而不是去猜。2.3 确认根因后我选出的路线根因确认了Secure Manager 不支持 HKDF 的 Extract/Expand。接下来有三个可选路线在非安全侧用纯软件实现完整的 HKDF完全不依赖 Secure Manager。用 Secure Manager 提供的 SHA-256 哈希服务在非安全侧基于哈希自己拼出 HMAC再实现 HKDF。修改 mbedTLS 的 TLS 1.3 密钥调度模块绕开 PSA 的 KDF 接口直接调用 mbedTLS 自带的mbedtls_hkdf软件实现。我最终选了方案 2。原因有三个第一完全走软件方案 1会把哈希计算全部丢给 CPUHMAC-SHA-256 本身计算量不小TLS 1.3 握手阶段要派生好几轮密钥性能上不划算第二方案 3 需要改动 mbedTLS 库内部的 TLS 1.3 状态机这类改动很容易破坏握手兼容性后续库升级也麻烦第三Secure Manager 的 SHA-256 是经过验证的硬件加速实现能确保数据在处理过程中不暴露给非安全侧安全性比纯软件实现更可控。实测下来让 Secure Manager 做底层哈希、自己在非安全侧拼 HMAC 和 HKDF 逻辑整个方案的性能和安全性都更均衡。纯软件方案只是保底除非你确认项目对性能完全无所谓否则不推荐。3. 可行方案在 Non-secure 侧实现 HKDF3.1 方案对比与选择依据我做了个小对比表方便你按自己的项目需求选方案实现方式安全强度性能实现难度依赖ASecure Manager 提供 HMAC高HMAC 全程在安全侧高低需要 Secure Manager 支持 HMAC 算法BSecure Manager 提供 SHA-256自己拼 HMAC较高哈希计算在安全侧拼接逻辑在非安全侧中高中需要 Secure Manager 支持 SHA-256C完全软件实现 HKDF取决于软件实现质量低低无如果你的 Secure Manager 版本支持PSA_ALG_HMAC直接方案 A 最省事。但我这个版本不支持单独的 HMAC这不算罕见很多 PSA 实现只保证哈希和加解密原语HMAC 这种“组合算法”也不在列表里所以走了方案 B。3.2 HKDF-Extract 的软件实现细节HKDF-Extract 的定义是PRK HMAC-Hash(salt, IKM)也就是说用盐作为 HMAC 的密钥对 IKM 做一次 HMAC。比如 TLS 1.3 握手过程中handshake_secret HKDF-Extract(derived_secret, ECDHE_shared_secret)。如果 Secure Manager 没有提供 HMAC而是只提供了哈希服务那就需要自己构造 HMAC。HMAC 的标准构造方式是HMAC(K, text) H((K XOR opad) || H((K XOR ipad) || text))其中K是经过填充的密钥如果密钥长度大于哈希块大小SHA-256 的块大小是 64 字节就先对密钥做哈希否则直接使用末尾补 0 到块大小。ipad和opad分别是 0x36 和 0x5C 填充块。我把这个逻辑封装成了集成 Secure Manager 哈希服务的 HMAC 函数。核心代码如下#include psa/crypto.h #include string.h #define SHA256_BLOCK_SIZE 64 #define SHA256_DIGEST_SIZE 32 static psa_status_t hash_sha256(const uint8_t *data, size_t data_len, uint8_t *digest, size_t *digest_len) { psa_hash_operation_t op PSA_HASH_OPERATION_INIT; psa_status_t status; status psa_hash_setup(op, PSA_ALG_SHA_256); if (status ! PSA_SUCCESS) { return status; } status psa_hash_update(op, data, data_len); if (status ! PSA_SUCCESS) { return status; } status psa_hash_finish(op, digest, SHA256_DIGEST_SIZE, digest_len); return status; } static psa_status_t hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *text, size_t text_len, uint8_t *hmac_out, size_t *hmac_out_len) { uint8_t k_pad[SHA256_BLOCK_SIZE] {0}; uint8_t inner_hash[SHA256_DIGEST_SIZE]; size_t inner_hash_len 0; uint8_t outer_input[SHA256_BLOCK_SIZE SHA256_DIGEST_SIZE]; psa_status_t status; // 密钥过长时先做哈希否则补零到块大小 if (key_len SHA256_BLOCK_SIZE) { status hash_sha256(key, key_len, k_pad, inner_hash_len); if (status ! PSA_SUCCESS) { return status; } } else { memcpy(k_pad, key, key_len); } // H((K XOR ipad) || text) uint8_t inner_input[SHA256_BLOCK_SIZE 512]; // 按需调整 for (int i 0; i SHA256_BLOCK_SIZE; i) { inner_input[i] k_pad[i] ^ 0x36; } memcpy(inner_input SHA256_BLOCK_SIZE, text, text_len); status hash_sha256(inner_input, SHA256_BLOCK_SIZE text_len, inner_hash, inner_hash_len); if (status ! PSA_SUCCESS) { return status; } // H((K XOR opad) || inner_hash) for (int i 0; i SHA256_BLOCK_SIZE; i) { outer_input[i] k_pad[i] ^ 0x5C; } memcpy(outer_input SHA256_BLOCK_SIZE, inner_hash, SHA256_DIGEST_SIZE); status hash_sha256(outer_input, SHA256_BLOCK_SIZE SHA256_DIGEST_SIZE, hmac_out, hmac_out_len); return status; }有个细节要提醒inner_input这个临时缓冲我初始给了SHA256_BLOCK_SIZE 512的容量实际使用时你要根据 text 的最大长度来裁剪或改成动态分配。在 TLS 1.3 里HKDF-Extract的 IKM 通常是 ECDHE 共享密钥长度一般是 32 或 48 字节超过 256 字节的情况很少但HKDF-Expand里被哈希的数据还可能包括上一轮的输出和 label 信息所以缓冲要留足。然后在 HMAC 之上实现 HKDF-Extract 就非常简单了psa_status_t hkdf_extract(const uint8_t *salt, size_t salt_len, const uint8_t *ikm, size_t ikm_len, uint8_t *prk, size_t *prk_len) { return hmac_sha256(salt, salt_len, ikm, ikm_len, prk, prk_len); }注意这里盐可能为空为空时按照 RFC 5869用全 0 填充到块大小作为 HMAC 密钥。hmac_sha256里key_len为 0 时memcpy不会执行k_pad自然就是全 0逻辑上是正确的。3.3 HKDF-Expand 的软件实现细节HKDF-Expand的定义是T(0) 空字符串 T(i) HMAC-Hash(PRK, T(i-1) || info || byte(i)) OKM T(1) || T(2) || ... || (截断到所需长度)这里 PRK 作为 HMAC 密钥info 是上下文信息byte(i) 是从 0x01 开始递增的整数字节。实现时要注意上一轮的输出会被完整拼接进下一轮的输入所以必须维护一个previous_output缓冲。代码如下psa_status_t hkdf_expand(const uint8_t *prk, size_t prk_len, const uint8_t *info, size_t info_len, uint8_t *okm, size_t okm_len) { uint8_t previous[SHA256_DIGEST_SIZE] {0}; size_t previous_len 0; uint8_t counter 1; size_t generated 0; while (generated okm_len) { uint8_t input[SHA256_DIGEST_SIZE * 2 128]; // 按需调整 size_t input_len 0; if (previous_len 0) { memcpy(input input_len, previous, previous_len); input_len previous_len; } memcpy(input input_len, info, info_len); input_len info_len; input[input_len] counter; size_t block_len 0; psa_status_t status hmac_sha256(prk, prk_len, input, input_len, previous, block_len); if (status ! PSA_SUCCESS) { return status; } previous_len block_len; size_t to_copy (okm_len - generated block_len) ? (okm_len - generated) : block_len; memcpy(okm generated, previous, to_copy); generated to_copy; counter; } return PSA_SUCCESS; }这个实现里有两个容易踩坑的点。第一个是 counter 溢出HKDF-Expand要求输出长度不能超过255 * HashLen对 SHA-256 来说就是 8160 字节TLS 1.3 里不会用到这么大但如果你在别处复用了这段代码务必加上长度检查。第二个是previous缓冲是定长 32 字节的如果 Secure Manager 提供的哈希算法不是 SHA-256 而是 SHA-384块大小 48 字节这个缓冲要相应调整。3.4 TLS 1.3 的 HKDF-Expand-Label 包装TLS 1.3 并不是直接用HKDF-Expand而是用HKDF-Expand-Label。它规定HKDF-Expand-Label(Secret, Label, Context, Length) HKDF-Expand(Secret, HkdfLabel, Length) HkdfLabel uint16(Length) || uint8(len(Label)) || Label || uint8(len(Context)) || Context其中 Label 前面还带一个固定的tls13 前缀。也就是说传给HKDF-Expand的 info 不是简单的业务标签而是一个精心构造的结构体。这里我提供一个完整的tls13_hkdf_expand_label实现psa_status_t tls13_hkdf_expand_label(const uint8_t *secret, size_t secret_len, const char *label, size_t label_len, const uint8_t *context, size_t context_len, uint8_t *out, size_t out_len) { uint8_t hkdf_label[256]; size_t hkdf_label_len 0; static const char tls13_prefix[] tls13 ; size_t full_label_len label_len sizeof(tls13_prefix) - 1; // HKDF-Label uint16(Length) || uint8(len(Label)) || tls13 || Label || uint8(len(Context)) || Context hkdf_label[hkdf_label_len] (uint8_t)((out_len 8) 0xFF); hkdf_label[hkdf_label_len] (uint8_t)(out_len 0xFF); hkdf_label[hkdf_label_len] (uint8_t)full_label_len; memcpy(hkdf_label hkdf_label_len, tls13_prefix, sizeof(tls13_prefix) - 1); hkdf_label_len sizeof(tls13_prefix) - 1; memcpy(hkdf_label hkdf_label_len, label, label_len); hkdf_label_len label_len; hkdf_label[hkdf_label_len] (uint8_t)context_len; memcpy(hkdf_label hkdf_label_len, context, context_len); hkdf_label_len context_len; return hkdf_expand(secret, secret_len, hkdf_label, hkdf_label_len, out, out_len); }这个函数里最容易错的地方就是full_label_len的计算。我见过有人把tls13 前缀的长度漏算或者把uint16(Length)的 Length 填成了HkdfLabel的长度而不是输出长度这两种错都会导致派生的密钥和服务端不一致。调试这种问题特别痛苦因为握手双方各自算出来的密钥不同表现就是服务端收到Finished后校验失败或者客户端直接报bad_record_mac。我的建议是先把 RFC 8448 里的测试向量跑一遍确认无误再接 TLS 握手。4. 实操过程完整实现与验证4.1 硬件环境和软件栈我用来验证的参考环境硬件STM32H573 Discovery Kit固件包STM32CubeFW_H5版本建议用当前最新Secure Manager通过 STM32TrustedPackageCreator 烧录或出厂预置注意核对版本号TLS 库mbedTLS 3.xTF-PSA-Crypto 分支驱动stm32h5_psaPSA 驱动IDESTM32CubeIDE4.2 在现有代码里接入自定义 HKDF我把上一节的三段代码整理成了hkdf_psa.c对外暴露了hkdf_extract、hkdf_expand和tls13_hkdf_expand_label三个函数。接入到 mbedTLS 的时候我的做法不是直接改 mbedTLS 源码而是通过配置宏让 TLS 1.3 密钥调度模块在依赖 PSA 密钥派生时失效切换到自己的实现。在mbedtls_config.h里我做了两处修改关闭会让 mbedTLS 调用 PSA HKDF 的宏路径例如PSA_WANT_ALG_HKDF保持关闭或者具体看MBEDTLS_PSA_CRYPTO_C的配置。保留MBEDTLS_SSL_PROTO_TLS1_3确保 TLS 1.3 协议本身不被裁剪。然后在 mbedTLS 的library/ssl_tls13_keys.c里把原来调用psa_key_derivation_*的地方替换成调用自己的tls13_hkdf_expand_label。具体替换点有mbedtls_ssl_tls13_hkdf_expand_labelmbedtls_ssl_tls13_generate_handshake_secretmbedtls_ssl_tls13_generate_application_secret这些函数的共同点是最终都会走到 HKDF 的派生逻辑。替换的时候要小心函数签名mbedTLS 内部传参是mbedtls_md_type_t、const unsigned char *这些类型我的实现用的是uint8_t *需要做一层适配。更简单的替代做法如果你的 mbedTLS 版本里mbedtls_hkdf是独立的一个函数并且底层没有强制走 PSA那可以直接在编译链接层面把mbedtls_hkdf符号重定向到自己的实现。不过实测下来 mbedTLS 3.x 的默认构建已经在大量使用 PSA 接口建议还是走上面的源码替换路线可控性更强。4.3 用 OpenSSL 做联调验证代码改完后我在 PC 上用 OpenSSL 起了一个 TLS 1.3 服务端让板子作为客户端去连接openssl s_server -accept 4433 -cert server.crt -key server.key -tls1_3 -www服务端需要正经的 RSA 或 ECDSA 证书server.crt和server.key可以用 OpenSSL 自签生成。板子端跑 TLS 1.3 客户端程序连接成功后服务端控制台会打印出请求的路径信息说明握手已经完成。如果握手失败通常会在服务端看到类似error:1417A0C1:SSL routines:tls_post_process_client_hello:no shared cipher之类的提示这时候去抓包看哪个环节出的问题。我在联调时发现一个隐蔽问题板子端打印的握手日志显示server_handshake_traffic_secret已经计算出来了但服务端一直没有发EncryptedExtensions之后的握手消息。用 Wireshark 抓包才发现客户端在发送ClientHello时携带的key_share是 P-256但服务端配置的证书是 RSA密钥交换算法对不上。解决办法是在 mbedTLS 配置里设置MBEDTLS_ECP_DP_SECP256R1_ENABLED并确保客户端首选曲线是 P-256。4.4 一致性验证用测试向量兜底联调成功只能说明“能用”不能证明你的 HKDF 实现是对的。要证明对必须用 RFC 里的官方测试向量验证。我推荐用 RFC 8448 附录里的HKDF-Expand-Label测试向量或者 RFC 5869 附录 A 的 HKDF 基础测试向量。RFC 5869 A.1 有一个经典测试用例IKM 0x0b * 22 salt 0x000102030405060708090a0b0c info 0xf0f1f2f3f4f5f6f7f8f9 L 42 PRK 0x077709362c2e32df0ddc3f0dc47bba63 90b6c73bb50f9c3122ec844ad7c2b3e5 OKM 0x3cb25f25faacd57a90434f64d0362f2a 2d2d0a90cf1a5a4c5db02d56ecc4c5bf 34007208d5b887185865我把这个向量写进板子端的自测代码里每次上电先跑一遍hkdf_extract和hkdf_expand比对结果是否一致。这一步看着啰嗦实际能帮你省下大量查错时间。特别是改了 Secure Manager 版本、升级固件包之后回归测试可以快速发现底层行为变化。提示TLS 1.3 的测试向量一定要用 RFC 8448 而不是 RFC 5869因为 TLS 1.3 的HKDF-Expand-Label在 info 里塞了tls13 前缀和长度字段直接套用 RFC 5869 的普通 HKDF 测试向量会误导你。5. 常见问题与排查技巧实录5.1 问题速查表我把这几天遇到和想到的问题整理成一个速查表方便参考现象可能原因排查思路解决办法psa_key_derivation_setup返回PSA_ERROR_NOT_SUPPORTEDSecure Manager 未实现 HKDF 算法查 Secure Manager Release Notes 的支持矩阵按本文第 3 节实现自己的 HKDF握手后服务端报bad_record_macHKDF-Expand-Label 中length字段或标签长度算错用 RFC 8448 测试向量逐步比对重点检查HkdfLabel构造里的uint16(out_len)和前缀长度客户端报no shared cipher密钥交换算法与 TLS 库配置不匹配检查 ClientHello 的 key_share 和 server 的证书类型配置MBEDTLS_ECP_DP_SECP256R1_ENABLED确保证书类型与密钥交换算法一致纯软件 HKDF 性能太差HMAC 全部在 CPU 上跑没走硬件加速用定时器统计单次派生耗时改用 Secure Manager 的哈希服务或者 STM32H573 的硬件哈希外设Secure Manager API 调用返回PSA_ERROR_COMMUNICATION_FAILURESEA IPC 通道异常或内存隔离配置错误检查 TrustZone 边界配置和stm32h5_psa驱动初始化确认非安全侧调用前调用了psa_crypto_init()并正确配置了隔离 memory5.2 我踩过的坑这轮开发里我踩了三个比较有代表性的坑逐个说一下。第一个坑是我最开始试图改 mbedTLS 的ssl_tls13_keys.c把整个密钥调度逻辑替换掉。这个想法听起来很直接实际上执行起来特别麻烦因为 TLS 1.3 的密钥调度函数之间耦合很重我自己实现的函数签名必须和 mbedTLS 内部结构体完全对齐否则编译期不报错运行期指针错乱还特别难排查。后来我调整策略只在最底层的 HKDF 函数处做替换上层流程完全不动改动面小了很多。第二个坑是HkdfLabel里的长度字段。TLS 1.3 中HKDF-Expand-Label的第一个字段是输出长度用的是uint16网络字节序也就是大端。如果你习惯了小端 MCU 的直觉写(out_len 0xFF00) 8或者直接*((uint16_t *)buf) out_len出来的字节序就是反的。这类 bug 非常隐晦因为两边都在算但算法细节对不上。第三个坑是关于 Secure Manager 的psa_hash_update分块调用。Secure Manager 的哈希服务一次性输入的 buffer 是有上限的如果你把一个很大的文本一次性传给psa_hash_update驱动会返回错误或者截断。正确做法是循环分块调用每块控制在几百字节以内。我用在 HMAC 计算时没有踩这个问题因为 HMAC 的两次哈希输入都不大但如果你复用了这个思路去做大文件哈希一定要注意分块。5.3 性能对比实测我在跑通之后顺手对三种方案的 HKDF-Extract 做了个粗略计时使用 DWT-CYCCNT 读取 CPU 周期主频 250 MHz方案单次 HKDF-Extract 耗时估算说明Secure Manager SHA-256 非安全侧拼 HMAC约 60-100 微秒主要时间花在两次 PSA 哈希调用和 IPC 通信上纯软件 SHA-256 HMAC约 150-250 微秒无 IPC 开销但 CPU 密集计算时间长Secure Manager 原生 HKDF如果支持约 30-50 微秒可以省掉 IPC 往返和 HMAC 拼接逻辑这个数据只是我手头环境的大致量级不同 Secure Manager 版本、不同编译器优化等级都会有差异但作为参考足够说明问题前两种方案性能差异没有想象中那么大在大多数 TLS 1.3 场景里都能满足需求。除非你做的是高频握手的小型网关否则不需要为了这几十微秒去冒险改 mbedTLS 源码。6. 这个方案后续还能怎么优化如果你确实对握手性能有硬要求或者嫌弃在非安全侧拼 HMAC 不够优雅还有两条路可以继续走。第一条路是检查 Secure Manager 是否有更新版本支持了 HKDF。Secure Manager 的 API 实现随时间迭代很快ST 有在 Release Notes 里列出新增算法。我写这篇文章时用的版本不支持 HKDF但保不准几个月后就有新版本加上了。升级 Secure Manager 的好处是 HKDF 全程在安全侧完成非安全侧调一次 API 就能拿到派生结果无论是安全性还是性能都好于自己拼。缺点是升级 Secure Manager 本身会刷掉安全侧固件要确认和你当前的密钥管理方案兼容。第二条路是把HKDF-Expand-Label的实现往项目里沉淀成可复用模块。我在完成这个项目后把hkdf_psa.c里的函数整理成了独立组件支持 SHA-256 和 SHA-384 两种哈希算法并针对 STM32H5 的 PSA 驱动做了分块哈希适配。如果你后续要做 TLS 1.3 的 PSK 预共享密钥、证书链验证等场景这个模块能直接拿来用不用再改 mbedTLS 源码。我个人在实际操作中的体会是Secure Manager 这类预置安全固件的出现确实把很多安全功能的门槛降低了但也意味着你要接受它的能力边界。遇到not supported错误第一反应不是骂库有问题而是静下心查它到底支持什么、不支持什么然后在不破坏整体安全设计的前提下找一个折中方案。这次 HKDF 的问题最终方案虽然是在非安全侧实现了部分逻辑但核心的哈希计算仍然在 Secure Manager 内部完成密钥材料没有以明文形式暴露给非安全世界整体的可信边界依然成立。这就够了。最后再分享一个小技巧如果你和我一样是在 mbedTLS 3.x 上改代码建议先翻一下library/ssl_tls13_keys.h里的函数注释里面会标明每个函数的输入输出、所属的阶段和调用时机。根据注释去定位修改点比直接全局搜索hkdf靠谱得多。
返回列表