4、硬件基础:eFuse与OTP原理、如何利用eFuse实现版本锁定
讲防回滚,绕不开两个硬件基础:eFuse 和 OTP。说白了,它们就是芯片上的一次性可编程存储单元。你想想看,如果攻击者能随便改写固件版本号,那防回滚就是个笑话。所以,我们需要一种「烧进去就改不了」的机制。
我个人习惯把 eFuse 和 OTP 比作芯片上的「熔断式保险丝」。烧断一根,就再也接不回去了。这个特性,恰好可以用来锁定版本号。
4.1 eFuse 的工作原理
eFuse 这个词,其实是「电子保险丝」的缩写。它最早是 IBM 搞出来的技术。原理很简单:
- 芯片内部有一小段金属连线(通常是铜或硅化物的导线)
- 正常状态下,这段导线是导通的,代表逻辑「1」
- 通过施加一个较大的电流(比如 10-30mA),导线会熔断
- 熔断后,该位变成开路状态,代表逻辑「0」
嗯,这里要注意:熔断是不可逆的。你不可能把断掉的导线再接回去。这就是 eFuse 实现「一次编程」的物理基础。
关键点: eFuse 的编程是物理层面的破坏。不是像 Flash 那样通过电子隧穿擦写。所以,任何试图恢复 eFuse 状态的操作,都需要在显微镜下做芯片级逆向工程——成本极高,成功率极低。
我在项目中遇到过一件事:有个客户想用 eFuse 存储产品序列号,结果发现编程电流没控制好,把相邻的位也熔断了。后来我们加了一个电流限制电路,才解决这个问题。所以,eFuse 编程的时序和电流参数,一定要严格按照芯片手册来。
4.2 OTP 存储器的原理
OTP 是 One-Time Programmable 的缩写。它和 eFuse 本质上是同一类东西,但实现方式更多样:
| 类型 | 编程方式 | 典型容量 | 常见用途 |
|---|---|---|---|
| eFuse | 电流熔断金属线 | 1-64 Kb | 密钥存储、芯片ID、版本锁定 |
| 反熔丝 OTP | 高压击穿栅氧化层 | 1-256 Kb | 配置存储、校准参数 |
| 一次性 EPROM | 紫外/电编程 | 1-512 Kb | 早期 MCU 代码存储 |
| 一次性 Flash | 特殊工艺禁止擦除 | 1-2 Mb | 低成本代码存储 |
目前嵌入式设备里,最常用的是 eFuse 和反熔丝 OTP。它们都集成在 SoC 内部,不需要外部存储芯片。
我的经验: 选型时别只看容量。eFuse 的编程电压通常需要外部提供(比如 1.8V/3.3V 之外的专用编程引脚),而反熔丝 OTP 往往可以用芯片内部电荷泵升压。如果 PCB 空间紧张,后者更方便。
4.3 如何用 eFuse 实现版本锁定
好了,原理讲完了。现在说说实战——怎么用 eFuse 锁住固件版本号。
基本思路是这样的:
- 在芯片的 eFuse 区域,划出若干位作为「版本号寄存器」
- 每次固件升级时,Bootloader 检查新版本号是否大于当前版本号
- 如果合法,Bootloader 将新版本号写入 eFuse
- 一旦写入,该版本号就永久生效,无法回退
举个例子。假设我们用 8 位 eFuse 来表示版本号:
// 伪代码:eFuse 版本锁定流程
#define EFUSE_VERSION_ADDR 0x1A000000
#define CURRENT_VERSION (*((volatile uint8_t*)EFUSE_VERSION_ADDR))
int check_and_update_firmware(uint8_t new_version) {
// 1. 读取当前 eFuse 中的版本号
uint8_t current_ver = CURRENT_VERSION;
// 2. 检查新版本是否合法
if (new_version <= current_ver) {
return ERROR_ROLLBACK_DETECTED; // 回滚攻击!
}
// 3. 验证新固件的签名(省略具体实现)
if (!verify_firmware_signature()) {
return ERROR_SIGNATURE_INVALID;
}
// 4. 写入新版本号到 eFuse
write_efuse_byte(EFUSE_VERSION_ADDR, new_version);
// 5. 写入后立即回读验证
if (CURRENT_VERSION != new_version) {
return ERROR_EFUSE_WRITE_FAILED;
}
return SUCCESS;
}
注意: eFuse 编程通常是一次性写入多个位。你不能只改其中一位而不影响其他位。所以,版本号字段要单独分配一个 eFuse 区域,不要和其他配置混在一起。我曾经见过一个产品,把版本号和密钥放在同一个 eFuse 字里,结果升级时把密钥也冲掉了——那叫一个惨。
4.4 版本锁定的安全边界
用 eFuse 锁版本,听起来很完美。但实际部署时,有几个坑要避开:
- 版本号位数要留够:8 位最多支持 256 个版本。如果产品生命周期长,建议用 16 位或 32 位。
- 不要用 0x00 或 0xFF 作为初始值:未编程的 eFuse 位可能是全 0 或全 1(取决于工艺)。如果初始版本号恰好是 0x00,那第一个版本就写不进去了。
- 考虑 eFuse 的可靠性:eFuse 编程有成功率问题。写入后一定要回读验证,必要时可以写冗余位。
我建议: 在量产阶段,把 eFuse 版本号的初始值设为 0x01。这样,第一个正式固件版本从 0x02 开始。留出 0x01 作为工厂测试版本,方便产线调试。
4.5 eFuse 与版本锁定的整体流程
下面这张图,把整个流程串起来了。从芯片出厂到固件升级,每一步都离不开 eFuse 的参与。
这张图展示了从芯片出厂到固件升级的完整链路。你注意看那个菱形判断框——这是防回滚的核心决策点。只有新版本号大于当前 eFuse 中存储的版本号,升级才能继续。
4.6 实际部署中的注意事项
最后,分享几个我在实际项目中踩过的坑:
- eFuse 编程次数有限:虽然叫一次性编程,但有些芯片支持按位编程(比如一次写 1 位,分多次写完)。不过,频繁编程会降低 eFuse 的可靠性。我建议每个版本只写一次。
- 考虑 eFuse 的寿命:eFuse 熔断后,金属残留物可能会在高温下迁移,导致短路。虽然概率很低,但在高可靠性产品(比如汽车电子)中,建议使用冗余设计。
- 不要依赖单一防护:eFuse 版本锁定只是防回滚的一环。还要配合签名验证、安全启动、JTAG 锁定等措施。单一防护点被攻破,整个防线就垮了。
警告: 有些芯片的 eFuse 区域可以通过特殊调试接口(比如 JTAG、SWD)读取或改写。量产时一定要禁用这些接口。否则,攻击者可以绕过 Bootloader,直接修改 eFuse 内容。我见过一个案例,就是因为 JTAG 没锁,导致整个防回滚机制形同虚设。
好了,eFuse 和 OTP 的原理、以及如何用它们实现版本锁定,就讲到这里。记住一句话:硬件锁死,软件验证,两者缺一不可。