基于PSA Certified的MCU安全Flash存储方案解析

发布时间:2026/8/28 10:40:23
基于PSA Certified的MCU安全Flash存储方案解析 最近一个做智能门锁的朋友找我说他们的OTA升级出了大问题设备升级失败自动回滚后原来存储在Flash里的设备密钥数据全丢了云端直接拒绝这批设备接入。排查了一天最后发现根源很简单——他们的MCU把密钥和应用固件放在同一片Flash里没有任何隔离和版本回滚保护。这件事让我挺感慨的很多产品经理一说安全就是“加个加密芯片”但真正到了MCU这一层密钥存哪儿、固件怎么校验、升级回滚怎么防才是底层逻辑。这篇文章就围绕一个具体方案展开PSA Certified认证的MCU如何提供安全的Flash存储方案。我会从PSA认证分级讲起把安全启动、密钥存储、防回滚、生命周期管理这些核心环节逐一拆开结合我实际做过的项目给出可复现的配置和代码示例。适合正在做物联网设备、智能硬件、边缘节点尤其是需要保护设备证书、业务密钥、计量数据的开发者参考。就算你对PSA还不太熟看完也能知道怎么在一颗MCU上把安全存储这件事做扎实。1. 为什么需要PSA Certified一次固件回滚事故引发的思考讲方案之前先说清楚为什么靠传统MCU自己去规划存储很难做到安全。如果你处理过不止一个量产品应该对下面这些场面不陌生固件升级到一半失败bootloader回滚到旧版本结果新版本在Flash里写的密钥配置被旧代码当成无效数据清掉了或者产线上开了JTAG/SWD调试口返修时被第三方直接把Flash读出来密钥和证书全部泄露再或者只改了Flash地址映射旧固件仍能读新固件的存储区防了外部却没防住内部。这些问题不是功能缺失而是安全边界从一开始就没划分过。MCU的传统Flash使用方式是bootloader占一段应用占一段参数区占一段密钥和证书直接明文塞在参数区后面。这种安排对早期不需要联网的设备没什么大问题但智能设备一旦联网攻击面就不再只是物理接触还有固件抓包、OTA中间人、降级攻击等远程手段。到这一步就不再是“把密钥藏起来”的问题而是整个启动和存储链路都要经受架构级审视。1.1 传统MCU闪存存储的三大痛点我把这些年在项目里反复见到的存储安全问题整理成三个典型痛点不夸张地说只要做过量产设备的嵌入式工程师至少踩中过一个。第一个痛点是明文存储和调试接口直通。很多MCU出厂默认JTAG/SWD是开放的固件里也没有做读保护RDP或者读保护等级设置不当。产线上位机为了刷固件方便会把调试口一直开着到了用户手里哪怕只开了一次调试口芯片里的Flash数据就可以被直接读走。密钥、服务器地址、加密算法参数全暴露后续协议加密形同虚设。第二个痛点是缺少防降级机制。OTA升级时如果只校验“新固件签名合法”不校验“新固件比旧固件新”攻击者完全可以自己构造一个旧版本的固件旧版本签名密钥如果泄露了把设备降级回有漏洞的版本再用漏洞提取密钥。更常见的是崩溃后的自动回滚虽然回滚的是合法旧固件但旧固件的存储布局逻辑跟新版本不一致误删误写就把安全存储区破坏了。第三个痛点是敏感数据和应用数据没有隔离。同一个Flash地址空间应用代码可以任意寻址哪怕没有调试口只要应用被攻击者通过远程漏洞拿到了执行权限就能直接读取Flash里所有内容。要让存储变得安全必须从CPU核和总线层面对安全数据区域做访问控制而不是靠代码层面的“约定俗成”。1.2 PSA Certified在认证什么PSAPlatform Security Architecture是Arm牵头定义的一套物联网安全方案框架后来演进成了PSA Certified认证体系。它不只是一纸证书更像是一份可从架构上落地的安全需求清单。认证分三个级别从L1到L3对应的防护能力有本质区别。级别评估方式主要防护能力适合场景Level 1自评问卷流程审查证明厂商有安全设计流程和产品安全策略风险较低或成本敏感的消费类设备Level 2实验室评估验证基于硬件的软件隔离能力能抵御软件攻击智能家居、工业传感器、车联网T-BoxLevel 3实验室深度评估评估物理攻击防护如侧信道、故障注入支付终端、安全模块、高价值计量设备我在实际项目里最常建议的是Level 2这个级别明确要求MCU具备Safe/Protected storage、Secure Boot、Secure Update、Isolation等能力。这些能力不是芯片厂商开发的软件而是硬件结构和固件机制在出厂前就被认证过的结果。换句话说你选用一颗通过了PSA Level 2认证的MCU安全存储这块的硬件基础就等于有第三方背书了。1.3 谁该关注这个方案如果你正在做以下类型的产品我建议认真看完后面的内容需要接入云端并保存设备证书的产品例如门锁、摄像头、家电需要存储计量数据的电表、水表、工业传感器需要防抄板或者防固件被逆向的控制器以及任何需要OTA升级并且要求“升级失败能回滚但回滚不能破坏安全数据”的产品。这套方案确实比普通MCU的裸Flash存储复杂一些构建时间会长一点但换来的是密钥和证书即使被读取也只是密文固件升级回滚有了版本防降级保护调试口在生产完成后可以被永久关闭。对量产产品来说这几点都是实打实的成本节约——少一次现场返工比什么都值。2. 安全闪存存储的底层设计逻辑理解了为什么需要PSA之后我们来看它到底是怎么在MCU内部把安全存储这件事落地的。这部分是理解后面所有代码和配置的基础所以我尽量用生产过程看得见摸得着的逻辑讲。2.1 信任根与安全启动链路安全存储的前提是系统启动时的第一段代码必须可信否则后续所有校验都是空谈。这就是信任根Root of TrustRoT。在MCU上信任根通常固化在BootROM里芯片出厂时写死用户无法修改。它的职责是校验下一级固件的签名并决定是否继续执行。以典型的PSA/TF-M启动流程举例过程是这样的芯片上电后先从BootROM开始执行BootROM校验并加载BL2通常就是MCUbootBL2再校验Secure Firmware和Non-Secure Application的签名签名通过后才跳转执行。这个链条上一级的公钥哈希通常保存在eFuse/OTP区域是不可写入Flash的硬件熔断区。用日常来类比BootROM就像登机口的闸机负责验证证件是否真实BL2是海关检查行李箱里有没有违禁品应用固件是旅客只有前两道全部通过才能登机。一旦链条中间任何一环被篡改系统就停在那里不会执行被篡改的代码。2.2 密钥存放纪律eFuse与Flash分工很多工程师问我的第一个问题是安全存储是不是就是把密钥放在Flash的某个特定地址答案是一半对一半不对。真正的密钥层级是这样的芯片内部有唯一硬件密钥HUKHardware Unique Key它存储在eFuse/OTP区域出厂即烧写、无法读取、无法修改。业务密钥和设备证书放在Flash安全存储区但它们不会明文保存而是由HUK经过特定算法派生出的密钥加密后再写入Flash。这样设计的原因值得细说eFuse/OTP容量很小通常只有几百字节到几KB且只能烧写一次放不下大量业务数据而Flash容量大、可擦写但容易被读出。让HUK留在设备内部的“保险箱”让业务数据以密文形式躺在“普通保险柜”里双重保险。就算攻击者物理读出Flash拿到的也只是一串无法解密的密文因为没有硬件HUK离线暴力破解的成本会高到放弃。所以在规划存储时你需要明确什么数据必须放OTP什么数据放加密Flash什么数据可以明文放普通Flash。烧录进OTP的、需要一次性写死的例如根密钥哈希、防回滚计数器基准值需要频繁更新的业务数据例如会话密钥、设备证书、计量累计值使用PSA提供的Protected Storage功能写入加密分区不需要保密但需要防篡改的配置项例如固件版本号、启动参数建议放在带校验的区域而不是随随便便丢在应用代码旁。2.3 Secure与Non-Secure世界的访问控制PSA方案里MCU的地址空间会在硬件层面被划分为Secure和Non-Secure两套世界。简单理解就是一台芯片内部跑了两个“虚拟机”Secure世界负责安全启动、密钥管理、安全存储Non-Secure世界跑应用逻辑和通信协议。普通应用代码永远无法直接访问Secure世界的Flash区域这种隔离是由TrustZone或等价隔离硬件保证的。这样带来的一个实际好处是即使应用层被攻击者拿下了任意代码执行权限他也只能折腾Non-Secure区域。想读安全存储区必须先通过PSA定义的接口例如PSA Protected Storage API向Secure世界发起请求而Secure世界可以检查请求来源、数据长度、UID权限相当于给存储访问加了一道门禁。3. 实操从零搭建PSA安全存储理论讲完直接上实操。这一节我会以一颗通过PSA Level 2认证的通用MCU为例带着大家把安全存储从硬件选型一直做到应用层调用。虽然不同厂商的SDK有差异但底层的TF-M框架和PSA API是同一套看懂思路后换平台也能快速上手。3.1 硬件选型与开发环境准备选型时不用盯着“最高安全级别”而要看自身产品生命周期和成本。目前市面上主流的、明确宣传通过PSA Level 2认证的MCU包括NXP LPC55S6x系列、ST STM32L5系列、瑞萨RA系列的部分型号等。这些芯片内部集成了TrustZone-M或等价隔离硬件同时也配套了现成的TF-M固件包省去很多从零移植的工作。开发环境方面我推荐直接用各家的SDK通常已经默认开启了TF-M。以NXP的MCUXpresso SDK为例里面直接带有LPC55S69的Secure Boot和TF-M示例工程ST的STM32CubeProgrammer也支持密钥注入和OTP烧写。另外准备好OpenSSL用于生成固件签名密钥对以及Python环境TF-M构建脚本依赖。3.2 Flash分区规划与TF-M配置安全存储不是把数据随便往Flash一写就行它需要预先给Bootloader、Secure Firmware、Non-Secure App、Protected Storage、NV Counters预留独立分区。分区表错了后面所有校验都会失败。我习惯的一个典型内存布局如下以内部Flash 1MB为例分区起始地址大小用途BL2 (MCUboot)0x0000000064KB第二级引导校验固件签名Secure FW0x00010000256KBTF-M Secure服务Non-Secure App0x00050000512KB应用代码Protected Storage0x000D000064KB加密业务数据NV Counters0x000E00008KB防回滚计数器ITS/Internal Trusted Storage0x000E20008KB内部敏感数据分区规划完成后在TF-M构建配置里需要显式打开Protected Storage功能并配置存储区大小对齐。以CMake构建TF-M为例关键配置项大致如下cmake -DTFM_PLATFORMplatform \ -DCMAKE_BUILD_TYPERelease \ -DTFM_PROFILEprofile_small \ -DTFM_PS_ENABLEON \ -DTFM_ITS_ENABLEON \ -DTFM_NV_COUNTERS_ENABLEON \ -DTFM_ISOLATION_LEVEL2 \ -DBL2ON \ -DMCUBOOT_USE_PSA_CRYPTOON这里的TFM_ISOLATION_LEVEL2是关键它决定了Secure和Non-Secure之间的隔离强度Level 2意味着Non-Secure应用无法直接访问Secure存储必须通过IPC接口调用。构建完成后会生成BL2、Secure FW、Non-Secure App的镜像文件。3.3 固件签名与密钥注入安全启动链路里所有固件都必须有合法签名。这里我用OpenSSL生成一组RSA-3072密钥对实际项目中密钥位数不要低于RSA-2048更稳妥的是RSA-3072或ECC P-256。# 生成签名密钥对 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out provisioning_key.pem openssl rsa -in provisioning_key.pem -pubout -out provisioning_key_pub.pem # 计算公钥哈希并转成二进制格式 openssl dgst -sha256 -binary provisioning_key_pub.pem key_hash.bin然后把公钥哈希烧入芯片的OTP/eFuse区域。这一步在量产阶段非常敏感建议使用芯片厂商提供的工具完成烧写并保证此时调试接口处于关闭状态。烧写完OTP后可以用签名工具对MCUboot镜像签名# 使用MCUboot签名工具 imgtool sign \ --key provisioning_key.pem \ --align 8 \ --version 1.0.0 \ --header-size 0x1000 \ --pad-header \ --slot-size 0x10000 \ tfm_s_signed.bin tfm_s_final.bin注意OTP/eFuse是一次性写入区域写错就没有回头路。量产前一定要拿几颗测试芯片反复验证流程确认签名哈希、分区地址、版本号全部正确后再进入正式产线。我从没见过哪个人烧错OTP还能笑着回来的。3.4 应用层调用PSA存储API示例安全存储的最终目标是给应用层使用借助PSA APINon-Secure应用可以通过标准接口向Secure世界发起存储读写请求。下面是一段典型的存储和读取流程#include psa/protected_storage.h #include psa/crypto.h #define KEY_UID 0x12345678 static uint8_t device_secret[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 }; int store_secret(void) { psa_status_t status; // 初始化PSA Crypto建立与Secure世界的通信通道 status psa_crypto_init(); if (status ! PSA_SUCCESS) { return -1; } // 写入安全存储区写一次后不可再修改适合存放设备密钥 status psa_ps_set(KEY_UID, sizeof(device_secret), device_secret, PSA_STORAGE_FLAG_WRITE_ONCE); if (status ! PSA_SUCCESS) { return -2; } return 0; } int load_secret(uint8_t *buf, size_t buf_size, size_t *len) { psa_status_t status; status psa_ps_get(KEY_UID, 0, buf_size, buf, len); if (status ! PSA_SUCCESS) { return -3; } return 0; }代码里有两处值得特别注意。第一是psa_crypto_init()必须成功这是Secure世界通信通道建立的前提但我在很多示例工程里看到有人忽略返回值这会导致后续每次psa_ps_set都静默失败。第二是PSA_STORAGE_FLAG_WRITE_ONCE这个标志代表的是一次性写入保护设置之后该UID下数据就不能再修改或删除适合设备证书和出厂密钥但不适合需要定期更新的会话凭据业务密钥轮换时要用普通标志位。从Non-Secure应用调用存储接口时实际执行流程是应用调用PSA API - Secure世界接收到IPC请求 - Secure固件校验权限和数据长度 - 加解密后写入Flash - 返回结果。中间几层转换对上层透明但要知道这是个请求-响应的开销不是直接操作寄存器也正因为如此它才能保证Non-Secure应用永远接触不到真正的明文密钥。4. 更新链路上的防回滚与密钥轮换安全存储不只是“保存数据”它跟固件更新机制紧密咬合。很多设备刚部署时一切正常一次OTA后钥匙就丢了原因就是更新链路没有跟存储的生命周期联动起来。这一节单独把更新链路拉出来讲因为它是安全存储方案里最容易被忽略也最致命的一环。4.1 NV计数器如何挡住降级攻击降级攻击的原理前面提过攻击者想办法让设备执行旧版本固件利用旧版本已知漏洞攻击系统。要防住这种攻击单靠签名校验是不够的因为旧固件的签名如果仍然有效比如厂商没吊销旧版密钥设备会认为是合法固件。PSA方案里会用NV计数器Non-Volatile Counter记录当前已部署的固件版本号。每次固件升级成功后Secure世界会单调递增该计数器启动时Bootloader会校验新固件的版本号是否大于等于当前计数器值不满足就不启动。在MCUboot和TF-M的配置里需要在BUILD配置中打开防降级选项-DMCUBOOT_DOWNGRADE_PREVENTIONON -DTFM_NV_COUNTERS_ENABLEONNV计数器存储在内部Flash的独立区域它只允许递增不允许递减而且递增操作由Secure世界固件控制Non-Secure应用无法直接修改。这意味着即使攻击者把应用固件刷回旧版本Bootloader也会因为NV计数器的版本号比旧固件高而拒绝启动。实际项目中要特别留意计数器分区大小计数器用Flash字节存储有擦写次数限制频繁OTA会导致该区域提前磨损。量产设备建议把OTA频率控制住并且在固件开发阶段就估算Flash擦写寿命和整机使用年限的匹配度。如果设备需要频繁更新固件比如每月一次NV计数器区域最好预留更大的空间并使用带磨损均衡的存储驱动。4.2 密钥轮换现场实践设备部署后如果需要更新业务密钥实现方式要格外小心。直接调用psa_ps_set覆盖同一个UID如果中途断电Flash写入可能处于中间状态旧密钥丢了、新密钥没写完设备直接变砖。更稳妥的做法是采用双缓冲机制先为新密钥分配一个临时UID写入写入成功后再把主UID的读写原子地切换到新密钥。比如首先用psa_ps_set(NEW_KEY_UID, ...)写入新密钥然后调用psa_ps_set(MAIN_KEY_UID, ...)更新主UID的索引指针最后用psa_ps_remove(NEW_KEY_UID)清理临时数据。这个过程中的关键点是主UID的更新操作应该使用支持事务的存储API如果平台提供或者在代码层做CRC校验确保万一中断下次启动时能自动回退到旧密钥。另外使用PSA_STORAGE_FLAG_WRITE_ONCE保存的出厂根密钥是不允许轮换的轮换的只能是上层业务密钥。设计密钥体系时就要把“不可变根密钥”和“可轮换业务密钥”分层这样密钥轮换时根密钥稳定不动否则设备会变成一台无法证明自己身份的“孤儿机”。4.3 生命周期状态与量产管控MCU安全存储还有一个容易被忽略的维度芯片生命周期状态Lifecycle State。典型状态分为开发态、部署态、生产态三档。在开发态调试接口完全开放方便调试但这时固件里绝不能放生产密钥部署态允许部分调试能力可以写入测试密钥一旦进入生产态调试接口永久关闭OTP区域锁定所有安全属性被固化。生命周期状态从开发到生产是不可逆的单向过程这是为了确保出厂后的设备无法被调试仪器“开盖取件”。量产时最常见的问题是厂商为了返修方便把一批设备留在部署态结果这批设备的安全等级直接降到可被物理攻击的级别。我经验是进入生产态之前必须做一次完整的功能测试包括密钥注入、固件签名校验、安全存储读写、OTA升级、防回滚验证。测试通过后关闭所有调试接口、锁定OTP再出厂。产线上的任何“临时方便”都可能成为事后安全事件的入口。5. 踩坑记录与排查技巧最后分享一些实操中积累的排查经验。安全功能最大的特点是平时不起作用一旦起作用就是系统级故障而且往往很难定位。下面这些问题都是我们自己在项目中真实踩过的希望你能少走弯路。5.1 常见问题速查表现象可能原因排查思路上电后系统卡死在BootROM不进BL2公钥哈希没烧进OTP或固件签名与OTP中的公钥不匹配检查OTP是否为空或写错重新签名并烧录BL2加载Secure FW时报hash错误Secure FW镜像被分区擦除工具改写确认地址/大小与分区表一致重新刷写镜像调用psa_ps_set一直返回错误码未初始化psa_crypto_init或存储分区满打印初始化返回值检查PS分区剩余空间读回数据全是0xFF存储区被擦除或Flash写保护导致写入失败检查Flash保护寄存器、分区地址重新写数据设备执行旧固件成功NV计数器未打开或版本比较逻辑不正确确认MCUBOOT_DOWNGRADE_PREVENTION已置ONOTA升级后密钥丢失旧版本存储布局与新版本不兼容回滚时误删使用双缓冲密钥存储并校验版本兼容性排查这类问题时建议先在Secure世界增加一条简单的调试日志通道仅开发态开启打印初始化结果、存储调用结果、分区地址信息。日志里不要打印任何密钥内容只打印状态码和错误位置。否则安全日志本身又成了新的泄露面。5.2 高价值实操建议先说一个跟Flash磨损相关的血泪教训。PSA Protected Storage默认会把数据加密后写入内部Flash内部Flash的擦写寿命通常只有1万次到10万次。如果应用进程频繁把传感器状态写入安全存储比如每5分钟一次一年下来就是10万次擦写Flash很快报废。我建议把高频的小数据量业务状态放在普通RAM或带磨损均衡的外部Flash中只在关键节点断电前、断电恢复时、认证刷新时同步到安全存储区。产线工具方面密钥注入和OTP烧写要尽可能与固件刷写分离。如果产线上传固件和注入密钥是同一台电脑同一个脚本一旦某次脚本被污染批量设备的安全根基都会同样被污染。我见过的较稳妥做法是一台独立工位设备负责OTP注入不和固件烧写共享工具链注入后的芯片做标记下一道工序才允许烧固件。最后再提一个细节不要在应用日志里打印UID和存储长度。很多人写开发日志习惯性把psa_ps_set(uid, len)的参数打出来看起来无害但攻击者可以通过观察日志流量推测安全存储区数据模式。安全项目里任何不必要的信息都是信息泄露的一部分日志做好脱敏也是安全设计的一环。回到开头那个智能门锁的项目后来我们把方案调整成基于PSA Level 2认证的MCU配合TF-M安全存储和防降级计数设备密钥丢失的问题彻底消失。虽然前期适配花了些时间但换来的是整个产品生命周期里不用再担心Flash被读、密钥被抄、固件被降级这几类最麻烦的事故。如果你也在规划下一款需要联网的终端设备我建议早点把PSA Certified和Secure Flash Storage纳入选型评估而不是踩完坑再补课。