机密固件泄露的坑,我替你先踩了一遍。玩 ESP32 的小伙伴应该都干过这事儿:跑通了功能,拿烧录器把固件一写,就兴冲冲拿去量产或者交付了。这个操作放在个人项目或原型验证阶段没什么毛病,可一旦涉及商业产品、付费服务,或者设备要部署到用户现场,隐患就来了——你的 Flash 芯片里躺着的,是完整的、可以直接被读出来的二进制固件。别人拿个烧录器,或者通过 ESP32 的esptool.py read_flash命令,就能把你的代码逻辑、WiFi 密码、云平台密钥、甚至 TLS 证书一锅端走。
现在的芯片级方案,就是给 Flash 里的内容做加密(Flash Encryption),让外部读取变成一堆密文;再给固件加上签名校验(Secure Boot),确保只有你签过名的固件才能启动。这套组合拳打下来,固件泄露和篡改两个大坑算是能堵上大半。
这篇算是“ESP32 快速入门”系列的第十一篇,重点讲讲 Secure Boot v2 和 Flash Encryption 的原理、启用流程、实操步骤,以及我在真机上踩过的一些坑。不管你是做智能硬件、边缘网关还是开源周边,只要产品里用了 ESP32,这篇都值得花十分钟看完。
1. 安全启动与 Flash 加密的整体设计思路
1.1 两个功能各自解决什么问题
先说结论:Flash 加密是防“读”,Secure Boot 是防“改”。两者是独立的机制,但实际部署时建议一起上,因为不配合会出漏洞。
Flash Encryption 的作用很直白:把存储在外部 SPI Flash 上的应用程序固件、文件系统、配置数据加密。ESP32 内置的硬件 AES 引擎会在 CPU 取指令或者读取数据的时候实时解密。也就是说,固件在 Flash 里是密文,但在运行时对 CPU 是透明的,代码照常执行。这样一来,攻击者就算把 Flash 芯片拆下来,用编程器读出里面的二进制,看到的也是一堆随机数一样的密文,没有密钥还原不出原始固件。
Secure Boot v2 的作用则是确保芯片只运行经过你私钥签名的固件。ESP32 芯片内置的eFuse 存储器里保存着公钥摘要(或者公钥本身)。每次上电启动时,BootROM(掩膜 ROM 里的只读引导程序)会用硬件加速器验证固件头部的数字签名。验证通过才把控制权交给二级引导程序,否则拒绝启动。这意味着攻击者即使拿到了密文,想改写固件逻辑也是徒劳——改任何一个小字节,签名校验就会失败。
1.2 为什么要两个一起用:安全模型的对撞
很多人会问:我只开 Flash Encryption,不开 Secure Boot,行不行?
理论上可以,但这就是典型的安全缺口。Flash 加密的密钥(flash encryption key)存放在 eFuse 里,而Secure Boot 是用来保护这个密钥和引导链路的关键一环。如果不开 Secure Boot,攻击者可以制作一个自定义的引导程序,通过 ROM 的串口下载模式注入,或者直接替换掉 Flash 里的 bootloader。这个伪引导程序可以在系统启动时偷偷读取解密后的代码(因为 CPU 取指令是解密的),然后把固件内容通过串口、外设或网络传出去。这是真实的攻击路径,业内叫“Bootloader 替换攻击”。
反过来,只开 Secure Boot 不开 Flash 加密也一样有问题。固件虽然不能被篡改,但 Flash 里的内容全部是明文,攻击者用编程器一读,代码照样泄露。所以正统的做法是:同时启用 Secure Boot v2 和 Flash Encryption,并保持默认的Development 模式(对应开发阶段)或Release 模式(对应量产阶段)。
1.3 引入 eFuse:整个安全体系的地基
eFuse(电子熔丝)是 ESP32 芯片内部一组一次性可编程存储器。它跟 Flash 不一样:Flash 可以反复擦写,eFuse 一旦从 0 写成 1,就再也回不去了。eFuse 里的每一位,都像一张不能撕毁的标签。ESP32 有两种类型的 eFuse:
- 写保护(Write Protect):阻止后续再对某个 eFuse 字段写入。
- 读保护(Read Protect):阻止从外部读取某个 eFuse 字段。
Secure Boot 和 Flash Encryption 的密钥、功能开关、模式选择,全部存放在 eFuse 中。这也决定了这两个安全功能有一个共同的特点:启用操作是不可逆的。特别是密钥写入 eFuse 之后,一旦烧错或者丢失备份,这颗芯片基本就报废了。我后面实操环节会专门讲怎么避免“一失足成千古恨”。
2. 核心机制解析:Secure Boot v2 与 Flash Encryption 的底层原理
2.1 Secure Boot v2 的启动链路与签名验证流程
Secure Boot v2 是 ESP-IDF v4.0 之后引入的改进版本,它使用RSA-3072 签名算法(早期版本 v1 是 RSA-1024,已不推荐)。v2 的安全引导流程是整个引导链逐级验证:
- 芯片上电复位。BootROM(芯片出厂固化的引导程序)开始执行。
- BootROM 读取 eFuse 中的
SECURE_BOOT_EN标志位。如果该位被烧写为 1,BootROM 进入 Secure Boot 验证模式。 - BootROM 从 Flash 地址
0x0(Bootloader 分区)读取引导程序的签名块,使用内置的 RSA 公钥哈希(这个哈希存放在 eFuseSECURE_BOOT_KEY区域,或者是在开发模式下的secure_boot_v2_public_keydigest)验证 bootloader 签名。 - 验证通过后,BootROM 将控制权交给 bootloader。Bootloader 继续验证
partition table(分区表)和app(应用程序)分区的 RSA 签名。 - 每一级在跳转前,都会验证下一级镜像的签名以及整个镜像的 SHA-256 哈希。任何一步失败,芯片进入死循环或直接拒绝引导。
这个链式校验的强度在于:攻击者想绕过 Secure Boot,必须在拿到你的 RSA 私钥的前提下,对所有镜像重新签名。而这把私钥,在正确部署的前提下,只存在于你的构建服务器或开发机上,芯片和固件包里都没有。
有个容易被忽略的细节:Secure Boot v2 模式也有一个Secure Boot 使能位,位于 eFuse 的ABS_DONE_0。这个位一旦烧写,BootROM 将永久启用安全引导,且不可逆。开发模式下,ESP-IDF 会提示你有一次“再次确认”的机会;量产模式的烧写脚本则直接一烧到底。
2.2 Flash Encryption 的密钥生成策略与全盘加密模型
Flash Encryption 的原理是基于XTS-AES-128算法,对 Flash 中的内容进行扇区级加密。ESP32 支持两种 Flash 加密模式:
- Development 模式(开发模式):flash encryption key 可以随机生成,然后写入 eFuse,也可以由用户手动指定。此模式下,密钥未加密写入 eFuse,且允许通过
esptool.py重新烧录固件。重启后加密会生效,但保留再次烧录的能力。这个模式只适合开发调试。 - Release 模式(量产模式):flash encryption key 随机生成并写入 eFuse 后,
FLASH_CRYPT_CONFIG、SPI_BOOT_CRYPT_CNT等 eFuse 位会被写死(写保护),且DISABLE_DL_ENCRYPT、DISABLE_DL_DECRYPT等下载模式的加密/解密通道会被关闭。此时 Flash 加密进入“封死”状态,唯一的烧录途径是本地 OTA(通过网络升级),串口烧录基本被封禁。
密钥写入 eFuse 后,Flash 中的内容在首次启动时会被自动加密。ESP-IDF 的idf.py flash流程中,如果检测到 Flash 加密已使能,会自动将编译好的明文固件拷贝到内存中,通过加密通道写入并烧写到对应分区,避免固件在串口传输过程中以明文泄露。用一个直观的类比来形容这个加密模型:
整个 Flash 芯片像一个大保险柜。以前保险柜的门不关,任何路过的人都能翻里面的文件(明文)。现在保险柜的门关上了(Flash 加密),但 ESP32 手里有一把唯一的钥匙(eFuse 里的密钥)。别人就算把这个保险柜整个搬走,打不开门也拿不到文件,因为抽屉里的每一份文件都是拿钥匙加密过的密文。
Flash 加密会覆盖整个应用分区(factory、ota_0、ota_1)、NVS 分区、以及 SPIFFS/LittleFS 分区。但要注意:不是所有数据都需要加密。ESP-IDF 允许在分区表中通过encrypted标志指定哪些分区需要加密,未标记的分区以明文存储。常见的做法是:NVS 里存 WiFi 密码等敏感信息必须加密,而 OTA 升级的临时缓存分区可以考虑不加密以提升性能。
2.3 密钥存储与不可逆性:一把钥匙锁死一扇门
理解这两个功能,最核心的一点是要接受“不可逆”这个事实。eFuse 是一次性的,烧录之前你可以模拟、可以纠结,但一旦执行写入操作,后面就没有后悔药。
具体来说,Secure Boot v2 的密钥是2048 位 RSA 公钥的 SHA-256 哈希,被放置在 eFuse 的SECURE_BOOT_KEY区域;Flash Encryption 的密钥是256 位随机数,存放在独立的FLASH_CRYPT_KEYeFuse 区域。这两把钥匙一旦写死,都不支持重新生成。
开发阶段的“后悔药”是:ESP-IDF 在Development模式下会保留重新烧录的能力。但你一旦选择Release模式并把写保护烧断,芯片就只能运行当前 Flash 里的内容 + 通过 HTTPS/OTA 签名的后续固件。所以我的建议是:除非是最后一量产,否则永远不要碰 Release 模式。开发板上用 Development 模式跑通全部流程,再把同样的配置脚本转移给产线。
3. 实操全流程:从开启功能到烧录避坑
3.1 前置准备:硬件、软件与密钥文件
在做任何操作之前,先检查你的环境是否满足条件。我用的是某款常见的 ESP32-WROOM-32D 模组,开发环境是ESP-IDF v5.x(建议至少 v4.4 以上,Secure Boot v2 体验才完整)。
需要准备的东西:
- 一块 ESP32 开发板,最好是带 16MB 以上 Flash 的型号(8MB 也行,但 OTA 双分区会比较紧张)。
- 一个 USB 转 UART 适配器或开发板自带的串口芯片。
- 最新版 ESP-IDF 环境。
- 一台 Linux/macOS 电脑,Windows 也能做,但命令行体验略差。
- Python 环境(用于运行
esptool.py)。
在开始配置安全功能前,先确定自己的分区表方案。我推荐使用Factory + OTA 双分区结构。原因后面会说——安全启动 + OTA 升级,才是量产设备的完整生命周期支持。
3.2 开启 Secure Boot v2:配置生效前的关键一步
在工程目录下,执行:
idf.py menuconfig进入Security features菜单:
- 打开Enable hardware secure boot。
- 选择Secure Boot V2。
- 在Secure boot version处确认是
2。 - 选择签名私钥生成方式:建议首次配置时选择
Generate signing keys automatically,ESP-IDF 会在构建目录下生成secure_boot_signing_key.pem。
保存退出 menuconfig 后,重新编译项目:
idf.py clean idf.py build编译产物里会生成:
build/bootloader/bootloader.binbuild/partition_table/partition-table.binbuild/xxx.binbuild/secure_boot_signing_key.pem
这个.pem私钥文件是整个安全引导的核心资产,必须离线备份到安全位置。生成后建议复制一份到专门的密码管理器或离线加密磁盘里,然后从工作目录中移走。私钥一旦丢失,后续所有固件都无法通过签名验证,而这颗芯片也基本等同于报废。
3.3 开启 Flash Encryption:注意烧录顺序
继续在menuconfig中配置:
- 在Security features菜单里打开Enable flash encryption。
- 选择加密模式。开发板上请选Development;量产时再选Release。
- 是否允许在启动时重新烧录:Development 模式默认允许。
- 如果有 NVS 或分区表加密需求,记得在
Partition Table->Partition Table配置中把相应分区的encrypted位打开。
接下来是烧录。这一步是最容易犯错的:
idf.py flash第一次烧录时,ESP-IDF 会自动生成 flash 加密密钥并写入 eFuse,同时对 Flash 中的内容进行加密。执行完idf.py flash后,芯片会自动重启。
重启后,Flash 里的内容已经变成密文。为了验证确实生效,你可以用下面的命令尝试读出 Flash:
esptool.py read_flash 0x0 0x10000 flash_raw.bin然后打开flash_raw.bin,正常情况下看到的应该是无法理解的随机字节,而不是可读的字符串。我建议把0x0偏移处的 bootloader 内容也用十六进制编辑器看一眼,基本都是乱码就说明加密成功。
3.4 固件签名与烧录验证:开发模式下的必要步骤
在 Secure Boot v2 + Flash Encryption 都开启后,固件的烧录流程跟普通模式不太一样。直接用idf.py flash也能工作,但为了更好地适配生产流程,建议用脚本化方式签名再烧录:
esptool.py --chip esp32 elf2image --flash_mode dio --flash_freq 80m --flash_size 4MB \ build/xxx.elf esptool.py --chip esp32 image_info build/xxx.bin固件编译结束后,对app镜像进行签名:
espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem \ --output build/xxx-signed.bin build/xxx.bin然后把签名后的固件烧录到factory或ota_0分区:
esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash 0x10000 build/xxx-signed.bin注意:在 Development 模式下,这个流程没问题;但在 Release 模式下,串口写入通道会被禁用(DISABLE_DL_ENCRYPT、DISABLE_DL_DECRYPT被烧死),此时只能走 UART 的绕过方式或者 OTA 升级。这也是为什么我之前强调开发阶段用 Development 模式。
3.5 三种烧录场景对照:开发板、量产线与 OTA 升级
为了让你更直观地选择正确的路径,我把常用的三种场景的配置和操作列了个表:
| 场景 | Secure Boot 模式 | Flash 加密模式 | 烧录方式 | 密钥处理 |
|---|---|---|---|---|
| 开发板调试 | v2,Development | Development | 串口直接烧录 | 每次构建生成新签名密钥,可重新烧录 |
| 小批量产(100台内) | v2,Development | Development | 串口批量烧录加 OTA 部署 | 使用同一个签过名的固件包,通过 OTA 升级 |
| 正式量产 | v2,Release | Release | 产线专用烧录器 + 一次性 eFuse 流程 | 产线使用统一的烧录脚本,烧完后即锁死 |
在正式量产时,产线建议这么做:
- 第一遍烧录明文固件:先把未加密、未启用的固件通过串口写入 Flash。
- 第二遍启用安全功能:复用同一个烧录脚本,执行一次
idf.py flash让 eFuse 写入密钥,同时将 Flash 内容加密。 - 校验:产线测试程序通过 BLE/WiFi 连接设备,确认启动正常、功能正常,再打上“已安全烧录”标签。
- 封箱。
这个流程的优势在于:即使固件在产线的中转环节被复制,攻击者得到的也只是密文,没有目标设备的 eFuse 密钥,无法在其他芯片上还原运行环境。
4. 常见问题与排查技巧实录
4.1 烧录后串口打印乱码或循环重启
现象:开启 Flash 加密后,第一次重启,串口输出乱码或者反复复位。
原因:最常见的是eFuse 密钥写入成功,但 Flash 中的内容还没完成加密。或者你烧录时使用的是构建目录里的明文固件,直接覆盖了加密过的区域。
排查步骤:
- 用
esptool.py read_flash读出0x0内容,检查 bootloader 区域是否为密文。 - 检查
idf.py flash的日志中是否包含Flash crypto check: Flash encryption enabled。 - 如果确认是明文覆盖,最简单的处理是:在 Development 模式下重新执行
idf.py erase-flash,再idf.py flash。
注意:在 Release 模式下,这个操作行不通。所以量产前一定要在开发板上把流程完全跑通,不要指望产线去解决技术问题。
4.2 使用 Secure Boot 后 OTA 升级 401 错误
现象:OTA 升级时,日志出现ERROR: App image invalid或Signature verification failed。
原因:OTA 固件包没有用当前secure_boot_signing_key.pem签名,或者使用了旧的签名密钥。
排查步骤:
- 检查 OTA 服务器上的固件包构建时间,确认构建时是否启用了 Secure Boot。
- 用
espsecure.py verify_signature --version 2 --keyfile secure_boot_signing_key.pem验证本地构建的固件。 - 如果验证失败,重新构建并在签名步骤里明确指定正确的私钥。
4.3 Flash 读取是密文,但芯片无法启动
现象:烧录成功,Flash 内容也是密文,但设备上电后无启动日志。
原因:很可能是Flash 加密密钥和烧录时的密钥不匹配。产线烧录时,如果每台设备都重新随机生成了密钥,但烧录脚本又试图复用同一个固件包,就会出现“这个固件的密文是用另一种密钥加密的”情况。
解决:量产时禁止每台设备随机生成 flash 加密密钥。正确的做法是:首台设备生成的 flash 加密密钥(flash_encrypt_key.bin)后续所有设备共用一份。把这份密钥安全地交给产线,在烧录脚本里通过--flash_encrypt_key参数指定。不然每台设备密钥不同,固件就得单独加密,产线根本跑不动。
4.4 为什么 SPI Flash 频率会影响加密模式
另一个容易被忽略的点:ESP32 在开启 Flash 加密后,SPI Flash 的通信频率会受到限制。如果你的 Flash 型号支持 80MHz,但开启加密后只能稳定跑 40MHz,不要惊讶。这是因为加密引擎对 SPI 时序有额外要求,过高的频率在某些 Flash 型号上会导致偶发的读写错误。遇到偶尔启动失败或 Flash 数据损坏,先把 Flash 频率降到 40MHz 试试。
5. 量产与升级策略:密钥怎么管、固件怎么更
5.1 开发、量产两套密钥体系
从设计之初就应该准备两套 Secure Boot 签名密钥:
- 开发密钥:用于开发阶段,所有开发者共享。在
.pem文件生成后统一分发到团队内部,用密码管理器保存。 - 量产密钥:仅存在于产线或 CI 系统的安全环境中,开发者环境一律不存放。
Flash Encryption 的密钥则建议采用首台样机生成 + 全产线共用的模式。简单来说,产线烧录的第一台设备上,由烧录脚本自动生成flash_encrypt_key.bin。后续设备在写入 eFuse 时使用同一个密钥文件,避免密钥漂移。
5.2 OTA 升级与回滚机制的配合
Secure Boot v2 开启后,OTA 固件的签名验证是强制性的。ESP-IDF 的 OTA 机制支持版本回滚,但需要额外配置:
idf.py menuconfig在Security features → Enable flash encryption下,打开Enable anti-rollback。这时固件版本号(CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK)会被作为版本门槛。一旦 OTA 升级到 v2,旧版本固件(v1)将无法被刷入或回滚,有效防止降级攻击。
这个机制对生产环境非常有用,但它要求你的固件版本管理必须严格,不要随意改递减版本号,否则设备会拒绝更新。
5.3 生产数据的加密:不只是保护固件
安全启动和 Flash 加密不是只保护程序代码。设备运行过程中,很多敏感数据会写入 NVS 分区:
- WiFi 配置
- 云平台设备证书
- 用户私有配置
- 设备影子数据
这些数据如果不加密,攻击者可以从 Flash 读出明文 NVS 内容。在启用 Flash 加密时,记得在分区表中将 NVS 分区标记为encrypted:
# CSV 格式分区表示例 nvs, data, nvs, 0x9000, 0x6000, encrypted factory, app, factory, 0xF000, 0x2000006. 经验小结:这套机制真实能防住什么,防不住什么
最后聊点实在的。很多刚接触安全功能的开发者,以为开了 Secure Boot 和 Flash 加密就高枕无忧了。实际上它防的是物理攻击和离线篡改,但防不住这些场景:
- 运行时的内存侧信道攻击:如果攻击者能通过漏洞让 CPU 执行任意代码(比如读取解密后的数据),Flash 加密是防不住的,因为解密发生在芯片内部。
- 串口命令行注入:开启 Security 功能后,串口下载模式会被限制,但 UART 控制台(如果有开启)仍然是潜在攻击面。量产固件建议关闭
console组件。 - 密钥泄露:如果私钥自己泄露,这套体系就形同虚设。
所以我的建议是:安全功能是防护体系的一部分,不是全部。把Secure Boot + Flash Encryption + 固件签名 OTA + 安全日志 + 设备证书组合使用,才能构建立体的设备安全。对于个人开发者,至少做到前两步,能挡住绝大多数伸手党式攻击,就够用了。
我自己的习惯是,凡是发出去的开发板、送测的样机、给客户演示的设备,全部开启 Development 模式的安全引导与加密。这套流程一开始会多一些步骤,但摸熟之后,整个烧录时间只多增加十几秒。比起后面固件被抄、服务器被薅的麻烦,这十几秒非常值。