PSA Certified MCU上的Secure Flash Storage安全闪存存储实战解析

📅 发布时间:2026/8/28 5:00:41
PSA Certified MCU上的Secure Flash Storage安全闪存存储实战解析
做嵌入式这几年我见过太多“看起来加了密实际一捅就破”的产品。最常见的一种把密钥、校准数据、设备证书直接放在Flash里打开读保护就当安全了结果攻击者用几条命令就能让固件自己把数据吐出来或者干脆降级固件版本绕过限制。真正在MCU上做Secure Flash Storage核心不是“加密”而是回答清楚一个问题当攻击者物理拿到你的设备、能改你的固件、甚至能拆芯片做侧信道分析时你的密钥和数据还拿不拿得出来这篇内容围绕“PSA Certified MCU提供Secure Flash Storage方案”展开讲的是如何利用通过PSA Certified认证的MCU自带的硬件隔离能力在资源受限的嵌入式设备上搭建一套真正可信的安全闪存存储。适合正在做物联网设备安全设计、产品接入方案评估、或者刚接手MCU安全功能开发的工程师。我会把架构原理、数据格式、密码学选型、实操步骤和踩坑记录一次讲透不写PPT式的废话。1. 为什么MCU上的安全存储不是给Flash加个锁这么简单1.1 三个真实存在的攻击角度先泼一盆冷水。在MCU上保存敏感数据如果只依赖“读保护”和“加密存储”至少有三个方向的攻击能轻松撕开口子。第一个是物理攻击。芯片开封后用聚焦离子束FIB修线、用微探针采信号甚至用化学腐蚀逐层剥离都能直接读取Flash内容。这类攻击成本高但针对高价值设备比如支付终端、门禁卡母卡完全可行。对抗它的不是加密算法而是密钥永不落盘、密钥与芯片物理绑定的设计。第二个是固件重打包攻击。攻击者不用读Flash而是拿到你的固件镜像反汇编找到读取密钥的逻辑修改其中的判断条件或数据地址重新签名如果签名校验有漏洞或者直接利用非安全侧漏洞注入代码让设备自己把密钥输出给攻击者。这个过程中加密算法一次都没被破但数据照样丢了。第三个是调试接口后门。芯片出厂时默认开着的调试口、工厂测试模式下残留的扫描链、Bootloader降级后的DFU接口都是常见突破口。攻击者只要能把CPU带入调试状态就能读任意Flash区域。很多芯片的读保护在“降级固件 → 进入Bootloader → 关闭保护”的组合拳下根本撑不住。所以安全存储的真实需求是即使攻击者拿到芯片、可以选择任意物理或逻辑手段存储在设备上的机密信息仍然无法被以明文或可用形式提取。这已经不是“加个AES库”能解决的问题而是需要一个从硬件隔离到系统软件的完整信任链。1.2 PSA Certified 到底认证了什么PSAPlatform Security Architecture是Arm提出的物联网安全架构包含10个安全目标Security Goals覆盖安全启动、安全更新、安全存储、隔离、密码学、初始状态安全等。其中和Secure Flash Storage直接相关的是Secure Storage、Isolation、Secure Boot和Secure Update这四项。PSA Certified是Arm联合多家独立测试实验室做的认证体系分为三个等级等级评估内容适合场景Level 1安全文档、威胁模型与方案自评估早期评估、产品规划Level 2实验室评估验证隔离机制、TrustZone配置等安全功能的实际有效性绝大多数物联网产品Level 3在Level 2基础上增加侧信道、物理攻击防护测试支付、身份认证、高价值资产选MCU的时候很多团队容易陷入“Level越高越安全”的误区。实际上Level 3会显著增加成本和功耗对智能锁、传感器节点这类设备反而不合适。我的经验是先明确产品的威胁模型再对应选择认证等级。比如一个只存校准参数和临时会话密钥的设备Level 2 良好的密钥管理已经足够但如果要做支付终端或数字版权管理就该上Level 3设备。这个决策直接影响后续存储方案设计的复杂程度。2. 硬件隔离让钥匙和锁具分家2.1 TrustZone-M 与安全/非安全世界划分PSA Certified MCU普遍采用Arm TrustZone技术Cortex-M23/M33/M55/M85内核它把处理器和内存分为两个世界安全世界Secure World和非安全世界Non-Secure World。非安全世界跑RTOS、协议栈、应用逻辑安全世界运行安全服务如密钥管理、安全存储、安全启动。这个划分的关键在于隔离是在CPU总线层级强制执行的。非安全代码访问安全内存区域直接触发Bus Error或HardFault不存在“软件绕过”的可能。也就是说哪怕攻击者完全控制了非安全侧的应用代码也只是一只“在笼子里乱撞的鸟”根本碰不到安全世界里的数据。我在实际项目里把这种设计理解成“钥匙和锁具分家”钥匙密钥、证书、关键数据存放在安全世界的保险柜里锁具加密/解密逻辑也运行在安全世界非安全世界只有一个“寄柜服务窗口”通过固定接口申请存取。应用层甚至不知道密钥长什么样只知道“存了个数据”“取回来一段密文”。2.2 安全审计单元SAU与安全MPU的配合TrustZone在Cortex-M上的落地依赖两个硬件单元SAUSecurity Attribution Unit和IDAUImplementation Defined Attribution Unit。SAU由软件配置为每个内存区域标注安全属性Secure、Non-Secure或Non-Secure Callable允许非安全侧调用安全函数的特殊入口。IDAU则由芯片厂商固化为硬件属性无法被软件修改。两者的规则组合后形成一个“只有更严格者生效”的最终属性表。这意味着即使SAU配置出错IDAU的硬件下限也能兜底保证了隔离机制不会因软件bug被整体击穿。实际操作中芯片上电后安全启动代码做的第一件事就是配置SAU把安全Flash和安全SRAM划出来。比如一块512KB Flash的MCU我会把前面64KB作为安全区域存放安全固件和存储对象区剩余448KB作为非安全区域跑应用。这个比例不是拍脑袋定的需要根据安全服务的代码量、存储对象大小的上限以及非安全侧应用对资源的需求综合评估。这里还要提一个和MCU启动流程强相关的点SAU必须在任何非安全代码执行前完成配置。所以安全启动Secure Boot流程的第一个阶段必须在安全世界执行先加载安全固件、配置SAU/MPU再跳转到非安全侧启动RTOS。如果顺序反了非安全代码可以先执行并篡改SAU配置整个隔离就形同虚设。2.3 安全世界和非安全世界的通信NSPE与SPE有了隔离还要让两个世界能协作。PSA定义了两种处理环境SPESecure Processing Environment和NSPENon-Secure Processing Environment两者通过NSPMNon-Secure Processing Environment Management接口通信。在Cortex-M上NSPE调用SPE服务的方式是触发一个特定指令如SG指令进入安全世界再由安全侧的SVC Handler分发请求。用户态应用不需要关心底层细节只需要调用TF-MTrusted Firmware-M提供的PSA API。比如// 非安全侧APP调用安全存储接口 psa_status_t status psa_its_set(uid, data_length, p_data, create_flags); // uid: 数据对象标识符 // data_length: 数据长度 // p_data: 数据指针 // create_flags: 创建标记如PSA_STORAGE_FLAG_WRITE_ONCE// 读取数据对象 psa_status_t status psa_its_get(uid, offset, size, p_data, data_length);这类API调用看起来像一个普通库函数但内部经历了“非安全调用入口 → 参数校验 → 安全世界执行 → 结果返回”的完整路径并且参数在穿越安全边界时会被重新校验。这里要特别提一个坑别在非安全侧就解引用参数指针。正确做法是传数据缓冲区地址和长度由安全侧去拷贝和校验防止TOCTOUTime-of-Check to Time-of-Use攻击。TF-M还区分了两套接口psa_its_*Internal Trusted Storage内部可信存储和psa_ps_*Protected Storage受保护存储。ITS侧重于完整性验证适合存放本身不需要机密但必须防篡改的数据PS则在ITS基础上增加了机密性保护加密。设计存储方案时要区分对待设备公钥、校准参数可以放进ITS私钥、会话密钥、证书私钥则必须放进PS。3. 安全闪存存储的数据格式与生命周期设计3.1 数据对象格式不是简单加密后写进去把密文直接写到Flash是不够的因为攻击者可以执行“密文剪切粘贴攻击”或“旧数据重放攻击”。一个完整的安全存储对象至少要包含几个部分字段作用说明Magic标识对象起始固定值用于启动时扫描对象列表版本号标识对象结构版本结构升级时用于数据迁移数据大小明文原始长度解密后校验完整性Flags属性标记如只写一次、可覆盖等Rollback Counter抗回滚计数器防止旧版本数据重放Nonce随机数/计数器用于认证加密算法不能重复Ciphertext密文数据AES-GCM/CCM加密输出MAC/Tag完整性标签由认证加密算法产生这个结构不是拍脑袋定的。其中Magic的作用是扫描安全存储在Flash里通常不是单块数据而是一个对象池。启动时遍历Flash块用Magic判断是否是有效对象。版本号和Rollback Counter是配套的版本号变了数据结构可能不兼容回滚计数器递增可以让攻击者把固件和数据恢复到一个调用过的旧版本时系统直接拒绝。一个容易忽略的细节是数据对象IDUID的分配规则。TF-M用UID来索引对象UID是调用者和存储系统之间约定的“文件名”。如果UID可以被攻击者预测或篡改可能导致对象替换攻击用攻击者自己的对象覆盖合法对象。所以UID应该在产品设计阶段就统一规划并且非安全侧的UID使用范围要避开安全侧保留段。3.2 掉电安全与磨损均衡Flash的物理特性决定了“写入”和“擦除”是不同粒度的操作而且写入过程中掉电会产生半写状态。安全存储在嵌入式设备上常会遇到供电不稳或电池被拔的情况因此掉电一致性必须提前设计。一个常用的方案是双槽交替写入也叫Ping-Pong或Double Buffer对象更新时不直接覆盖旧数据而是写入另一个空槽写入完成后更新状态标记比如写一个“New”标记到头部下次上电启动时根据状态标记决定选用哪个副本旧的槽在下一次更新时被复用。这个方案的代价是Flash空间利用率低但换来了操作的原子性。另一种方案是在每个块头部记录序列号启动时找序列号最高的块作为最新版本。TF-M的SSTSecure Storage模块实际就用了类似机制叠加了磨损均衡逻辑避免每次都擦写同一个块导致Flash寿命瓶颈。还要补充一个经验Flash磨损均衡不能只在存储层做。安全存储频繁写入的Rollback Counter、日志型数据会集中消耗特定扇区擦写寿命。我见过一块内嵌Flash标称10万次擦写寿命因为某个对象被频繁更新整块Flash在几个月内就报废了。解决办法有二一是把频繁更新的小数据放到RAM镜像定期批量写回Flash二是用NOR Flash的多个扇区轮流做日志记录区域让磨损均匀分布。3.3 生命周期从出厂预置到设备退役安全存储不是部署完就一劳永逸的它的生命周期贯穿设备的整个服役过程。出厂阶段芯片出厂时安全存储区域是空白的。此时要做三件事一是烧录硬件唯一密钥HUK如果有OTP/eFuse则提前烧好二是用固件签名密钥对Bootloader和初始安全固件签名三是将设备的身份证书、生产批次信息等一次性数据写入安全存储区域并标记为PSA_STORAGE_FLAG_WRITE_ONCE不可再改写。这一步如果漏了后续想再写入只读对象就要重新回产线成本很高。运行阶段设备在用户手里运行时安全存储对象的生命周期由业务逻辑决定。比如安全固件升级时需要保留用户的配网凭证恢复出厂设置时要清除用户敏感数据但保留信任根证书。这要求API设计上区分“可删除”“不可删除”“只读”等不同标记。退役阶段设备报废或转售时安全存储内的数据是否可被可靠清除PSA对这种场景强调“安全擦除”不仅要把数据区覆盖写还要考虑密钥在OTP里无法删除的问题。实际产品中退役设备通常的做法是把Flash中的密钥索引作废配合后端服务器注销设备证书来实现“逻辑删除”。4. 密码学实现细节哪些参数要仔细设计4.1 密钥从哪来HUK与设备唯一密钥安全存储的信任根是HUKHardware Unique Key存储在芯片内部的OTP、eFuse或专用安全单元中软件无法直接读取明文。每次设备上电安全固件通过平台接口获取HUK然后派生各种用途的子密钥。注意这里的关键设计不要直接用HUK加密业务数据。原因是HUK只有一份如果业务密钥泄漏或需要轮换HUK无法更换。正确做法是用HKDF基于HMAC的密钥派生函数从HUK派生出多个用途密钥// 伪代码从HUK派生存储加密密钥 hkdf_sha256(huk, huk_len, salt, secure_storage_key, 18, derived_key, key_len); // salt可以是设备序列号或随机数保证不同设备即使HUK相同一般不会也得到不同派生密钥派生标签info要区分用途比如存储加密密钥、固件签名验证公钥的散列、会话密钥的种子各用各的标签。这样即使某个业务密钥泄漏攻击者也很难推导出其他密钥。这个设计模式在PSA的文档里叫“Per-Device Key Hierarchy”。4.2 认证加密算法与参数选择Secure Flash Storage的机密性和完整性需求对应的是认证加密AEAD算法最常用的是AES-GCM和AES-CCM。这两种算法在加密的同时产生MAC能同时检测密文和附加认证数据AAD是否被篡改。为什么不用AES-CBC/AES-ECB因为它们是纯机密性算法。攻击者虽然看不懂密文但可以翻转密文块导致解密后的明文发生可预测的变化。在安全存储场景里数据的冗余信息比如数据头格式很容易被攻击者利用来构造有效或者说“不是随机乱码”的篡改数据所以完整性校验和机密性同等重要。参数选择上有几个取舍点要重点说明Nonce长度GCM对Nonce非常敏感如果实际加密时重复使用相同Nonce攻击者能直接恢复出密钥流。推荐使用96位12字节Nonce其中一部分用随机数一部分用计数器确保统计上不会重复。Tag长度GCM默认支持128位Tag但有些协议栈允许缩短为64位甚至32位。物联网场景里我建议至少用112位以上。少8个字节的存储空间省不了多少Flash但安全性差距是数量级的。AAD附加认证数据把对象UID、版本号、存储位置等元数据放入AAD可以让篡改行为在解密前就被识别。如果不放AAD攻击者可能把A对象的内容整体搬到B对象的存储位置系统读B时发现数据“合法”但内容其实来自A。4.3 抗回滚不只是检测篡改安全存储要防的不仅仅是“数据被改”还有“数据被换成旧版本”。比如攻击者把设备的固件降级到有漏洞的旧版本然后从备份Flash里恢复旧存储数据让系统认为一切都正常——这种回滚攻击在现实中非常常见很多“无法修复”的设备漏洞就是这么被利用的。对抗回滚的核心是单调递增的Rollback Counter。每次固件升级或关键数据更新时计数器递增并安全存储。计数器本身也需要防篡改要么存入OTP区域写一次就不能改要么用Flash存储并配合签名/认证码校验并在启动时与固件版本联动校验。一个实用的联动方案是Bootloader启动时读取当前固件版本号和安全存储里的Replay Counter两者比对。若计数器小于固件中的最低要求版本则拒绝启动并进入恢复模式。这样即使攻击者恢复了旧存储数据也会因为计数器不匹配被拒绝整个信任链是闭合的。5. 实操在PSA Certified MCU上落地Secure Storage5.1 选择开发平台与参考软件目前最容易上手的PSA Certified MCU开发平台可以参考NXP LPC55S6x系列或STM32L5系列两者都内置Cortex-M33内核并支持TrustZone。软件上使用开源参考实现TF-MTrusted Firmware-M它实现了PSA的安全启动、安全存储、密码服务分区并且支持通过多样化平台接口适配不同MCU。搭建开发环境时我的建议是直接用Arm官方的开源工具链编译工具链arm-none-eabi-gcc或Arm Compiler 6调试下载OpenOCD CMSIS-DAP探针或使用芯片厂商自己的IDE不依赖IDE也能编但厂商工程整理好的配置文件能省不少事参考文档TF-M官方文档和PSA API头文件是首选比网上各种二手教程准确得多。例如psa/storage_common.h、psa/its.h中定义了完整API。另外提一句有读者问到Cadence OrCAD如何快速导出MCU引脚信息这和安全开发虽无直接关系但我在设计安全产品原理图时发现把SWD调试口、安全启动模式选择引脚、OTP烧录引脚这些关键信号提前规划好比后期飞线改版重要得多。建议在原理图阶段就用OrCAD的Part Manager把MCU的电源域、调试域单独标注方便后续跟固件团队对齐引脚约束。5.2 配置安全分区与构建SPE/NSPETF-M的工程通常一次性生成两个镜像SPE镜像安全侧固件和NSPE镜像非安全侧应用。构建的关键步骤是修改平台配置文件常见配置项包括配置项说明参考值FLASH_AREA_SIZE安全分区Flash大小典型64KB~128KBPS_ENABLED启用Protected Storage1ITS_ENABLED启用Internal Trusted Storage1ITS_MAX_ASSET_SIZE单个对象最大大小取决于业务数据如2KBITS_NUM_ASSETS支持的最大对象数如30~50个CRYPTO_AEAD_ALG加密算法AES-CCM或AES-GCM配置完成后编译脚本会把安全和非安全两份镜像链接到指定地址。这里最容易出的问题是Flash区域重叠如果安全Flash的基地址和大小配置与链接脚本不一致启动时TrustZone属性表会指错位置现象是安全固件能跑但非安全应用一访问就HardFault。排查时优先检查链接脚本里FLASH_ORIGIN与SAU配置是否匹配。我建议的做法是把内存布局表写进工程README像这样0x08000000: Bootloader非安全侧可读写保护0x08010000: SPE镜像Secure Flash0x08030000: NSPE镜像Non-Secure Flash0x08070000: SST存储区Secure Flash对象池这样大家读代码、调配置时心里都有谱不会你改一块我改一块。5.3 在非安全应用中调用安全存储API构建好SPE/NSPE镜像后写一个简单的存储接口调用验证整条链路是否通#include psa/its.h #define UID_DEVICE_CERT 0x1001 static const uint8_t cert_data[] device_cert_01; void secure_storage_demo(void) { psa_status_t status; // 写入一个设备证书对象标记为只写一次 status psa_its_set(UID_DEVICE_CERT, sizeof(cert_data), cert_data, PSA_STORAGE_FLAG_WRITE_ONCE); if (status ! PSA_SUCCESS) { // 处理错误 return; } // 读取并验证 uint8_t read_buf[sizeof(cert_data)]; size_t read_len 0; status psa_its_get(UID_DEVICE_CERT, 0, sizeof(cert_data), read_buf, read_len); if (status PSA_SUCCESS read_len sizeof(cert_data)) { // 数据读取成功 } }这段代码看起来平平无奇但它内部的调用路径是非安全侧通过SG指令进入Secure WorldTF-M的IPC层校验UID和缓冲区然后调用安全存储分区执行Flash读写和加解密最后返回结果。对业务代码来说不感知这些细节就能获得安全能力这正是PSA模型的优势。调试时有个小技巧如果psa_its_set返回PSA_ERROR_NOT_PERMITTED九成是UID冲突或对象已被标记为WRITE_ONCE如果返回PSA_ERROR_INSUFFICIENT_SPACE则是存储区满了或单对象超过ITS_MAX_ASSET_SIZE。把这两种错误码的排查逻辑写进自检函数能少踩很多坑。6. 实战中的典型问题与排查方法6.1 使用调试器时的访问权限问题开发阶段最常踩的坑和调试器有关。当SPE运行在Secure状态、NSPE运行在Non-Secure状态时调试器如果以非安全方式挂接会发现安全地址全是不可访问的“黑洞”——读出来全0xFF写不进去单步也看不到安全代码。解决思路分两种情况开发阶段关闭TrustZone的调试隔离设置或在调试探针中配置成安全调试模式从Secure Debug接口访问。具体方法因芯片而异常见的是在连接调试器前先用Bootloader把调试认证状态设为“开放”。现场调试故障如果设备已经锁死安全调试口就没法在线调试了。建议在固件中增加“安全日志”功能把安全存储的失败原因以事件码形式记录在一个非安全侧可读取的RAM区域通过串口或日志抽象层输出这样即使不能断点调试也能定位问题。6.2 GCM认证失败GCM认证失败是运行阶段最常见的异常之一表现为psa_its_get或psa_ps_get返回PSA_ERROR_INVALID_SIGNATURE。可能的原因按出现频率排可能原因排查方向Nonce重复检查Nonce生成逻辑是否在重启后被重置是否在异常路径上重复使用存储区被篡改用安全固件导出的校验和比对对象区前后变化UID与AAD不匹配确认写入时传入的UID和解密时校验AAD用的UID一致Flash写入顺序错误检查是否在数据完全写入前就更新了状态标记排查时不要直接输出密钥或密文而是把“认证失败时的元数据摘要”UID、期望长度、读取偏移和“失败错误码”以日志形式输出这两个信息已经足够定位绝大多数问题。6.3 Flash掉电导致的损坏设备在写入存储对象时突然断电恢复后对象读取失败。这个问题的根源不是加密算法而是Flash写操作不是原子的。我的处理方案是双管齐下在存储固件中实现“写后校验”写完数据后立即读回比对发现不一致就标记该对象为损坏并回退到旧副本。在应用层增加“恢复流程”启动时扫描所有对象发现损坏且无法恢复的对象从备份区或云端重新拉取。不过这里建议不要轻易在应用层做无限重试否则可能把Flash刷坏。更稳妥的做法是限制重试次数并给后续的升级流程留出操作空间。6.4 调试端口与产品化选择的权衡量产设备不可能向每个开发者都开放安全调试口否则等于给攻击者开了一扇门。产品化阶段通常选择下面两种策略之一完全关闭调试口芯片出厂后调试功能不可恢复开发和维护全靠日志升级。适合对物理安全要求极高的产品但要有极其完善的远程诊断机制。证书认证调试调试口默认关闭但允许持有特定证书的调试器临时解锁。兼顾了产线调试和售后维护但需要实现一个独立的调试认证服务管理证书生命周期。我的建议是产品定义时先想清楚“谁来调试、在哪个阶段调试”而不是等量产了再临时关闭调试口。很多项目就是没规划这一步导致产品上市后发现产线也无法烧录、售后无法分析只能回炉改板。踩过几次坑之后我最大的体会是安全存储的设计一定要和Secure Boot、安全固件升级、密钥管理体系打包考虑不能在项目后期“单独加一个加密模块”。前面提到的回滚保护、HUK派生、双槽写入每一样单独拿出来都是标准件但组合在一起并适配你具体MCU的Flash资源、启动流程和应用场景才是真正的工作量所在。如果还是从零开始建议从TF-M的SST模块跑起再逐步替换为自己的业务密钥管理和存储格式稳很多。