开头先甩个真实场景。早几年我做一款联网设备,功能调通、测试通过、小批量出货,心里还挺美。结果没过多久,圈里朋友发我一个链接——有人把它的固件从Flash里读了出来,里面的服务器地址、通信协议、甚至是固件里的静态密钥全被扒得干干净净,还出了个“逆向分析报告”。那一刻我才明白,很多嵌入式产品的“裸奔”程度远超想象:攻击者不用多高深的技术,一把电烙铁、一个读Flash的夹子、几条命令就能把Firmware里的秘密全部带走。
作为ESP32快速入门系列的第十一篇,这篇我们不聊怎么点灯、怎么联网,而是解决一个更底层的问题:如何让写进Flash里的固件读不出来、也改不掉。对应的就是ESP32的两大安全特性——Secure Boot(安全启动)和Flash encryption(Flash加密)。这篇文章会从原理讲到实操,把密钥怎么生成、eFuse怎么烧、板子怎么验证、踩了哪些坑,一次性讲透。适合所有准备做产品、或者已经在做产品但还没考虑固件安全的开发者。
1. 不锁固件的代价:先认识真实威胁
1.1 固件裸奔到底有多危险
很多开发者对“固件被逆向”没概念,觉得代码被看了就看了,大不了重写。但嵌入式设备的固件远不止代码这么简单。我从实际经验出发,给你列一下固件泄露真正会带走的资产:
- 业务逻辑源码:不仅包含功能实现,还有整个产品的设计思路和判断规则。别人拿着你的固件反汇编,比对字符串和调用关系,基本能还原出80%的框架。
- 硬编码密钥和凭据:不少产品会把云服务API Key、设备鉴权密钥、TLS证书私钥直接写死在固件里。固件一泄露,这些密钥全部暴露,攻击者可以直接冒充设备接入你的云端。
- 通信协议与私有字段:你自己的协议设计、命令字、加密方式,全都能从固件里分析出来。轻则被别人做兼容设备,重则被利用来发恶意指令。
- 后门和漏洞点:通过diff不同版本的固件,攻击者能看到你修了什么漏洞,然后专门去攻击还没升级的设备。
Flash加密就是把Flash上存储的内容变成密文,从物理层面阻止别人直接读出明文固件。它防的是“读取”。
Secure Boot防的是“篡改”和“替换”。它保证芯片每次上电都只能启动经过签名的固件。攻击者就算把Flash里的内容改掉、或者塞进一个恶意固件,芯片也不会执行。
这两个功能是互补的:Flash encryption负责“让人读不到”,Secure Boot负责“让芯片只认你写的”。一个管保密性,一个管完整性,合起来才是完整的多层防护。只开其中一个,防护链其实都是断的——只加密不签名的场景里,攻击者虽然读不出内容,但可以盲改数据造成设备异常;只签名不加密,攻击者照样能提取分析你的固件逻辑。
1.3 这个功能到底该不该开
我的判断标准很简单,就三条:
- 准备量产的商业产品,尤其是带付费、会员、云端服务、私有协议、用户隐私数据的,必须开。
- 处于原型验证阶段、代码天天变、经常需要烧录调试的,可以先不开,但要在工程规划阶段把密钥管理和烧录流程设计好,免得正式做的时候手忙脚乱。
- 只是学习DIY、开源自用的,可以不开,但至少要知道这两个功能的存在。
这么说的原因很现实:安全功能是有“代价”的,主要代价就是开发和调试不再那么随意。一旦开了Secure Boot和Flash encryption,烧录流程会变复杂,OTA升级也要带签名,出问题后的恢复手段也变少。所以我的建议不是“所有项目都立即开启”,而是“所有商业化项目的安全策略都应该从第一天就考虑进去”。
2. 两个安全机制的底层原理
2.1 Secure Boot:一条从ROM到App的信任链
Secure Boot的核心思想是“链式信任”。ESP32的启动过程本来就有层级:芯片上电后先执行固化在ROM里的引导代码,ROM再加载二级Bootloader(ESP-IDF编译生成的bootloader),Bootloader再去校验和加载分区表,最后加载App固件。Secure Boot就是在每一层加载下一层之前,先做一次签名/哈希校验,校验不过就拒绝执行。
以ESP32经典的Secure Boot V1为例,签名算法是RSA-3072,校验链路是这样走的:
- ROM引导代码里固定存储了一段哈希摘要,这个摘要是开发者公钥的SHA-256值,烧录在芯片的eFuse中,任何人改不了。
- ROM加载二级Bootloader时,Bootloader头部包含完整的签名和公钥信息。ROM会用eFuse里存的摘要去验证Bootloader携带的公钥是否正确,再用这个公钥验证Bootloader的签名。
- Bootloader验证通过后接管控制,接着用同样的流程验证分区表和应用固件。分区表和App都附带了由开发者私钥生成的签名,Bootloader使用公钥逐一校验。
- 只要任意一环失败,启动流程直接中止,芯片不执行任何可疑代码。
这个设计最巧妙的地方在于:信任根不在Flash里,而在芯片硬件eFuse里。攻击者就算把整个Flash都换掉,也绕不过ROM里和eFuse里的验证过程。想要伪造一条完整的启动链,唯一的办法是拿到你的签名私钥,否则就是白忙活。
2.2 Flash encryption:一颗“透明加解密”的保险柜
Flash encryption的原理,可以理解成在芯片内部给Flash加了个“透明加密层”。ESP32内部有一个硬件AES加密单元,当你通过内存映射方式访问Flash时,硬件会自动解密数据返回给CPU;当CPU向Flash写入数据时,硬件又自动加密后再落到Flash上。CPU本身感知不到加解密过程,就像在操作普通Flash一样,但外面任何试图绕过CPU去读Flash的行为,拿到的都只是密文。
关键点在于密钥存放位置——Flash encryption的128位(经典ESP32为256位)AES密钥存在eFuse的密钥块中,CPU内部硬件可以直接使用,但没有任何软件路径能把它读出来。所以就算攻击者物理拆下Flash芯片,用编程器读出所有内容,也只是一堆无意义的乱码,因为没有密钥就解不开。
这里有一个很容易被初学者误解的地方:Flash encryption并不阻止别人“烧写”Flash。它只保证“没有密钥的读取是无效的”。所以当你启用了Flash encryption之后,如果再烧录新的固件,必须使用支持加密烧写的方式(比如带有相同密钥的esptool加密烧写在开发模式下),否则烧进去的内容就是明文,芯片启动后读出来就是乱码,照样跑不起来。
另外要注意,Flash encryption加密的是Flash中的所有用户数据和代码区域,但并非所有字节都会加密,比如Flash头的少部分区域会保留明文,这是为了让ROM引导代码在早期就能读取必要信息。这个边界一般在Bootloader配置里已经处理好了,正常使用不需要太关心。
2.3 eFuse:烧进去就改不掉的信任根
eFuse是ESP32芯片上一块特殊的存储单元,全称是“电子保险丝”。它的特点就是:一次性可编程,烧进去的位不能再变回0。它相当于芯片的“铁档案”,记录着芯片的各种配置和状态,Secure Boot的密钥摘要、Flash encryption的AES密钥,都存放在eFuse里。
要把Secure Boot和Flash encryption讲明白,必须理解eFuse的几个关键位置:
- 密钥存储块:用于存放Flash加密密钥、Secure Boot公钥摘要等敏感数据。
- 控制位:比如使能Secure Boot的位、Flash加密启用计数的位、禁止下载模式的位等。
- 软件开发模式与生产模式的差异,本质上也是通过烧录不同的控制位组合来实现的。
eFuse的不可逆性带来一个双重性质:一方面它是安全方案的基石,攻击者无法篡改信任根;另一方面它也意味着误操作的成本极高,烧错一个关键位,芯片可能就废了。所以后面实操部分我会反复强调:烧eFuse之前一定要看清楚提示,确认要写入的位和值。
2.4 开发模式与生产模式怎么选
ESP32在配置安全特性时,会区分Development(开发)模式和Release(生产)模式。这个选择直接决定了之后整个调试和量产流程的手感,先看表格:
| 对比项 | Development模式 | Release模式 |
|---|---|---|
| 重新烧录固件 | 支持,可使用加密烧录或回退操作 | 生产后基本只能加密烧录,普通烧录通道被收紧 |
| eFuse计数回退 | 允许一定次数内的回退(Flash加密计数递减) | 禁止回退,置位更多保护控制位 |
| 调试接口 | 保留更多调试能力,但仍有门槛 | 禁掉JTAG等功能,安全性最强 |
| 适用场景 | 开发调试阶段,固件还在迭代 | 产品定型后的量产和发布阶段 |
| 误操作代价 | 相对可控 | 烧错关键位基本没有后悔药 |
我实际使用下来的建议是:开发阶段用Development模式把流程跑通,功能、烧录、OTA都验证没问题之后,再切到Release模式做最终的确认。千万不要在开发阶段就直接上Release模式,否则一旦某个环节没调好,板子会非常难救,特别是在你还没完全搞清楚eFuse机制的时候,很容易把自己绕进死胡同。
3. 实操:在ESP32上开启安全启动与Flash加密
3.1 软硬件准备
实操前先确认环境,我用的是:
- 开发板:一块ESP32 DevKitC模组开发板(用的经典ESP32芯片)
- 软件:ESP-IDF v5.2版本,自带Python和esptool工具链
- 操作系统:Linux环境串口为/dev/ttyUSB0,Windows下则为COMx
建议准备至少两块板子。一块用来做实验,烧坏了不心疼;另一块留着对照实验,方便排查问题。另外强烈建议准备一个TTL转USB工具或带有完整串口的开发板,以备某些锁死状态下做应急恢复尝试。
3.2 密钥生成与menuconfig配置
第一步是生成Secure Boot签名密钥。在ESP-IDF环境下,直接用espsecure工具生成:
espsecure.py generate_signing_key --version 1 secure_boot_signing_key.pem这条命令会生成一个PEM格式的RSA私钥文件。这个文件就是整个安全体系里最值钱的东西,一定要妥善保存,最好放到离线设备或加密存储里,绝对不要提交进git仓库。为了安全,也可以把私钥复制一份存到安全的地方,同时给文件设置严格的权限位。
接着配置menuconfig:
idf.py menuconfig按路径找到Security features菜单,重点设置以下几项:
- 勾选Enable hardware secure boot。
- 选择Secure Boot version 1(经典ESP32默认常用)。
- 勾选Sign binaries during build,让编译时自动对Bootloader、分区表和App进行签名。
- 回到Security features菜单,勾选Enable flash encryption on boot。
- Flash encryption mode选择Development mode或Release mode,开发阶段建议先选Development。
- 如果你在开发阶段还希望保留一些灵活操作,可以临时勾选Potentially insecure options下的Allow potentially insecure options,但生产前一定要关掉。
配置完后正常编译:
idf.py build编译完成之后,在build目录下会有我们需要的产出物。安全的生效位置是在flash启动时由bootloader处理,但Secure Boot的eFuse烧录需要单独处理。这里我按官方推荐的流程走:先烧eFuse,再烧固件,让第一次启动自动完成Flash加密动作。
3.3 首次烧录:烧eFuse、写固件、首次启动自动加密
先进入build/secure_boot目录,找到Secure Boot公钥摘要文件,一般名为secure_boot_key_digest.bin。在首次烧录前,需要先把公钥摘要写入eFuse:
esptool.py --port /dev/ttyUSB0 write_efuse "BLOCK0" secure_boot_key_digest.bin这条命令会烧写Secure Boot公钥摘要到eFuse的密钥区。执行时会要求确认,一定要仔细看即将写入的摘要内容是否正确。这一烧,Secure Boot的信任根就固定在芯片上了。接着使能Secure Boot控制位:
esptool.py --port /dev/ttyUSB0 burn_efuse "SECURE_BOOT_EN"这一步完成后,再烧录固件:
idf.py -p /dev/ttyUSB0 flash monitor这里有一个很容易被忽视的过程:第一次上电启动时,Bootloader会检测到Flash encryption尚未生效,于是自动对整片Flash进行加密,然后写入相应的eFuse计数位。日志里会看到类似Flash encryption enabled的提示,代表Flash已经正式从明文状态转变成了密文状态。
注意,这个“首次启动自动加密”的动作是非常关键的转折点。在这之后,Flash上所有可执行代码和用户数据都已经是密文,再用普通方式直接读Flash只会得到乱码。如果你在第一次烧录前忘了先烧Secure Boot的eFuse摘要就跑一次flash,也是能跑的,但Secure Boot实际上没有生效——这就是为什么顺序那么重要。
3.4 验证:从日志与efuse summary看结果
固件正常跑起来后,验证工作分两步。第一步查eFuse状态:
esptool.py efuse summary看输出信息里的Secure Boot、Flash encryption相关的位是否已经置位。不同芯片的摘要关键词略有差异,但基本能看到Secure Boot对应的标志位为Enabled、Flash cryptographic count等数值已经非零。
第二步是读取Flash内容,亲自验证一下能不能看到明文:
esptool.py read_flash 0x0 0x400000 flash_dump.bin然后用十六进制工具打开dump文件。如果安全功能生效,你会发现自己写的字符串、代码里的常量、文件系统里的内容,全都变成了不可读的密文。这时候你才真正知道“隔离”是什么感觉。如果仍然能直接在dump里搜到明文ASCII字符串,那就说明Flash encryption实际上没有生效,需要回头检查配置和启动流程。
另外可以做一个更直观的篡改实验:写一个最简单的App,编译出bin文件,直接用普通写Flash的方式覆盖烧录进去。因为不是加密烧写,也没有合法签名,重启后Bootloader会拒绝启动这个被篡改的固件,日志会给出校验失败的报错。这个实验能同时验证Secure Boot的防篡改和Flash encryption的防明文烧写。
4. 实战中的坑与排查技巧
4.1 启动卡住,反复重启
这是开启安全功能后最常见的现象,表现是芯片一直循环复位,串口偶尔打印一些简短日志,看不太清楚具体原因。通常逃不出这几个原因:
- 公钥摘要没烧对。Secure Boot的eFuse摘要和Bootloader里携带的公钥不一致,导致ROM层验证不过,根本进不到Bootloader。
- Bootloader/App没有签名。最常见的场景是“编译时忘了勾选Sign binaries during build”,然后直接把产物烧进去,芯片校验签名失败就会拒绝启动。
- 烧录了旧固件。之前没开安全功能时编译的bin,和现在开了签名验证之后是不兼容的,重新build一次再烧。
排查时先把串口日志完整抓下来。在Secure Boot场景下,芯片通常会给出安全校验失败的明确提示。如果日志里一点信息都刷不出来,多半是卡在了更早的ROM阶段,优先检查摘要和eFuse。
4.2 板子“锁死”还有救吗
很多新手在烧eFuse时手一抖,或者配置选成了Release模式又烧了不可逆的位,然后发现板子再也不能通过常规方式烧录了,顿时觉得“变砖了”。实际上要对“锁死”分级别看待:
- 仅仅使能了Secure Boot但密钥正确:还有救,重新烧录带签名的固件就能继续。
- 使能了Secure Boot但私钥或摘要搞丢了:基本没救,但可以尝试用Secure Boot的key revoke机制——只要当初设了多个密钥槽位,并且还没全部吊销,还能换一个有效密钥槽签名新固件,否则只能换芯片。
- Flash encryption开了Release模式,并且下载模式被烧录位禁止:后期想要重新烧普通固件基本没戏,但如果手里有正确的密钥,并且芯片支持加密下载模式,理论上还有一线希望。不过实际量产场景里,这种板子的定位就是“作废一盘”。
- 把eFuse熔丝区域烧满或者烧错关键位置:没救,物理报废。
所以我的原则是:凡是涉及eFuse的烧录,动手前先确认变量名、值、板子型号,并且预留实验板。生产板不要拿第一手就烧不可逆位。宁可慢一分钟,也要把esptool提示的即将写入内容逐行看清楚。
4.3 密钥管理实操建议
在安全方案的整个生命周期里,最容易出的问题反而是“自己人把自己锁在门外”。密钥管理不只是保管一个私钥那么简单,我根据自己的实践给几点建议:
- 私钥至少做两份离线备份,放到不同物理位置的加密介质里,不要只存在开发电脑上。
- 密钥文件设置只读权限,日常构建通过CI在受控环境里使用,避免开发机被人顺手拷走。
- 如果项目里有多个开发者,建议构建签名统一由一个人或一个受控系统负责,开发者本地不需要所有人都持私钥。
- 密钥一旦怀疑泄露,假设它已经泄露,立即开始走密钥吊销和替换流程。注意Secure Boot V1的吊销是有限度的,所以更要提前规划。
- Flash encryption密钥建议使用“芯片生成随机密钥”的方式,这样即使所有Flash内容被拷贝到另一块芯片上,也无法直接解密。只有需要“批量镜像烧录”这种特殊诉求的产品,才适合考虑自定义固定密钥。
4.4 常见问题速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 串口无输出/无反应 | eFuse摘要错误、ROM阶段校验失败 | 检查KEY摘要是否烧录正确,确认BOOT引脚和供电,必要时换板对比 |
| 启动后反复重启 | Bootloader或App签名缺失/错误 | 确认编译时勾选签名,重新clean build并烧录 |
| 读取Flash出来还是明文 | Flash encryption没真正开启 | 查看首次启动日志,确认Flash encryption状态,检查Development模式下是否被临时禁用 |
| 普通烧录后固件起不来 | 未使用加密烧录或烧入了未签名镜像 | 使用支持加密和签名的烧录流程,生产前固化脚本 |
| Release模式后无法再调试 | 下载模式/JTAG被置位禁止 | 量产板一般不调试;确有调试需求只能留独立调试板 |
| OTA升级失败 | 新固件未签名或签名校验失败 | 使用持同一私钥的构建流程,把OTA包纳入签名体系 |
这表格看着简单,实际排查的时候最花时间的往往不是找问题,而是确认自己当初到底烧了什么eFuse位。建议每次烧eFuse前先执行一次esptool.py efuse summary把当前状态保存下来,作为操作日志。万一出了问题,翻日志比翻记忆靠谱得多。
最后再分享一段实操体会
我第一次完整启用这套安全方案时,最深的感受是:Secure Boot和Flash encryption在配置层面其实并不复杂,难的是整个流程里的每一个“确认”节点都关系到成败。后来我把这套方案固化成了内部量产流程的一部分:密钥离线生成、CI构建签名、eFuse烧录脚本化、每一步都有日志和校验。这样,就算换新人来操作,也不会因为不熟悉而烧废板子。
如果你准备在自己的产品里用这套方案,建议先拿一块板子完整跑一遍开发模式,再跑一遍Release模式,每一步都记录下输出和状态变化。等两条流程都摸清了,再上量产的烧录线和产线脚本。安全功能的价值,只有在产品真正面向攻击者时才会体现出来,但那时再补救已经来不及了——所以要提前锁好这扇门。