☰
ESP32固件安全实战:Secure Boot v2与Flash Encryption避坑指南
2026/10/14 1:12:13 网站建设 项目流程

机密固件泄露的坑,我替你先踩了一遍。玩 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 的安全引导流程是整个引导链逐级验证:

  1. 芯片上电复位。BootROM(芯片出厂固化的引导程序)开始执行。
  2. BootROM 读取 eFuse 中的SECURE_BOOT_EN标志位。如果该位被烧写为 1,BootROM 进入 Secure Boot 验证模式。
  3. BootROM 从 Flash 地址0x0(Bootloader 分区)读取引导程序的签名块,使用内置的 RSA 公钥哈希(这个哈希存放在 eFuseSECURE_BOOT_KEY区域,或者是在开发模式下的secure_boot_v2_public_keydigest)验证 bootloader 签名。
  4. 验证通过后,BootROM 将控制权交给 bootloader。Bootloader 继续验证partition table(分区表)和app(应用程序)分区的 RSA 签名。
  5. 每一级在跳转前,都会验证下一级镜像的签名以及整个镜像的 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菜单:

  1. 打开Enable hardware secure boot。
  2. 选择Secure Boot V2。
  3. 在Secure boot version处确认是2。
  4. 选择签名私钥生成方式:建议首次配置时选择Generate signing keys automatically,ESP-IDF 会在构建目录下生成secure_boot_signing_key.pem。

保存退出 menuconfig 后,重新编译项目:

idf.py clean idf.py build

编译产物里会生成:

  • build/bootloader/bootloader.bin
  • build/partition_table/partition-table.bin
  • build/xxx.bin
  • build/secure_boot_signing_key.pem

这个.pem私钥文件是整个安全引导的核心资产,必须离线备份到安全位置。生成后建议复制一份到专门的密码管理器或离线加密磁盘里,然后从工作目录中移走。私钥一旦丢失,后续所有固件都无法通过签名验证,而这颗芯片也基本等同于报废。

3.3 开启 Flash Encryption:注意烧录顺序

继续在menuconfig中配置:

  1. 在Security features菜单里打开Enable flash encryption。
  2. 选择加密模式。开发板上请选Development;量产时再选Release。
  3. 是否允许在启动时重新烧录:Development 模式默认允许。
  4. 如果有 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,DevelopmentDevelopment串口直接烧录每次构建生成新签名密钥,可重新烧录
小批量产(100台内)v2,DevelopmentDevelopment串口批量烧录加 OTA 部署使用同一个签过名的固件包,通过 OTA 升级
正式量产v2,ReleaseRelease产线专用烧录器 + 一次性 eFuse 流程产线使用统一的烧录脚本,烧完后即锁死

在正式量产时,产线建议这么做:

  1. 第一遍烧录明文固件:先把未加密、未启用的固件通过串口写入 Flash。
  2. 第二遍启用安全功能:复用同一个烧录脚本,执行一次idf.py flash让 eFuse 写入密钥,同时将 Flash 内容加密。
  3. 校验:产线测试程序通过 BLE/WiFi 连接设备,确认启动正常、功能正常,再打上“已安全烧录”标签。
  4. 封箱。

这个流程的优势在于:即使固件在产线的中转环节被复制,攻击者得到的也只是密文,没有目标设备的 eFuse 密钥,无法在其他芯片上还原运行环境。

4. 常见问题与排查技巧实录

4.1 烧录后串口打印乱码或循环重启

现象:开启 Flash 加密后,第一次重启,串口输出乱码或者反复复位。

原因:最常见的是eFuse 密钥写入成功,但 Flash 中的内容还没完成加密。或者你烧录时使用的是构建目录里的明文固件,直接覆盖了加密过的区域。

排查步骤:

  1. 用esptool.py read_flash读出0x0内容,检查 bootloader 区域是否为密文。
  2. 检查idf.py flash的日志中是否包含Flash crypto check: Flash encryption enabled。
  3. 如果确认是明文覆盖,最简单的处理是:在 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签名,或者使用了旧的签名密钥。

排查步骤:

  1. 检查 OTA 服务器上的固件包构建时间,确认构建时是否启用了 Secure Boot。
  2. 用espsecure.py verify_signature --version 2 --keyfile secure_boot_signing_key.pem验证本地构建的固件。
  3. 如果验证失败,重新构建并在签名步骤里明确指定正确的私钥。

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, 0x200000

6. 经验小结:这套机制真实能防住什么,防不住什么

最后聊点实在的。很多刚接触安全功能的开发者,以为开了 Secure Boot 和 Flash 加密就高枕无忧了。实际上它防的是物理攻击和离线篡改,但防不住这些场景:

  • 运行时的内存侧信道攻击:如果攻击者能通过漏洞让 CPU 执行任意代码(比如读取解密后的数据),Flash 加密是防不住的,因为解密发生在芯片内部。
  • 串口命令行注入:开启 Security 功能后,串口下载模式会被限制,但 UART 控制台(如果有开启)仍然是潜在攻击面。量产固件建议关闭console组件。
  • 密钥泄露:如果私钥自己泄露,这套体系就形同虚设。

所以我的建议是:安全功能是防护体系的一部分,不是全部。把Secure Boot + Flash Encryption + 固件签名 OTA + 安全日志 + 设备证书组合使用,才能构建立体的设备安全。对于个人开发者,至少做到前两步,能挡住绝大多数伸手党式攻击,就够用了。

我自己的习惯是,凡是发出去的开发板、送测的样机、给客户演示的设备,全部开启 Development 模式的安全引导与加密。这套流程一开始会多一些步骤,但摸熟之后,整个烧录时间只多增加十几秒。比起后面固件被抄、服务器被薅的麻烦,这十几秒非常值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询