用STM32CubeMX生成TF-M项目,实现与ESP32的安全启动联动
你有没有遇到过这样的场景?
一个IoT设备里,主控是颗带TrustZone的Cortex-M33芯片,通信靠的是ESP32。看起来功能齐全、成本可控——但一旦有人拆开外壳,用JTAG刷个恶意固件上去,整个系统就彻底沦陷了。
更糟的是,ESP32虽然有自己的安全启动机制,但它那套基于eFuse和RSA签名的流程, 信任根散落在各处,缺乏统一管控 。开发阶段私钥可能还存在开发者的笔记本里,产线烧录时又得手动导入……这哪是什么“安全”,简直是给攻击者留后门。
那么问题来了:我们能不能把 整个系统的信任根集中起来 ,让一块真正具备硬件级安全能力的MCU来掌管全局?比如,用STM32L5运行TF-M作为“安全大脑”,由它来验证并控制ESP32的每一次启动?
答案是肯定的。而且现在借助 STM32CubeMX ,你可以不用手写一行TrustZone配置代码,就能自动生成完整的TF-M工程结构,并通过PSA API完成对ESP32镜像的签名验证与安全加载控制。
从零构建双芯片安全体系:为什么需要STM32+ESP32架构?
先别急着点“Generate Code”。咱们得先搞清楚:为什么非得用两颗芯片?不能直接在ESP32上做安全吗?
说实话,我也希望可以。但现实很骨感:
- ESP32(包括ESP32-S系列)不支持Arm TrustZone技术,意味着没有硬件隔离执行环境;
- 它的安全启动依赖ROM引导程序 + eFuse中预烧录的公钥哈希,灵活性差;
- 私钥管理混乱,常见做法是在PC端签名后再烧录,极易泄露;
- JTAG调试接口默认开放,除非启用“永久禁用”模式,否则物理攻击门槛极低。
反观STM32L5系列,它是目前市面上少数原生支持Armv8-M TrustZone的Cortex-M33芯片之一。配合TF-M(TrustZone for Armv8-M),能提供真正的 安全世界(Secure World)与非安全世界(Non-Secure World)隔离 ,连内存访问权限都由SAU/IDAU硬件强制划分。
所以,聪明的做法不是强求ESP32变安全,而是让它“听话”——只允许在通过验证后才能启动。
于是就有了这个经典组合:
🛡️ STM32L5 做“保安队长”
🔧 负责密钥存储、加密运算、固件签名验证
✅ 决定什么时候放行ESP32启动📶 ESP32 做“通信小弟”
📡 只管Wi-Fi/蓝牙连接、数据收发
⛔ 没有授权?休想跑起来!
这种“ 安全主控 + 无线协处理器 ”的架构,在工业网关、医疗终端、车联网T-Box中越来越常见。关键是:如何快速搭建起这套机制,而不是花三个月去啃TF-M源码?
STM32CubeMX一键生成TF-M项目:真的能做到“开箱即用”吗?
我曾经也怀疑过。毕竟TrustZone听起来就很底层,SAU、MPU、SG调用、veneer函数……光术语就够学一周了。
但当我打开STM32CubeMX 6.10版本,选中STM32L562QEI,点击“Project Manager” → “Advanced Settings” → 启用“TrustZone”,然后勾上“TF-M”模块……几分钟后,一个包含 安全初始化、分区配置、PSA服务接口 的完整工程就出来了。
你没看错, 不需要改任何链接脚本,也不用手动配置SAU寄存器 。
自动生成了什么?
看看它的目录结构就知道有多贴心:
/Projects/
├── Secure/
│ ├── Core/
│ │ ├── Src/
│ │ │ ├── tfm_init.c # TF-M启动入口
│ │ │ ├── tz_config.c # SAU/IDAU自动配置
│ │ │ └── secure_services.c # 安全服务注册
│ ├── Inc/
│ └── Middlewares/TFM/ # TF-M框架源码(精简版)
│ ├── platform/
│ ├── services/
│ └── configs/
├── NonSecure/
│ ├── Core/
│ │ ├── Src/
│ │ │ ├── main_ns.c # 非安全主函数
│ │ │ └── system_ns.c
│ │ └── Inc/
│ └── MDK-ARM/ # 支持Keil、IAR、Makefile
└── Drivers/ # 标准外设库
是不是有种“原来这么简单”的错觉?其实背后隐藏了不少工程智慧。
关键自动化逻辑解析
✅ 自动划分Secure/Non-Secure内存区域
假设你的芯片有512KB Flash和256KB RAM:
- Secure区占用前256KB Flash + 128KB RAM(用于TF-M核心和服务)
- Non-Secure区使用剩余部分(留给应用)
这些都在
.ioc
文件中可视化配置,生成时自动更新链接脚本(如
STM32L562QEIX_FLASH.ld
)。
✅ 自动生成TZ_Config.c:告别手动设置SAU
以前你要自己算地址范围、写
SAU->RNR
、
SAU->RBAR
、
SAU->RLAR
……稍有不慎就会导致HardFault。
现在呢?CubeMX会根据你的配置生成类似如下代码:
void TZ_Config(void)
{
/* 设置Region 0: Secure Flash [0x0C000000, 0x0C03FFFF] */
SAU->RNR = 0;
SAU->RBAR = 0x0C000000UL;
SAU->RLAR = 0x03FFFFUL | SAU_RLAR_ENABLE_Msk;
/* Region 1: Non-Secure SRAM (upper half) */
SAU->RNR = 1;
SAU->RBAR = 0x20008000UL;
SAU->RLAR = 0x07FFFFUL | SAU_RLAR_ENABLE_Msk | SAU_RLAR_NSATTR_Msk;
/* 启用SAU */
SAU->CTRL = SAU_CTRL_ENABLE_Msk;
}
完全不用你操心,甚至连NSC(Non-Secure Callable)段的定义都帮你处理好了。
✅ 提供标准PSA API封装头文件
最爽的一点是:你在非安全世界可以直接include标准头文件:
#include "psa/crypto.h"
#include "psa/storage.h"
然后就像调用普通函数一样使用加密服务,底层的SG跳转、上下文切换、栈保护全由TF-M runtime搞定。
如何让STM32为ESP32的安全启动“把关”?
好,现在STM32这边已经跑起来了,TF-M也初始化完成了。接下来才是重头戏: 怎么用它来控制ESP32的启动过程?
我们得回到ESP32本身的启动机制上来。
ESP32安全启动的本质:签名验证链
ESP32的安全启动V2(Secure Boot V2)流程大致如下:
- ROM引导程序读取eFuse中指定位置的 公钥摘要(digest) ;
- 加载Flash中的Bootloader镜像;
- 使用该摘要对应的公钥验证Bootloader的ECDSA/RSA签名;
- 成功则继续加载App,失败则停机。
注意关键点: 它验证的是“谁签的”,而不是“内容是否正确” 。也就是说,只要你用正确的私钥签名,哪怕是个挖矿程序,它也会照常运行。
所以我们不能只依赖ESP32自身的机制,而要加上一层外部审查——而这正是STM32的价值所在。
设计思路:以STM32为信任根,延伸至ESP32
我们可以这样设计:
🌐 所有ESP32固件升级包均由云端下发 → 存入STM32本地安全存储
🔐 STM32使用TF-M中的PSA Crypto服务计算其SHA-256哈希值,并用内置私钥签名
🧪 启动前,STM32将签名发送给服务器或本地策略引擎进行比对
✅ 若匹配,则释放GPIO信号,允许ESP32启动
❌ 否则保持复位状态,记录入侵事件
这样一来,即使攻击者拿到了ESP32的签名私钥(比如从旧设备提取),只要STM32不认可这份固件,它依然无法运行。
实现细节:GPIO控制 + 固件校验
假设我们使用以下引脚连接:
| STM32 引脚 | 功能 | ESP32 引脚 |
|---|---|---|
| PC13 | BOOT_CONTROL | GPIO0 |
| PC14 | ESP_ENABLE | EN |
| USART3 | 通信通道 | U0RX/U0TX |
启动逻辑如下:
// main_ns.c(非安全主函数)
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init(); // 初始化控制引脚
MX_USART3_UART_Init();
if (tfm_ns_interface_init() != TFM_SUCCESS) {
Error_Handler();
}
// 默认拉低EN,使ESP32处于关闭状态
HAL_GPIO_WritePin(ESP_ENABLE_GPIO_Port, ESP_ENABLE_Pin, GPIO_PIN_RESET);
// 从外部Flash或SD卡加载ESP32固件镜像
uint8_t *firmware = load_esp32_firmware_from_storage();
size_t fw_len = get_firmware_size();
// 请求TF-M进行签名
psa_status_t status;
uint8_t hash[32], signature[64];
size_t sig_len;
status = psa_hash_compute(PSA_ALG_SHA_256, firmware, fw_len, hash, sizeof(hash));
if (status != PSA_SUCCESS) goto deny_boot;
status = psa_sign_message(FLASH_SIGNING_KEY_ID,
PSA_ALG_ECDSA_WITH_SHA_256,
hash, sizeof(hash),
signature, sizeof(signature), &sig_len);
if (status != PSA_SUCCESS) goto deny_boot;
// TODO: 将signature发送至云端验证 或 查本地白名单
if (!is_signature_trusted(signature, sig_len)) {
goto deny_boot;
}
// ✅ 验证通过!允许启动ESP32
allow_esp32_boot();
goto end;
deny_boot:
trigger_alarm_led(); // 点亮红色报警灯
log_security_event("Unauthorized firmware detected");
while(1); // 锁死系统
end:
while(1);
}
其中
allow_esp32_boot()
函数实现如下:
void allow_esp32_boot(void)
{
// 拉高EN引脚,供电使能
HAL_GPIO_WritePin(ESP_ENABLE_GPIO_Port, ESP_ENABLE_Pin, GPIO_PIN_SET);
// 短暂延迟,等待电源稳定
HAL_Delay(10);
// 设置BOOT引脚为高电平(正常启动模式)
HAL_GPIO_WritePin(BOOT_CONTROL_GPIO_Port, BOOT_CONTROL_Pin, GPIO_PIN_SET);
// 此时ESP32开始执行已签名的Bootloader
printf("ESP32 boot sequence triggered.\n");
}
看到没?整个过程就像一位尽职的安检员,只有确认无误才会放行。
密钥安全管理:别再把私钥放在电脑上了!
很多人做安全启动的第一步,就是在自己的MacBook上生成一对RSA密钥:
openssl genrsa -out signing_key.pem 3072
然后用这个key去签名固件。问题是——下次你电脑丢了怎么办?CI/CD流水线被渗透了怎么办?
真正的安全,是从第一天就杜绝密钥导出的可能性。
而TF-M + STM32L5正好提供了这样的能力。
利用PSA Protected Storage保存密钥
TF-M内置了一个叫 PSA Protected Storage 的服务,它可以将敏感数据加密后存入Flash特定区域,默认只能由安全世界访问。
更重要的是: 你可以在安全世界内直接生成密钥对,且私钥永不离开芯片!
示例代码如下:
// 在secure_app.c中创建密钥
psa_status_t create_signing_key(void)
{
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_status_t status;
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE);
psa_set_key_algorithm(&attr, PSA_ALG_ECDSA_WITH_SHA_256);
psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_PERSISTENT);
// 设置Key ID(需在配置文件中声明持久化空间)
psa_set_key_id(&attr, FLASH_SIGNING_KEY_ID);
status = psa_generate_key(&attr, NULL, 0);
psa_reset_key_attributes(&attr);
return status;
}
这段代码会在第一次启动时生成一条P-256椭圆曲线密钥对, 私钥保存在安全存储中,永远无法被读取 ,只能用于签名操作。
你想导出私钥?对不起,PSA规范不允许。
你只能通过
psa_sign_message()
接口让它“帮忙签个名”,但看不到里面的内容——就像银行保险柜,你能存东西、取东西,但看不到金库长什么样。
这才是现代嵌入式安全的核心思想: 不是防止别人拿到数据,而是让他们即便拿到了也无法使用。
实际痛点解决:那些年我们在现场踩过的坑
理论讲完,来说点真实的。
我在参与一款智能锁项目时,就遇到过几个典型问题,最终都是靠这套架构解决的。
痛点一:OTA升级后变砖,客户打电话骂娘
原因是什么?因为开发人员用错了签名密钥!
原本应该用生产环境的key签名,结果用了测试key。设备启用Secure Boot后,拒绝加载“非法”固件,直接进不了系统。
传统方案只能返厂重新烧录eFuse——成本高、周期长。
我们的解决方案:
✅ 所有OTA包先传到STM32缓存区
🔍 STM32使用内置密钥重新签名ESP32固件
🚀 下发已认证版本给ESP32
这样哪怕原始包签错了,STM32也能“救场”,只要内部策略允许即可放行。
而且支持灰度发布:只对特定批次设备开启新版本验证规则。
痛点二:产测阶段如何不停机调试?
工厂测试需要频繁烧录不同固件,但如果启用了Secure Boot,每次都要重新签名,效率极低。
解决办法是引入“ 调试模式开关 ”。
我们在STM32中设计了一个逻辑:
if (check_debug_mode_jumper()) {
// 检测到短接帽,进入测试模式
allow_esp32_boot_without_verification();
} else {
perform_full_signature_check();
}
测试模式下,STM32会跳过签名验证,直接启动ESP32,方便产线快速刷机。
出厂前移除跳线帽,自动恢复安全模式。
既不影响生产效率,又保障了最终产品的安全性。
痛点三:如何应对物理攻击?
有人试图用探针监听SPI总线,抓取固件传输过程。
对策很简单: 加密通信 + MAC校验
我们在STM32和ESP32之间建立一条AES-GCM加密通道:
// STM32侧加密后再发送
uint8_t encrypted_fw[FW_SIZE + 16];
size_t enc_len;
psa_aead_encrypt(key_id, PSA_ALG_GCM, nonce, iv_len,
NULL, 0, fw_plaintext, fw_len,
encrypted_fw, sizeof(encrypted_fw), &enc_len);
HAL_UART_Transmit(&huart3, encrypted_fw, enc_len, 1000);
ESP32收到后解密并写入Flash。没有密钥?拿到数据也没用。
甚至还可以加入时间戳防重放攻击。
架构优化建议:不只是“能用”,更要“健壮”
这套方案落地后,我还总结了一些进阶优化点,值得你在设计时考虑。
🔄 双向认证:不仅STM32验证ESP32,也要反过来
目前只是单向控制。但如果ESP32被替换为假芯片,照样可能造成信息泄露。
可以增加:
- ESP32启动后向STM32发起挑战-应答认证;
- 使用PSA Attestation服务生成设备凭证;
- 双方协商会话密钥,建立双向加密信道。
⚡ 低功耗协同:别让ESP32一直耗电
很多IoT设备要求待机功耗低于10μA。如果STM32一直开着串口等ESP32回复,显然不行。
改进方案:
- STM32进入Stop Mode,通过EXTI唤醒;
- 外部传感器触发事件 → STM32苏醒 → 验证并启动ESP32 → 发送数据 → 完毕后再次关闭ESP32;
- 实现“按需激活”,大幅提升续航。
📦 固件缓存策略:要不要每次都下载完整镜像?
对于大尺寸固件(>1MB),每次OTA都从云端拉取并不现实。
可以在STM32外挂一片QSPI Flash,专门用于缓存最新ESP32固件。
同时维护一个元数据区:
{
"version": "2.1.0",
"sha256": "a1b2c3d...",
"timestamp": 1712345678,
"signature": "..."
}
下次启动时先比对哈希值,避免重复校验。
写在最后:这不是炫技,而是必须
有人问我:“我们产品只是个温湿度传感器,有必要搞得这么复杂吗?”
我的回答是: 当你不知道对手是谁的时候,就要假设他是国家级黑客。
今天的小设备,明天可能是你家门锁、医院呼吸机、电网继电器。一次成功的攻击,代价远超你节省下来的那几块钱BOM成本。
而STM32CubeMX + TF-M带来的,不仅是技术上的可行性,更是 工程落地的可操作性 。
你不需要成为密码学专家,也能构建出符合PSA Level 2标准的安全系统;你不必通读数百页ARM文档,就能拥有硬件级信任根。
这才是开源生态与成熟工具链的力量。
如果你正在做IoT终端开发,请认真考虑这个问题:
👉
你的设备,真的安全吗?还是只是“看起来挺安全”?
也许,是时候让STM32当一次“保安队长”了。🔐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
1万+

被折叠的 条评论
为什么被折叠?



