很多做嵌入式安全的开发者,第一次搞加密启动的时候,肯定都遇到过这种情况:烧完efuse密钥,把启动代码一烧,开机就卡在那个芯片厂商的logo,或者弹出个红色的‘Verification Failed’(验证失败),怎么都进不了系统,折腾半天找不到原因,其实大概率是密钥那一步没做好,要么是efuse烧错了内容,要么是密钥和启动代码不匹配。

一、加密启动卡flash验证的常见根因

1.1 什么是flash验证

简单说,flash验证就是芯片开机后的“门卫检查”:芯片内部固化的一段小代码(叫ROM代码),会先从efuse里拿出提前存好的“专属暗号”(密钥),然后去检查flash里存的启动代码是不是被篡改过——相当于门卫拿着你的身份证暗号,核对你带的人的信息对不对,不对就不让进,直接卡在验证这一步。

1.2 最容易踩的坑

新手遇到卡验证,90%是这几个原因:一是efuse里烧的密钥,和启动代码里的加密签名完全不搭;二是烧efuse的时候,选了错误的存储区域(比如烧到了普通flash,不是一次性的efuse);三是烧录过程中断电、误操作,把efuse熔丝烧坏了,导致密钥不完整。

二、密钥存储的正确姿势

2.1 为什么选efuse存密钥

很多人会问,为啥不把密钥存在flash里?其实flash就像你家里的可擦写笔记本,虽然能存东西,但容易被别人偷看、篡改,只要拿到调试器就能读走。而efuse是芯片里的“一次性保险箱”,烧进去之后就锁死了,既不能改也不能删,除非换芯片,是目前嵌入式系统里最安全的密钥存储方式,专门用来放加密启动的核心密钥。

2.2 烧录前的准备工作

首先要确认芯片的efuse区域规格:比如STM32的OTP(就是efuse)区域,一般要求密钥长度是128位(16字节)或256位(32字节),多了少了都不行;然后要用工具生成随机密钥,不能用默认的固定密钥,比如用openssl生成16字节的随机密钥,命令是:

# 用openssl生成16字节的随机密钥,转换成hex格式,方便烧录
openssl rand -hex 16 > otp_key.txt

这一步的注释:生成的密钥绝对不能泄露,是验证启动代码的核心,一定要存在安全的地方,烧的时候再用。

2.3 烧录前的必做检查

烧之前一定要先读efuse的原始内容,确认要烧的区域是空的——比如用ST-Link工具读STM32的OTP区域,如果读出来的全是0xFF,说明是空的,可以烧;如果有其他数值,说明已经被用了,烧进去会直接失败,甚至把旧内容搞坏,导致芯片锁死。

三、efuse熔丝烧录的核心注意事项

3.1 先锁还是先烧?

不同芯片的efuse规则不一样,大部分通用芯片(比如STM32、NXP的i.MX),是先烧密钥,再开efuse的写保护——因为烧完之后,绝对不能再改密钥,哪怕是自己也不行;如果反过来,先锁再烧,会因为权限不够烧不进去,新手这里很容易搞反,导致烧录失败。

3.2 烧录后的强制验证

这一步绝对不能省:烧完密钥之后,一定要把efuse里的内容读出来,和之前备份的原始密钥对比,确保每个字节都一模一样。比如用STM32的代码验证:

// STM32H7 读取OTP区域的密钥,用于烧录后验证
#include "stm32h7xx_hal.h"
// OTP区域的起始地址,STM32H7的固定地址是0x1FFF7000
#define OTP_KEY_ADDR ((uint8_t*)0x1FFF7000)
uint8_t burned_key[16]; // 存储烧录后的密钥,16字节对应128位

void verify_otp_key(void) {
    // 读取OTP区域的16字节内容
    memcpy(burned_key, OTP_KEY_ADDR, 16);
    // 这里可以加对比逻辑,和备份的密钥数组对比,如果不同就报错,比如:
    uint8_t original_key[16] = {0x12,0x34,0x56,0x78,0x9A,0xBC,0xDE,0xF0,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88};
    int match = 1;
    for(int i=0;i<16;i++){
        if(burned_key[i] != original_key[i]){
            match = 0;
            break;
        }
    }
    if(match){
        // 密钥验证通过,继续下一步
        printf("OTP key verify success!\r\n");
    } else {
        // 验证失败,不要继续烧启动代码
        printf("OTP key verify failed!\r\n");
        Error_Handler();
    }
}

这个代码的注释:烧完密钥后运行这个函数,就能快速确认密钥烧对了,避免后续卡验证。

3.3 常见烧录失败的解决方法

遇到烧录失败,先检查这几个点:一是电源稳定,烧录过程中断电是最大的杀手;二是烧录工具的驱动,ST-Link的驱动有没有装好,不然读不到efuse;三是芯片型号有没有选错,STM32H7的OTP地址和F4不一样,烧的时候要对应型号。

四、完整示例演示(技术栈:STM32CubeIDE + STM32H743IIT6)

4.1 准备工作

用STM32CubeMX配置芯片:开启Secure Boot(安全启动),选择OTP区域作为密钥存储,设置密钥长度为128位,生成STM32CubeIDE的工程。

4.2 烧录密钥

用ST-Link Utility烧录之前生成的密钥文件,命令行方式如下:

# ST-Link Utility 烧录OTP密钥的命令,地址是STM32H7的OTP起始地址0x1FFF7000
st-flash write otp_key.bin 0x1FFF7000

这里的注释:要把之前用openssl生成的txt密钥转换成bin文件,不然烧不了,转换命令是xxd -r -p otp_key.txt otp_key.bin

4.3 启动验证

烧完密钥后,用刚才的C代码验证密钥,确认通过后,烧录Flash里的启动代码,开机就不会卡在验证步骤了。

五、应用场景与技术考量

5.1 主要应用场景

这个方案主要用在需要防篡改的嵌入式设备:比如智能门锁(防止黑客修改门锁的启动代码,破解开锁权限)、工业控制器(防止篡改控制程序,引发生产事故)、汽车ECU(防止黑客远程修改汽车控制逻辑,导致安全问题),这些场景都必须用到加密启动,卡flash验证是最常见的调试问题。

5.2 技术优缺点

优点:efuse密钥不可篡改,安全级别高,flash验证能有效防止启动代码被篡改,成本低,大部分通用芯片都支持;缺点:efuse是一次性的,烧错就只能换芯片,调试时没有密钥就无法启动,对新手不友好,烧录过程要求高,不能出错。

5.3 核心注意事项

总结下来,不管用什么芯片,烧efuse密钥都要记住三个“必做”:一是烧前备份efuse原始内容,二是烧后必须验证密钥,三是烧录时保持电源稳定,不能断电。

5.4 最终总结

加密启动卡在flash验证,本质是密钥和签名不匹配,或者烧录错误;只要严格按照步骤来,就能避免这个问题,efuse是目前最可靠的密钥存储方式,虽然有一次性的缺点,但换来的是极高的安全性,是嵌入式安全启动的必备组件。