Mbed TLS PSA 子系统线程安全机制深度解析:密钥管理 API 的并发保证与实现原理
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
导读
本文以 Mbed TLS 3.6 引入的 PSA Crypto 子系统线程安全 MVP(最小可用实现)为切入点,系统讲解在 Flipper Zero 固件所内置的 lib/mbedtls 库中,PSA 密钥管理 API 与psa_crypto_init如何在多线程环境下保证数据一致性。读完本文,你将掌握 PSA 并发调用约定、密钥槽(key slot)四态状态机、"最后一人关门"式密钥销毁策略、系统线性化(linearizability)证明思路,以及当前版本对驱动、随机数发生器的线程安全边界,从而正确地在多线程固件应用中初始化、使用和释放 PSA 子系统。
背景:PSA 子系统的线程安全现状
在 Mbed TLS 的历次发布中,PSA Crypto API 调用本身并不是线程安全的。从 Mbed TLS 3.6 开始,官方实现了一个最小可用版本(MVP),让PSA Crypto 密钥管理 API和psa_crypto_init具备线程安全能力。该特性只影响多线程并发调用场景;如果实现仅在单线程中调用 PSA 函数,则完全不受此新特性影响。
本仓库在 lib/mbedtls 目录下完整携带了这份 Mbed TLS 源码,其线程安全设计文档即位于 lib/mbedtls/docs/architecture/psa-thread-safety/psa-thread-safety.md,配套的密钥存储设计文档见 lib/mbedtls/docs/architecture/psa-keystore-design.md。
近期已完成的工作可归纳为五条主线:
- 密钥存储(Key Store):槽位状态由 密钥槽状态 一节描述的状态机保证并发访问安全;全部密钥槽由一个全局互斥锁保护,见 密钥存储一致性与抽象函数;密钥销毁策略遵守 密钥销毁保证,其实现细节见 密钥销毁实现。
- 全局数据:
psa_crypto.c与psa_crypto_slot_management.c中的global_data变量已由互斥锁保护,见 全局数据。 - 测试系统:测试框架本身已线程安全化,测试可以派生多个线程,见 线程安全测试。
- 并发测试:已加入针对密钥管理 API 的多线程测试,见 测试与分析。
- 实现基座:整套方案复用既有的
MBEDTLS_THREADING_C线程抽象;核心层不对驱动(driver)提供额外保证,见 驱动策略。
除密钥管理外的其他 PSA Crypto API 函数计划在未来逐步线程安全化,但当前版本不做测试、也不做支持。
核心概念与定义
并发调用(Concurrent calls):PSA 规范定义为"在某些环境中,应用可以在不同线程中调用 Crypto API。此时并发调用是指两个或多个 API 调用,其执行在时间上可以重叠"(见 PSA 1.1 规范 overview/conventions 章节)。
线程安全(Thread-safety):一般而言,一个系统是线程安全的,如果任何一组合法并发调用都能被处理为"每个调用的效果与返回码等价于某种顺序执行"。Mbed TLS 实现的是较弱版本的线程安全:仅在 PSA 并发调用约定 所述的条件下提供保证。
线程安全保证(Guarantees)
开箱即用正确性
同时启用MBEDTLS_PSA_CRYPTO_C与MBEDTLS_THREADING_C编译出的代码是正确的:在psa_crypto_init已被调用之后(多线程正确初始化方式见 初始化),并发调用任意一组 PSA 密钥管理函数不会产生竞态条件(race conditions)、死锁(deadlocks)或活锁(livelocks)。
实现中不存在忙等待(busy-waiting),无论底层互斥锁采用何种加锁策略,每个 API 调用都能在有限步骤内完成。仅就密钥管理函数而言,Mbed TLS 3.6 满足 PSA 规范对并发调用的最低期望(见下文约定)。
PSA 并发调用约定
以下约定是计划写入 PSA 1.2 规范的条款,Mbed TLS 3.6 在仅考虑密钥管理函数时遵守它们:
两个或多个并发调用的结果,必须与这些调用按某种顺序顺序执行的结果一致,前提是调用满足以下约束:
- 一个调用的输出参数与另一个调用的输入或输出参数之间不得重叠;输入参数之间的重叠是允许的。
- 对
psa_destroy_key()的调用,不得与以下任一并发调用重叠:
- 任何以相同密钥标识符(key identifier)作为参数的调用;
- 任何多部分操作(multi-part operation)中,之前步骤使用了相同密钥标识符的调用。
- 并发调用不得使用同一个操作对象(operation object)。
若违反上述任一约束,行为未定义(undefined)。
一致性要求不适用于资源耗尽等资源类错误。例如,由资源耗尽导致的错误可以在并发执行中出现,而在顺序执行中不出现。
规则示例:假设两个并发调用试图以同一个尚不存在于密钥存储中的密钥标识符创建新密钥,则:
- 若其中一个调用返回
PSA_ERROR_ALREADY_EXISTS,则另一个调用必须成功。- 若其中一个调用成功,则另一个必须失败:要么返回
PSA_ERROR_ALREADY_EXISTS,要么返回其他错误码。- 两个调用都可以以非
PSA_ERROR_ALREADY_EXISTS的错误码失败。若应用在函数调用进行中并发修改输入参数,行为未定义。
在 lib/mbedtls/library/psa_crypto_slot_management.c 的槽位分配逻辑中,可以看到"两个线程同时用同一标识符创建密钥"这一场景正是通过"先保留槽位、后校验是否存在"的两阶段流程来保证并发创建唯一性的(详见 密钥创建与加载)。
向后兼容
Mbed TLS 3.6 之前可正常工作的代码仍然可用。仅在单线程中调用 PSA 函数、或用互斥锁保护所有 PSA 调用的实现,不受新特性影响;在 3.X 任一版本上可运行的应用,在 3.6 上依然可运行。
支持的线程实现
当前代码库中唯一随库发布的线程库支持是 pthread(通过MBEDTLS_THREADING_PTHREAD启用)。实现中使用的并发原语只有互斥锁(mutex),不依赖条件变量(关于未来引入条件变量的讨论见 条件变量)。
用户可以通过 Mbed TLS 平台抽象层为任何"有互斥锁的平台"自行添加支持,接口声明见 lib/mbedtls/include/mbedtls/threading.h。该头文件同时声明了本次线程安全改造引入的全局互斥锁:
| 互斥锁 | 声明位置 | 保护对象 |
|---|---|---|
mbedtls_threading_key_slot_mutex | threading.h | 整个密钥存储(key store) |
mbedtls_threading_psa_globaldata_mutex | threading.h | psa_crypto.c与psa_crypto_slot_management.c中的全局数据 |
mbedtls_threading_psa_rngdata_mutex | threading.h | 内置 RNG 的种子与随机状态(见 随机数发生器) |
官方计划在未来版本中发布包括 Windows 在内的其他平台支持。
密钥销毁保证
与其他 API 调用一样,psa_destroy_key不会无限期阻塞,并且当psa_destroy_key返回时:
- 密钥标识符不存在。这是持久密钥的功能性要求:任何线程都可以立即用同一标识符创建新密钥。
- 该密钥占用的资源已被释放。这使得线程可以在销毁后立即创建类似密钥,不受资源限制。
当对正在使用中的密钥调用psa_destroy_key时,保证 2 可能被违反。这与 PSA 规范一致——销毁使用中的密钥本身属于未定义行为。未来版本计划强制执行更强的销毁要求,见 长期密钥销毁需求。
驱动策略(Driver policy)
核心层(core)对驱动不做额外保证:驱动入口点(driver entry points)可能被多个线程并发调用;线程可以并发地使用同一个密钥调用驱动入口点;也没有对"销毁正在使用中的密钥"提供保护。驱动的线程安全责任完全在驱动自身。
随机数发生器
PSA RNG 既可以从各种 PSA 函数中访问,也可以从应用代码通过mbedtls_psa_get_random访问。
- 使用内置 RNG 实现(即
MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG未启用)时:查询 RNG 是线程安全的;mbedtls_psa_random_init与mbedtls_psa_random_seed仅在持有mbedtls_threading_psa_rngdata_mutex时调用才是线程安全的;mbedtls_psa_random_free不是线程安全的。 - 启用
MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG时:若启用了多线程,线程安全完全由外部实现负责。
多线程使用指南(Usage guide)
初始化
PSA 子系统通过调用psa_crypto_init完成初始化。这是一个线程安全函数,多次调用被明确允许。多个线程各自调用psa_crypto_init、随后(若初始化成功)调用任何 PSA 密钥管理函数,都是合法的使用方式。
从源码看,psa_crypto_init的实现位于 lib/mbedtls/library/psa_crypto.c,其内部对全局数据的访问均通过mbedtls_threading_psa_globaldata_mutex进行加锁/解锁(参见该文件中 psa_crypto.c 的初始化辅助函数),这正是"多次初始化合法"的实现基础。
常规使用
初始化之后,只要调用之间没有重叠(指并发调用约定中的参数与对象约束),线程可以使用任何 PSA 函数。所有线程共享同一组密钥:一旦某个线程通过密钥管理 API 调用返回(密钥创建/加载完成),该密钥立即可被任何线程使用。
需要注意的并发行为:
- 若多个线程尝试加载同一个持久密钥标识符的密钥,只有一个线程能成功,其余线程将返回
PSA_ERROR_ALREADY_EXISTS。 - 应用需要谨慎处理资源管理类错误。如并发调用约定所述,进行中的操作可能产生与内存相关的副作用:由于线程在将持久密钥加载进密钥存储时会临时占用一个空闲密钥槽,资源不足可能导致顺序执行中不会出现的错误。例如多个线程同时加载同一持久密钥、而该密钥当前不在密钥存储中时,部分线程可能返回
PSA_ERROR_INSUFFICIENT_MEMORY。 - 若互斥锁操作失败(仅在互斥锁实现本身出错时发生),将返回错误码
PSA_ERROR_SERVICE_FAILURE。一旦收到该错误码,必须停止 PSA 子系统的执行。所有内部有加锁/解锁(除加解锁发生在无返回值的函数中)的函数在此情形下都会以此错误码返回。
释放
不存在线程安全地释放全部 PSA 资源的方法——任何此类操作都需要等待所有其他线程完成任务后才能抹除资源。mbedtls_psa_crypto_free必须由单个线程在所有线程完成各自操作之后调用。
实现策略(Current strategy)
本节深入 Mbed TLS 3.6 的内部实现:保护了哪些资源、密钥槽状态机如何运作、系统如何保持一致、抽象模型是什么。
受保护资源
全局数据(Global data)
新增的互斥锁mbedtls_threading_psa_globaldata_mutex用于使psa_crypto_init线程安全。代码中存在两个psa_global_data_t结构体,各有一个名为global_data的实例:
- lib/mbedtls/library/psa_crypto.c 中的结构体由
mbedtls_threading_psa_globaldata_mutex保护。其中RNG 字段不受此锁保护,且不总是线程安全(见 随机数发生器)。 - lib/mbedtls/library/psa_crypto_slot_management.c 中的结构体有两个字段:
key_slots(由密钥槽互斥锁保护,见 密钥槽)和key_slots_initialized(由全局数据互斥锁保护)。
互斥锁使用约定
- 如果线程在已持有某互斥锁时再次尝试锁定该锁,会发生死锁,因此需要在持有全局互斥锁时调用的函数均有文档说明。
- 为避免性能退化,函数持有互斥锁的时间应尽可能短;尤其禁止在持锁期间启动昂贵操作(如执行密码学运算)。
密钥槽(Key slots)
密钥在内部存储于一个全局密钥槽数组中,称为"密钥存储"(key store),实现在 lib/mbedtls/library/psa_crypto_slot_management.c 中。每个槽位的数据结构psa_key_slot_t定义于 lib/mbedtls/library/psa_crypto_core.h,包含state状态字段与registered_readers读者计数等成员。
密钥槽状态机(Key slot states)
每个密钥槽有一个状态变量state和一个读者计数registered_readers,两者共同决定某操作能否访问该槽位、以及可以以何种方式使用该槽位。状态枚举定义于 lib/mbedtls/library/psa_crypto_core.h:
| 状态 | 语义 |
|---|---|
PSA_SLOT_EMPTY | 当前没有线程访问该槽,槽内无任何信息。任何线程都可以将状态改为PSA_SLOT_FILLING并开始向槽内装载数据。 |
PSA_SLOT_FILLING | 一个线程正在装载或创建槽内容,该线程负责下一次状态迁移。其他线程不能读取此状态槽的内容。 |
PSA_SLOT_FULL | 槽内已含一个密钥。任何线程在注册为读者(registered_readers加 1)后即可使用该密钥。 |
PSA_SLOT_PENDING_DELETION | 槽内密钥已被销毁或标记为待销毁,但至少还有一个线程仍注册为读者(registered_readers > 0)。任何线程都不能再注册读取该槽;槽内容必须等到最后一个读者注销时才被抹除,正是在最后一次注销时槽内容被擦除、状态置回PSA_SLOT_EMPTY。 |
┌────────────────────────────────────────────┐ │ 密钥槽状态迁移示意图 │ └────────────────────────────────────────────┘ PSA_SLOT_EMPTY ──(psa_reserve_free_key_slot)──▶ PSA_SLOT_FILLING ▲ │ │ │ (psa_finish_key_creation) │ ▼ │ PSA_SLOT_FULL │ │ └──(psa_wipe_key_slot, 由最后一个读者触发)◀── PSA_SLOT_PENDING_DELETION (psa_destroy_key 置位)状态迁移图中,从状态q1到状态q2的带标签f的箭头表示:若在f的线性化点(linearization point)之前槽状态为q1,则在其之后可能为q2。内部函数使用斜体标签。PSA_SLOT_PENDING_DELETION -> PSA_SLOT_EMPTY迁移可由任何调用psa_unregister_read的函数完成。
官方提供的状态迁移原图可直接在 diagrams.net 中打开编辑(见文档内嵌的编辑链接),对应图片为 key-slot-state-transitions.png。
密钥槽访问原语
- 状态迁移:槽状态通过内部函数
psa_key_slot_state_transition更新(定义于 lib/mbedtls/library/psa_crypto_slot_management.h)。要将slot从expected_state变更为非PSA_SLOT_EMPTY的new_state,调用psa_key_slot_state_transition(slot, expected_state, new_state);若实际状态不是expected_state,返回PSA_ERROR_CORRUPTION_DETECTED。该期望状态参数存在的唯一意义是帮助保证函数按预期工作——没有内部编码错误时,该错误码不会出现。 - 彻底抹除:将槽状态改为
PSA_SLOT_EMPTY通过psa_wipe_key_slot完成,此函数会抹除密钥槽的全部内容。 - 读者计数:
psa_register_read使计数加 1,psa_unregister_read使计数减 1。库函数通过psa_get_and_lock_key_slot_X系列函数注册读取槽位,读取内容后调用psa_unregister_read声明完成读取。
密钥存储一致性与抽象函数
整个密钥存储由一个全局互斥锁mbedtls_threading_key_slot_mutex保护。一致性的维护原则是:对slot->state和slot->registered_readers的所有读写都必须在持有该锁的情况下进行。上述所有访问原语都必须在持锁时调用;便捷函数psa_unregister_read_under_mutex将psa_unregister_read包裹在一对锁/解锁操作中。
线程只能在持有mbedtls_threading_key_slot_mutex时遍历密钥存储;持锁线程所能访问的密钥集合等价于:
{mbedtls_svc_key_id_t k : (∃ slot := &global_data.key_slots[i]) [ (slot->state == PSA_SLOT_FULL) && (slot->attr.id == k)]}该集合与"当前未加载进槽位的持久密钥集合"之并集,就是密钥存储的抽象函数(abstraction function):任何不在此并集中的密钥,对代码而言当前不存在(即使它位于PSA_SLOT_FILLING或PSA_SLOT_PENDING_DELETION状态的槽中)。尝试使用并集之外的任何密钥都会得到PSA_ERROR_INVALID_HANDLE。
锁的加解锁约定
若一次加锁或解锁操作失败,且这是函数内的首次失败,函数返回PSA_ERROR_SERVICE_FAILURE;若在已识别出其他失败之后加解锁失败,则不覆盖原有状态码。
在 lib/mbedtls/library/psa_crypto_core.h 中定义了一组宏来封装"(加)解锁互斥锁,失败即返回或跳转到退出标签"的常见模式:
PSA_THREADING_CHK_RET(f):互斥锁操作失败时,若status仍为PSA_SUCCESS则返回PSA_ERROR_SERVICE_FAILURE,否则返回原status;PSA_THREADING_CHK_GOTO_EXIT(f):失败时设置status(仅在成功状态下覆盖为PSA_ERROR_SERVICE_FAILURE)并跳转到exit标签。
这两个宏只在MBEDTLS_THREADING_C启用时编译生效。
密钥创建与加载
将新密钥装载进槽位使用以下内部工具函数:
psa_reserve_free_key_slot:必须在持有mbedtls_threading_key_slot_mutex时调用。遍历密钥存储寻找状态为PSA_SLOT_EMPTY的槽位;找到则将状态置为PSA_SLOT_FILLING以保留该槽;若未找到,则检查是否存在"已加载但无读者(registered_readers == 0)"的持久密钥,如有则将一个这样的密钥踢出密钥存储以腾出槽位。psa_start_key_creation:包装psa_reserve_free_key_slot。若找到槽位,则设置槽位 ID。此第二步不在互斥锁下进行——此时调用线程对该槽拥有独占访问权。psa_finish_key_creation:密钥内容装载完成后(装载同样不在持锁状态下进行),线程调用此函数。它获取互斥锁、检查密钥在密钥存储中不存在(此检查只能在此阶段进行)、将槽状态置为PSA_SLOT_FULL并释放互斥锁。成功后任何线程立即可使用该新密钥。psa_fail_key_creation:创建阶段任何一步失败时,此清理函数获取互斥锁、抹除槽位并释放互斥锁。解锁后任何线程即可将该槽用于下一次密钥加载。
上述状态迁移在源码中的落点可参见 lib/mbedtls/library/psa_crypto_slot_management.c(EMPTY -> FILLING)与同文件 psa_crypto_slot_management.c(FILLING -> FULL)。
持久密钥的重新加载
如前述,持久密钥在未被使用(registered_readers == 0)时可以被踢出密钥槽数组。当尝试使用一个已被踢出槽位的持久密钥时,psa_get_and_lock_key_slot会发现密钥不在槽中,于是调用psa_reserve_free_key_slot并重新加载密钥到保留槽中。这一整个序列在单次互斥锁持锁期间完成,这对线程安全是必需的(参见psa_get_and_lock_key_slot的文档)。
若psa_reserve_free_key_slot找不到合适的槽位(例如所有已加载密钥都正被读取),密钥无法重新加载,将导致PSA_ERROR_INSUFFICIENT_MEMORY错误。
使用已有密钥
单次(one-shot)操作使用已有密钥时遵循标准模式:
- 调用某个
psa_get_and_lock_key_slot_X函数,找到密钥并将当前线程注册为读者; - 对密钥槽进行操作,通常将密钥复制到独立缓冲区供操作使用——此步骤不在密钥槽互斥锁下进行;
- 完成后调用
psa_unregister_read_under_mutex。
多部分(multi-part)与可重启(restartable)操作各自有一个"setup"函数接收密钥,这些函数遵循上述模式:密钥被复制进operation对象,线程随即注销读取(后续操作不再访问密钥槽)。该密钥副本不会在psa_destroy_key调用期间被销毁,运行操作的线程负责在清理时删除自己的副本。为执行长期密钥销毁需求,此设计未来可能需要调整(见 长期密钥销毁需求)。
需要强调的是:单次操作与多部分操作目前尚不被视为线程安全,因为尚未测试它们是否依赖不受保护的全局资源;但这些操作中的密钥槽访问本身是线程安全的。
密钥销毁实现
销毁策略的详细说明见 lib/mbedtls/library/psa_crypto.c。调用psa_destroy_key的销毁线程并不总是亲自抹除密钥槽。销毁线程的流程是:注册读取该密钥 → 将槽状态置为PSA_SLOT_PENDING_DELETION→ 若密钥是持久密钥则从内存中抹除其持久化副本 → 注销对槽的读取。
关键在于:psa_unregister_read当且仅当槽状态为PSA_SLOT_PENDING_DELETION且槽的注册读者计数等于 1 时,内部调用psa_wipe_key_slot。这实现了"最后一人关门"(last one out closes the door)策略:最后一个注销读取被销毁密钥的线程会自动抹除槽内容;此时已无读者引用该槽,因此不可能出现数据损坏。
系统的线性化(Linearizability)
为满足 开箱即用正确性 的要求,函数在(特定约束下)必须是"线性化"的——即任何满足约束的并发调用集都表现得如同按某种顺序执行。
标准论证方法是给每个调用找出一个"线性化点"(linearization point):一个函数生效的单一执行步骤(通常是该调用的效果对其他线程可见的步骤)。若每个调用都有线性化点,则该调用集等价于按线性化点发生顺序顺序执行。
线性化要求仅在未返回资源管理错误时成立。在并发调用集中,允许某个调用c返回PSA_ERROR_INSUFFICIENT_MEMORY,即使不存在让c返回该错误的顺序执行方式;即便如此,所有调用仍须功能正确。
各 PSA 调用的线性化点(含计划中的)如下:
| 调用 | 线性化点 |
|---|---|
密钥创建函数(含psa_copy_key) | 成功时为psa_finish_key_creation中的互斥锁解锁(此时密钥对其他线程可见);失败时为首次识别失败后最近的一次互斥锁解锁 |
psa_destroy_key | 互斥锁解锁(槽进入PSA_SLOT_PENDING_DELETION意味着密钥已销毁);失败时同理 |
psa_purge_key、psa_close_key | 成功为抹除槽后的互斥锁解锁,失败为注销读取后的解锁 |
| 单次操作 | psa_get_and_lock_key_slot中互斥锁的最后一次解锁(即决定密钥是否存在的时刻) |
| 多部分操作 | 密钥输入函数为psa_get_and_lock_key_slot中互斥锁的最后一次解锁;其余步骤除密钥派生(归入密钥创建函数)外无非资源相关的副作用 |
测试与分析
线程安全测试
现在单个测试可以派生多个线程:测试中使用的全局变量已线程安全化;若多个线程同时失败于某断言,首个失败将以正确的行号被报告。
需要留意的是,部分测试使用的step功能虽然线程安全,但在多线程测试中可能产生意外结果:线程内对mbedtls_test_set_step或mbedtls_test_increment_step的调用顺序不定,因此当需要精确排序时可能得不到预期结果。
当前测试状态
测试工作仍在推进中。传统单线程测试无法以并发方式运行,需要为并发测试编写新的测试套件。目前测试仅在 pthread 上运行(API 本身已支持扩展)。
测试手段与范围:
- 使用ThreadSanitizer检测数据竞争(data races);
- 测试密钥存储、验证密钥槽状态机得到强制执行;
- 测试
psa_crypto_init的线程安全; - 并非每个 API 调用都被测试,也不可能穷举所有并发组合。API 调用大体可按类别划分,同类调用以相同顺序调用相同的内部密钥管理函数——正是这些内部函数负责加锁与访问密钥存储,因此对内部函数进行线程安全测试即是当前策略。
由于并非所有密码学操作都在并发下运行,当前不测试操作是否存在意外全局变量依赖。仓库中的相关测试套件如 lib/mbedtls/tests/suites/test_suite_psa_crypto_init.function、lib/mbedtls/tests/suites/test_suite_psa_crypto.function 可在lib/mbedtls/tests/suites/目录下进一步查阅。
测试扩展计划
未来的测试工作方向包括:
- 对每个 API 调用,编写同时运行该调用多个副本的测试;
- 在其他线程平台实现后,将测试扩展到这些平台;
- 增加对"将持久密钥踢出槽位"场景的测试;
- 在 ThreadSanitizer 下覆盖每个操作的并发场景,显式验证所有全局变量均受保护;
- 支持更多线程实现后,在其上运行测试。
性能特性
- 密钥加载一定程度上是并行执行的:密钥派生与复制进槽位的过程不在任何互斥锁下进行。
- 密钥销毁是完全串行的。这对持久密钥是必需的,否则无法避免重新加载密钥时出现的问题(在不改变线程安全方案的前提下无法规避)。
未来工作
长期目标
最终目标是将整个 PSA API线程安全化,这将在已完成工作的基础上推进,并需要一整套并发测试(见 测试扩展计划)。
长期性能需求
对密码学操作的规划是:不在任何全局互斥锁下执行。单次操作与多部分操作都只在"查找密钥槽中的相关密钥"和"操作完成后注销读者"两个时刻持有全局互斥锁,各自使用操作专属的互斥锁保护其共享数据。同时计划在可能的情况下用**读写锁(RWLocks)**替换部分/全部互斥锁。
长期密钥销毁需求
PSA Crypto 密钥销毁规范要求实现尽最大努力保证密钥材料不可恢复。长期来看,应保证psa_destroy_key抹除所有密钥材料副本。长期销毁目标清单:
psa_destroy_key不无限期阻塞,且返回时密钥标识符不存在(持久密钥的功能性要求:任何线程可立即用同一标识符创建新密钥);- 返回时密钥资源已释放(允许线程立即创建类似密钥);
- 不存在任何密钥材料副本(安全要求——当前尚未满足,需作为安全弱点记录,并计划未来满足)。
条件变量
条件变量(condition variables)理想情况下应加入未来大版本;出于向后兼容原因,不能将其作为默认MBEDTLS_THREADING_C的硬性要求。
条件变量将使得满足长期密钥销毁需求的最后一条成为可能,销毁流程将变为:
- 线程调用
psa_destroy_key,照常执行直到psa_unregister_read调用; - 不再调用
psa_unregister_read,而是等待条件slot->registered_readers == 1成立(销毁线程成为最后读者); - 此时销毁线程直接调用
psa_wipe_key_slot。
为符合销毁需求还需两处配套修改:
- 多部分操作需要保持注册为密钥槽的读者,直到其密钥副本被销毁(即 finish/abort 调用结束时);
- 需要移除"
psa_unregister_read可抹除密钥槽"的功能,槽位抹除只由销毁/抹除线程完成。
保护操作上下文
目前,库依赖 crypto service 保证同一操作不会被并发调用,这符合 PSA 规范(见 PSA 并发调用约定)。
对同一操作对象的并发访问可能破坏 crypto service:例如操作上下文含指针时(指针赋值是否原子取决于编译器和平台),会违反 crypto service 的功能正确性要求。若未来要在库内部防御此问题,操作需要增加一个由全局互斥锁保护的状态字段:API 调用入口检查状态,若为 ACTIVE 则返回错误;若为 INACTIVE 则置为 ACTIVE、执行操作段、返回前恢复为 INACTIVE。
驱动未来工作
未来可能对驱动强制执行的政策:
- 默认情况下,每个驱动在任意时刻最多只有一个入口点处于活动状态,即每个驱动有专属排他锁;
- 驱动可选
"thread_safe"布尔属性:为 true 时允许对该驱动的并发调用; - 即使驱动是线程安全的,核心也绝不会在操作进行中开始销毁密钥,也绝不并发调用同一多部分操作。
非线程安全驱动的自然假设/要求:
- 驱动不为它们提供入口点的操作调用核心;
- 核心不在对入口点的两次调用之间持有驱动互斥锁。
在这两条约束下,死锁的唯一途径是多个驱动形成循环依赖:驱动 A 发出调用被分派到驱动 B;B 执行中又调用被分派回 A。例如驱动 A 实现 CCM,调用驱动 B 做 CBC-MAC,B 又调用驱动 A 执行 AES。
可行的解决途径(按选择结果排序):
- 非线程安全驱动不得调用核心——过于严格;
- 提供驱动可安全调用的新公共 API——需稳定 API;
- 将分派层公开给驱动调用——需稳定 API;
- 对驱动可调用的核心 API 设置白名单——提供这些入口点的驱动在处理这些调用时不得回调核心(驱动仍可调用任何不可能有驱动入口点的核心 API)。
方案二、三需要将接口稳定化,且可能为一项相对少用的特性增加代码体积;方案四是最可行的选择,因此被采纳。
线程安全驱动:需要注意,若thread-safe属性为 true,驱动即为线程安全驱动。为使非线程安全驱动的重入(re-entrancy)正常工作,线程安全驱动在处理白名单上的核心 API 调用时也不得回调核心。线程安全驱动从核心获得的保证更少、需要实现更复杂的逻辑,但可以合理预期它们在重入方面更灵活。现阶段难以判断还有哪些保证既有用又可行,因此不再提供进一步保证;线程安全驱动不得对核心操作做本文之外的其他假设。
总结
Mbed TLS 3.6 的 PSA 线程安全 MVP 通过"全局互斥锁 + 密钥槽四态状态机 + 读者计数"的组合,为密钥管理 API 与psa_crypto_init提供了符合 PSA 并发调用约定的线性化保证,并以"最后一人关门"策略实现安全的密钥销毁。本文所涉及的互斥锁声明(lib/mbedtls/include/mbedtls/threading.h)、密钥槽状态机与访问原语(lib/mbedtls/library/psa_crypto_core.h、lib/mbedtls/library/psa_crypto_slot_management.c)以及全局数据保护(lib/mbedtls/library/psa_crypto.c)均可直接在仓库内交叉验证。对于在 Flipper Zero 这类嵌入式固件中集成 Mbed TLS 的开发者而言,正确理解这些边界——尤其是"仅密钥管理线程安全、驱动无保证、释放必须单线程"——是安全使用多线程 PSA 的前提。
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考