ESP-IDF 中 Flash 加密的完整实践:eFuse 烧录、Kconfig 配置与加密读写测试
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
本文基于 ESP-IDF 仓库中的 Flash 加密测试应用(components/spi_flash/test_apps/flash_encryption)展开,讲解 Flash 加密功能的启用前提、eFuse 密钥烧录流程、关键 Kconfig 配置项,以及esp_flash_write_encrypted/esp_flash_read_encrypted的 16 字节对齐约束等实现细节。读完之后,你可以独立完成一台开发板的 Flash 加密使能(含 eFuse 烧录),并复现仓库中完整的加密读写验证用例。
支持的目标芯片
Flash 加密测试应用覆盖 ESP-IDF 中所有具备硬件 Flash 加密能力的 Xtensa/RISC-V 目标芯片:
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
|---|
Flash 加密由 SoC 的加密引擎完成,加密密钥存放在不可擦除的 eFuse 中,因此该功能是"一次性"的——这正是本文后续"Prepare runner"步骤需要格外谨慎的原因。
测试应用的组成
测试项目位于 flash_encryption,是一个裁剪后的最小工程,其 CMakeLists.txt 通过set(COMPONENTS main esp_psram esptool_py)只引入main、esp_psram和esptool_py三个组件,项目名为test_flash_encryption,并额外挂载了tools/test_apps/components和components/spi_flash/test_apps/components两个公共测试组件目录(提供test_utils、ccomp_timer等工具)。
partitions.csv 定义了测试分区布局,其中flash_test(data/fat 类型、528K)是加密读写测试的数据区,起始地址由测试代码运行时通过esp_partition_find_first动态获取:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, , 0x6000, factory, 0, 0, , 1M flash_test, data, fat, , 528K测试入口 test_app_main.c 用 Unity 框架跑用例,并在每个用例前后记录 8BIT/32BIT 堆的剩余量做内存泄漏检查——代码注释解释了阈值的来源:Flash 加密路径中存在一些延迟分配(lazy allocated)的资源,因此阈值TEST_MEMORY_LEAK_THRESHOLD放宽为 -400 字节。
配置:sdkconfig 关键项
sdkconfig.defaults 给出了使能 Flash 加密的完整配置组合,是理解该功能最直接的样本:
| 配置项 | 作用 |
|---|---|
CONFIG_SECURE_FLASH_ENC_ENABLED=y | 使能 Flash 加密总开关 |
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y | 开发模式。密钥来自 eFuse 中烧录的 BLOCK_KEY2,适合调试 |
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y | 要求芯片上 eFuse 已实际烧录加密位,否则启动时直接报错退出(防止在"未加密但以为已加密"的板子上静默运行) |
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y/..._ALLOW_DEC=y/..._ALLOW_CACHE=y | 允许 UART 下载(esptool)在加密状态下写入/读出密文,并允许在 eFuse 未烧录加密位时用 UART bootloader 模拟加密/解密流程 |
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y/CONFIG_SECURE_BOOT_ALLOW_JTAG=y | 放开对 ROM basic 启动和 JTAG 的限制 |
CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS=y | 默认禁止擦除/写入 bootloader、分区表等危险区域(见后文测试用例) |
从 test_flash_encryption.c 头部的注释表还能看到官方对各芯片配置组合的归纳:ESP32 只需CONFIG_SECURE_FLASH_ENC_ENABLED;而 ESP32-S2/C3 还需要EFUSE_VIRTUAL不启用、并设置CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED。整个测试文件被#ifdef CONFIG_SECURE_FLASH_ENC_ENABLED包裹,即只有开启加密配置时这些用例才会编译。
Prepare runner:烧录 eFuse 使能加密
README 中给出的准备步骤是:运行 encrypt_flash.sh(注意该操作会烧写 eFuse,不可逆)。脚本内容非常短,值得逐行理解:
#!/bin/bash set -e if [ -z "$ESPPORT" ]; then echo "ESPPORT must be set" exit 1 fi dd if=/dev/zero of=key.bin bs=1 count=32 # Change the first byte as espsecure uses modules that won't # allow symmetric keys echo -ne \\xFF | dd conv=notrunc bs=1 count=1 of=key.bin espefuse --do-not-confirm -p $ESPPORT burn-efuse SPI_BOOT_CRYPT_CNT 0x1 espefuse --do-not-confirm -p $ESPPORT burn-key BLOCK_KEY2 key.bin XTS_AES_128_KEY其执行逻辑为:
- 要求设置
ESPPORT环境变量(串口设备路径),否则退出; - 生成 32 字节密钥文件
key.bin:先用dd生成全零,再把首字节改为0xFF——注释说明了原因:espsecure使用的密钥模块不允许纯对称密钥的"零键"之类弱键,首字节置0xFF是为了绕开该限制(此处为开发用随机弱键,量产应使用真正的随机密钥); - 烧录
SPI_BOOT_CRYPT_CNT=0x1:置位该 eFuse 字段后,芯片启动即要求 Flash 加密,硬件进入"必须解密读 Flash"的状态; - 烧录
BLOCK_KEY2:以XTS_AES_128_KEY类型把 32 字节密钥写入 BLOCK_KEY2,这就是 Flash 加密实际使用的 XTS-AES-128 块密钥。
从源码结构看,这与 Kconfig 中的两条路径是配套的:
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT(开发模式):密钥即上述手动烧录到 BLOCK_KEY2 的值,密钥"可见"于开发者,便于调试;- 量产模式则使用
espsecure从 Flash 加密密钥(FEK)派生块密钥,且要求安全启动公钥已烧录——测试应用走的是开发模式,因此只需espefuse手动烧录。
由于CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y,在 eFuse 尚未烧录的裸板上,应用启动时就会检测到"配置要求加密但硬件未加密"而拒绝运行,这正好倒逼开发者先完成本节的 Prepare runner 步骤。
加密读写 API 的行为约束
test_flash_encryption.c 是一组[flash_encryption]标签的 Unity 用例,集中体现了 Flash 加密 API 的语义,可以当作"API 行为规格书"阅读:
16 字节对齐是硬性约束
XTS 加密以 16 字节块为单位,加密写要求偏移和长度都对齐 16 字节。用例 "test 16 byte encrypted writes" 显式验证了两类非法调用:
/* 偏移未对齐 16 字节 → ESP_ERR_INVALID_ARG */ esp_flash_write_encrypted(NULL, start + 1, fortyeight_bytes, 32); /* 长度非 16 字节倍数 → ESP_ERR_INVALID_SIZE */ esp_flash_write_encrypted(NULL, start, fortyeight_bytes, 15);而读取接口esp_flash_read_encrypted允许非对齐的起始/长度(用例中在start+0x10读 16 字节做交叉验证)。
部分块写入不会"溢出"加密
"test 16 byte encrypted writes" 还验证了部分块(partial block)的边界行为:向start+0x30写 16 字节后,其前后相邻的 16 字节区域仍是擦除态0xFF(verify_erased_flash逐字节断言);同理,48 字节写入(长度不是 32 的整数倍)只影响其覆盖的 16 字节块。这说明实现对不足一个块的尾部不会越界写入相邻数据。
随机数据的整扇区读写回环
"test read & write random encrypted data" 在一个 4K 扇区内循环执行:以随机 16 字节步进、随机 16~192 字节长度写入rand()数据,最后再随机分块整扇区读回并逐字节比对,覆盖了"非对齐缓冲区 + 非对齐读"的组合场景。
大缓冲区与性能
另有两个用例使用 16432 字节(n*32+16形态)的large_const_buffer:一个放在 Flash 映射区(const),一个用DRAM_ATTR固定到 RAM,用于验证两种源地址下的加密写入,并通过ccomp_timer打印us/KB的写入速度(IDF_LOG_PERFORMANCE)。对 Flash 物理大小大于 16MB 的芯片,用例还会追加一轮对0x1030000偏移的测试,覆盖大容量 Flash 的加密区。
危险区域保护与越界检查
在CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS=y下,用例验证加密写/擦除到 bootloader(CONFIG_BOOTLOADER_OFFSET_IN_FLASH)和分区表(CONFIG_PARTITION_TABLE_OFFSET)区域都会被拒绝,返回ESP_ERR_INVALID_ARG;在CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y下则切换为另一组"越界边界"用例,验证擦除/写入超过 Flash 末尾(含UINT32_MAX溢出构造)一律报参数错误——CI 的rom_impl与verify配置特意打开了DANGEROUS_WRITE_ALLOWED(配置文件中注明"Unrelated to rom/verify, but to test if the boundary checking works well"),说明边界检查代码路径在 ROM 实现和验证模式下同样受检。
CI 配置矩阵与运行方式
pytest_flash_encrypted.py 定义了 CI 参数矩阵,配合 4 个 CI 配置文件运行:
| pytest 函数 | 目标芯片 | 配置(config) | 对应文件 |
|---|---|---|---|
test_flash_encryption | esp32 / esp32c3 | release、verify | sdkconfig.ci.release、sdkconfig.ci.verify |
test_flash_encryption_rom_impl | esp32c3 | rom_impl | sdkconfig.ci.rom_impl |
test_flash_encryption_f4r8 | esp32s3 | release_f4r8、rom_impl、verify | sdkconfig.ci.release_f4r8 |
test_flash_encryption_f8r8 | esp32s3 | release_f8r8 | sdkconfig.ci.release_f8r8 |
各配置文件的差异点:
release/verify/f4r8/f8r8:在 defaults 基础上启用FREERTOS_USE_TICKLESS_IDLE、COMPILER_OPTIMIZATION_SIZE,验证 tickless 空闲与体积优化编译下加密路径仍正常;f4r8额外打开CONFIG_SPIRAM_MODE_OCT+SPIRAM_TYPE_AUTO(Octal 80/80 PSRAM),f8r8进一步要求CONFIG_ESPTOOLPY_OCT_FLASH、FLASHMODE_OPI、32MB Flash,用于覆盖 S3 的 80/80/80 高速配置;verify额外开启CONFIG_SPI_FLASH_VERIFY_WRITE=y、CONFIG_SPI_FLASH_LOG_FAILED_WRITE=y、CONFIG_SPI_FLASH_WARN_SETTING_ZERO_TO_ONE=y,把 Flash 驱动自带的写后回读校验(verify write)与加密写叠加;rom_impl只设置CONFIG_SPI_FLASH_ROM_IMPL=y(Flash 操作使用 ROM 中的实现)与SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y,并关闭自定义分区表——验证 ROM 实现路径下的边界检查。
所有用例均为dut.run_all_single_board_cases(),即按标准 pytest-embedded-idf 流程单板运行;由于测试依赖 eFuse 烧录,本地执行前必须先完成上一节的ESPPORT=... ./encrypt_flash.sh准备步骤。
小结与注意事项
- Flash 加密测试应用是"配置 + eFuse 烧录 + 用例"三件套:sdkconfig.defaults 给出 Kconfig 组合,encrypt_flash.sh 完成 BLOCK_KEY2 与 SPI_BOOT_CRYPT_CNT 的不可逆烧录,test_flash_encryption.c 验证 API 语义;
- eFuse 烧录不可撤销,且
SPI_BOOT_CRYPT_CNT一旦置位,Flash 中已有的明文数据(含 bootloader 之外的旧分区)将无法被正确解读,务必在全新板子或允许报废的板子上操作; - 加密写的 16 字节对齐要求(
ESP_ERR_INVALID_ARG/ESP_ERR_INVALID_SIZE)是硬件块粒度决定的,应用层封装加密数据区时应自行保证对齐; - 若需理解各芯片配置差异(如 S2/C3 需要
REQUIRE_ALREADY_ENABLED),参考 test_flash_encryption.c 头部注释。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考