flash 加密

[English]

本文档旨在引导用户快速了解 ESP32-C3 的 flash 加密功能,通过应用程序代码示例向用户演示如何在开发及生产过程中测试及验证 flash 加密的相关操作。

概述

flash 加密功能用于加密与 ESP32-C3 搭载使用的片外 flash 中的内容。启用 flash 加密功能后,固件会以明文形式烧录,然后在首次启动时将数据进行加密。因此,物理读取 flash 将无法恢复大部分 flash 内容。

启用 flash 加密后,系统将默认加密下列类型的 flash 数据:

  • 固件引导加载程序

  • 分区表

  • 所有 “app” 类型的分区

其他类型的数据将视情况进行加密:

  • 任何在分区表中标有“加密”标志的分区。详情请见 加密分区标志

  • 如果启用了安全启动,则可以加密安全启动引导程序摘要(见下文)。

重要

对于生产用途,flash 加密仅应在“发布”模式下启用。

重要

启用 flash 加密将限制后续 ESP32-C3 更新。在使用 flash 加密功能前,请务必阅读本文档了解其影响。

相关 eFuses

flash 加密操作由 ESP32-C3 上的多个 eFuse 控制。以下是这些 eFuse 列表及其描述,下表中的各 eFuse 名称也在 espefuse.py 工具中使用,为了能在 eFuse API 中使用,请在名称前加上 ESP_EFUSE_,如:esp_efuse_read_field_bit(ESP_EFUSE_DISABLE_DL_ENCRYPT)。

flash 加密过程中使用的 eFuses

eFuse

描述

位深

BLOCK_KEYN

AES 密钥存储,N 在 0-5 之间。

256 位密钥块

KEY_PURPOSE_N

控制 eFuse 块 BLOCK_KEYN 的目的,其中 N 在 0 到 5 之间。对于 flash 加密,唯一的有效值是 4,代表 XTS_AES_128_KEY

4

DIS_DOWNLOAD_MANUAL_ENCRYPT

设置后,则在下载引导模式时禁用 flash 加密。

1

SPI_BOOT_CRYPT_CNT

设置 SPI 启动模式后,可启用加密和解密。如果在 eFuse 中设置 1 或 3 个比特位,则启用该功能,否则将禁用。

3

备注

  • 上表中列出的所有 eFuse 位都提供读/写访问控制。

  • 这些位的默认值是 0。

对上述 eFuse 位的读写访问由 WR_DISRD_DIS 寄存器中的相应字段控制。有关 ESP32-C3 eFuse 的详细信息,请参考 eFuse 管理器。要使用 espefuse.py 更改 eFuse 字段的保护位,请使用以下两个命令:read_protect_efuse 和 write_protect_efuse。例如 espefuse.py write_protect_efuse DISABLE_DL_ENCRYPT

flash 的加密过程

假设 eFuse 值处于默认状态,且固件的引导加载程序编译为支持 flash 加密,则 flash 加密的具体过程如下:

  1. 第一次开机复位时,flash 中的所有数据都是未加密的(明文)。ROM 引导加载程序加载固件引导加载程序。

  2. 固件的引导加载程序将读取 SPI_BOOT_CRYPT_CNT eFuse 值 (0b000)。因为该值为 0(偶数位),固件引导加载程序将配置并启用 flash 加密块。关于 flash 加密块的更多信息,请参考 ESP32-C3 技术参考手册

  3. 固件的引导加载程序使用 RNG(随机数生成)模块生成 256 位密钥,然后将其写入 BLOCK_KEYN eFuse。软件也为存储密钥的块更新了 KEY_PURPOSE_N。由于 BLOCK_KEYN eFuse 已设置编写和读取保护位,故无法通过软件访问密钥。KEY_PURPOSE_N 字段也受写保护。flash 加密操作完全在硬件中完成,无法通过软件访问密钥。

  4. flash 加密块将加密 flash 的内容(固件的引导加载程序、应用程序、以及标有“加密”标志的分区)。就地加密可能会耗些时间(对于大分区最多需要一分钟)。

  5. 固件引导加载程序将在 SPI_BOOT_CRYPT_CNT (0b001) 中设置第一个可用位来对已加密的 flash 内容进行标记。设置奇数位。

  6. 对于 开发模式,固件引导加载程序允许 UART 引导加载程序重新烧录加密后的二进制文件。同时,SPI_BOOT_CRYPT_CNT eFuse 位不受写入保护。此外,默认情况下,固件引导加载程序设置 DIS_DOWNLOAD_ICACHEDIS_PAD_JTAGDIS_USB_JTAGDIS_LEGACY_SPI_BOOT eFuse 位。

  7. 对于 发布模式,固件引导加载程序设置所有在开发模式下设置的 eFuse 位以及 DIS_DOWNLOAD_MANUAL_ENCRYPT。它还写保护 SPI_BOOT_CRYPT_CNT eFuse 位。要修改此行为,请参阅 启用 UART 引导加载程序加密/解密

  8. 重新启动设备以开始执行加密镜像。固件引导加载程序调用 flash 解密块来解密 flash 内容,然后将解密的内容加载到 IRAM 中。

在开发阶段常需编写不同的明文 flash 镜像并测试 flash 的加密过程。这要求固件下载模式能够根据需求不断加载新的明文镜像。但是,在制造和生产过程中,出于安全考虑,固件下载模式不应有权限访问 flash 内容。

因此需要有两种不同的 flash 加密配置:一种用于开发,另一种用于生产。详情请参考 flash 加密设置 小节。

flash 加密设置

提供以下 flash 加密模式:

  • 开发模式 - 建议仅在开发过程中使用。因为在这种模式下,仍然可以将新的明文固件烧录到设备,并且引导加载程序将使用存储在硬件中的密钥对该固件进行透明加密。此操作间接允许从 flash 中读出固件明文。

  • 发布模式 - 推荐用于制造和生产。因为在这种模式下,如果不知道加密密钥,则不可能将明文固件烧录到设备。

本节将详细介绍上述 flash 加密模式,并且逐步说明如何使用它们。

开发模式

在开发过程中,可使用 ESP32-C3 内部生成的密钥或外部主机生成的密钥进行 flash 加密。

使用 ESP32-C3 生成的密钥

开发模式允许用户使用固件下载模式下载多个明文镜像。

测试 flash 加密过程需完成以下步骤:

  1. 确保您的 ESP32-C3 设备有 相关 eFuses 中所示的 flash 加密 eFuse 的默认设置。

请参考如何检查 ESP32-C3 flash 加密状态

  1. 项目配置菜单,执行以下操作:

启用 flash 加密将增大引导加载程序,因而可能需更新分区表偏移量。请参考 引导加载程序大小

  1. 运行以下命令来构建和烧录完整的镜像。

idf.py flash monitor

备注

这个命令不包括任何应该写入 flash 分区的用户文件。请在运行此命令前手动写入这些文件,否则在写入前应单独对这些文件进行加密。

该命令将向 flash 写入未加密的镜像:固件引导加载程序、分区表和应用程序。烧录完成后,ESP32-C3 将复位。在下一次启动时,固件引导加载程序会加密:固件引导加载程序、应用程序分区和标记为“加密”的分区,然后复位。就地加密可能需要时间,对于大分区最多需要一分钟。之后,应用程序在运行时解密并执行命令。

下面是启用 flash 加密后 ESP32-C3 首次启动时的样例输出:

rst:0x1 (POWERON),boot:0xf (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x3fcd6260,len:0x1b8
load:0x3fcd6418,len:0x2538
load:0x403ce000,len:0x704
load:0x403d0000,len:0x34f0
entry 0x403ce03e
I (12) boot: ESP-IDF qa-test-v4.3-20201113-766-g870d 2nd stage bootloader
I (13) boot: compile time 12:10:57
I (13) boot: chip revision: 0
I (16) boot.esp32c3: SPI Speed      : 40MHz
I (17) boot.esp32c3: SPI Mode       : DIO
I (19) boot.esp32c3: SPI Flash Size : 2MB
I (22) boot: Enabling RNG early entropy source...
I (28) boot: Partition Table:
I (30) boot: ## Label            Usage          Type ST Offset   Length
I (33) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
I (37) boot:  1 storage          Unknown data     01 ff 00010000 00001000
I (41) boot:  2 factory          factory app      00 00 00020000 00100000
I (44) boot: End of partition table
I (46) esp_image: segment 0: paddr=0x00020020 vaddr=0x3c020020 size=0x05508 ( 21768) map
I (61) esp_image: segment 1: paddr=0x00025530 vaddr=0x3fc88780 size=0x014cc (  5324) load
I (64) esp_image: segment 2: paddr=0x00026a04 vaddr=0x40380000 size=0x08780 ( 34688) load
0x40380000: _vector_table at ??:?

I (81) esp_image: segment 3: paddr=0x0002f18c vaddr=0x00000000 size=0x00e8c (  3724)
I (84) esp_image: segment 4: paddr=0x00030020 vaddr=0x42000020 size=0x171a8 ( 94632) map
0x42000020: esp_ota_get_app_description at /home/marius/clean/esp-idf_2/components/app_update/esp_app_desc.c:63

I (132) boot: Loaded app from partition at offset 0x20000
I (133) boot: Checking flash encryption...
I (137) efuse: Batch mode of writing fields is enabled
I (140) flash_encrypt: Generating new flash encryption key...
I (144) efuse: Writing EFUSE_BLK_KEY0 with purpose 4
W (148) flash_encrypt: Not disabling UART bootloader encryption
I (152) flash_encrypt: Disable UART bootloader cache...
I (155) flash_encrypt: Disable JTAG...
I (161) efuse: Batch mode. Prepared fields are committed
I (162) esp_image: segment 0: paddr=0x00000020 vaddr=0x3fcd6260 size=0x001b8 (   440)
I (164) esp_image: segment 1: paddr=0x000001e0 vaddr=0x3fcd6418 size=0x02538 (  9528)
I (173) esp_image: segment 2: paddr=0x00002720 vaddr=0x403ce000 size=0x00704 (  1796)
I (174) esp_image: segment 3: paddr=0x00002e2c vaddr=0x403d0000 size=0x034f0 ( 13552)
I (571) flash_encrypt: bootloader encrypted successfully
I (627) flash_encrypt: partition table encrypted and loaded successfully
I (628) flash_encrypt: Encrypting partition 1 at offset 0x10000 (length 0x1000)...
I (685) flash_encrypt: Done encrypting
I (686) esp_image: segment 0: paddr=0x00020020 vaddr=0x3c020020 size=0x05508 ( 21768) map
I (696) esp_image: segment 1: paddr=0x00025530 vaddr=0x3fc88780 size=0x014cc (  5324)
I (699) esp_image: segment 2: paddr=0x00026a04 vaddr=0x40380000 size=0x08780 ( 34688)
0x40380000: _vector_table at ??:?

I (715) esp_image: segment 3: paddr=0x0002f18c vaddr=0x00000000 size=0x00e8c (  3724)
I (717) esp_image: segment 4: paddr=0x00030020 vaddr=0x42000020 size=0x171a8 ( 94632) map
0x42000020: esp_ota_get_app_description at /home/marius/clean/esp-idf_2/components/app_update/esp_app_desc.c:63

I (760) flash_encrypt: Encrypting partition 2 at offset 0x20000 (length 0x100000)...
I (14797) flash_encrypt: Done encrypting
I (14801) flash_encrypt: Flash encryption completed
I (14802) boot: Resetting with flash encryption enabled...

启用 flash 加密后,在下次启动时输出将显示已启用 flash 加密,样例输出如下:

rst:0x3 (RTC_SW_SYS_RST),boot:0xf (SPI_FAST_FLASH_BOOT)
Saved PC:0x403d0dde
SPIWP:0xee
mode:DIO, clock div:2
load:0x3fcd6260,len:0x1b8
load:0x3fcd6418,len:0x2538
load:0x403ce000,len:0x704
load:0x403d0000,len:0x34f0
entry 0x403ce03e
I (15) boot: ESP-IDF qa-test-v4.3-20201113-766-g870d 2nd stage bootloader
I (15) boot: compile time 12:10:57
I (16) boot: chip revision: 0
I (19) boot.esp32c3: SPI Speed      : 40MHz
I (19) boot.esp32c3: SPI Mode       : DIO
I (22) boot.esp32c3: SPI Flash Size : 2MB
I (24) boot: Enabling RNG early entropy source...
I (30) boot: Partition Table:
I (32) boot: ## Label            Usage          Type ST Offset   Length
I (36) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
I (39) boot:  1 storage          Unknown data     01 ff 00010000 00001000
I (43) boot:  2 factory          factory app      00 00 00020000 00100000
I (47) boot: End of partition table
I (49) esp_image: segment 0: paddr=0x00020020 vaddr=0x3c020020 size=0x05508 ( 21768) map
I (64) esp_image: segment 1: paddr=0x00025530 vaddr=0x3fc88780 size=0x014cc (  5324) load
I (67) esp_image: segment 2: paddr=0x00026a04 vaddr=0x40380000 size=0x08780 ( 34688) load
0x40380000: _vector_table at ??:?

I (86) esp_image: segment 3: paddr=0x0002f18c vaddr=0x00000000 size=0x00e8c (  3724)
I (88) esp_image: segment 4: paddr=0x00030020 vaddr=0x42000020 size=0x171a8 ( 94632) map
0x42000020: esp_ota_get_app_description at /home/marius/clean/esp-idf_2/components/app_update/esp_app_desc.c:63

I (139) boot: Loaded app from partition at offset 0x20000
I (139) boot: Checking flash encryption...
I (144) flash_encrypt: flash encryption is enabled (1 plaintext flashes left)
I (148) boot: Disabling RNG early entropy source...
I (160) cpu_start: Pro cpu start user code
I (160) cpu_start: cpu freq: 40000000
I (161) cpu_start: Application information:
I (161) cpu_start: Project name:     flash_encryption
I (164) cpu_start: App version:      qa-test-v4.3-20201113-766-g870d
I (168) cpu_start: Compile time:     Dec 21 2020 12:10:55
I (171) cpu_start: ELF file SHA256:  209e8947c2e6a6a6...
I (174) cpu_start: ESP-IDF:          qa-test-v4.3-20201113-766-g870d
I (178) heap_init: Initializing. RAM available for dynamic allocation:
I (181) heap_init: At 3FC8A9F0 len 00035610 (213 KiB): D/IRAM
I (184) heap_init: At 3FCC0000 len 0001F260 (124 KiB): STACK/DRAM
I (188) heap_init: At 50000000 len 00002000 (8 KiB): FAKEDRAM
W (192) flash_encrypt: Flash encryption mode is DEVELOPMENT (not secure)
I (195) spi_flash: detected chip: gd
I (197) spi_flash: flash io: dio
W (199) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (207) cpu_start: Starting scheduler.

Example to check Flash Encryption status
This is esp32c3 chip with 1 CPU core(s), WiFi/BLE, silicon revision 0, 2MB external flash
FLASH_CRYPT_CNT eFuse value is 1
Flash encryption feature is enabled in DEVELOPMENT mode

在此阶段,如果用户需要更新或重新烧录二进制文件,请参考 重新烧录更新后的分区

使用主机生成的密钥

可在主机中预生成 flash 加密密钥,并将其烧录到 eFuse 密钥块中。这样,无需明文 flash 更新便可以在主机上预加密数据并将其烧录。该功能可在 开发模式发布模式 两模式下使用。如果没有预生成的密钥,数据将以明文形式烧录,然后 ESP32-C3 对数据进行就地加密。

备注

不建议在生产中使用该方法,除非为每个设备都单独生成一个密钥。

使用主机生成的密钥需完成以下步骤:

  1. 确保您的 ESP32-C3 设备有 相关 eFuses 中所示的 flash 加密 eFuse 的默认设置。

请参考如何检查 ESP32-C3 flash 加密状态

  1. 通过运行以下命令生成一个随机密钥:

espsecure.py generate_flash_encryption_key my_flash_encryption_key.bin
  1. 在第一次加密启动前,使用以下命令将该密钥烧录到设备上,这个操作只能执行 一次

espefuse.py --port PORT burn_key BLOCK my_flash_encryption_key.bin XTS_AES_128_KEY

其中 BLOCKBLOCK_KEY0BLOCK_KEY5 之间的一个空闲密钥区。

如果未烧录密钥并在启用 flash 加密后启动设备,ESP32-C3 将生成一个软件无法访问或修改的随机密钥。

  1. 项目配置菜单 中进行如下设置:

启用 flash 加密将增大引导加载程序,因而可能需更新分区表偏移量。请参考 引导加载程序大小

  1. 运行以下命令来构建并烧录完整的镜像:

idf.py flash monitor

备注

这个命令不包括任何应该被写入 flash 上的分区的用户文件。请在运行此命令前手动写入这些文件,否则在写入前应单独对这些文件进行加密。

该命令将向 flash 写入未加密的镜像:固件引导加载程序、分区表和应用程序。烧录完成后,ESP32-C3 将复位。在下一次启动时,固件引导加载程序会加密:固件引导加载程序、应用程序分区和标记为 加密 的分区,然后复位。就地加密可能需要时间,对于大的分区来说可能耗时一分钟。之后,应用程序在运行时被解密并执行。

如果使用开发模式,那么更新和重新烧录二进制文件最简单的方法是 重新烧录更新后的分区

如果使用发布模式,那么可以在主机上预先加密二进制文件,然后将其作为密文烧录。具体请参考 手动加密文件

重新烧录更新后的分区

如果用户以明文方式更新了应用程序代码并需要重新烧录,则需要在烧录前对其进行加密。请运行以下命令一次完成应用程序的加密与烧录:

idf.py encrypted-app-flash monitor

如果所有分区都需要以加密形式更新,请运行:

idf.py encrypted-flash monitor

发布模式

在发布模式下,UART 引导加载程序无法执行 flash 加密操作,只能 使用 OTA 方案下载新的明文镜像,该方案将在写入 flash 前加密明文镜像。

使用该模式需要执行以下步骤:

  1. 确保您的 ESP32-C3 设备有 相关 eFuses 中所示的 flash 加密 eFuse 的默认设置。

请参考如何检查 ESP32-C3 flash 加密状态

  1. 项目配置菜单,执行以下操作:

启用 flash 加密将增大引导加载程序,因而可能需更新分区表偏移量。请参考 引导加载程序大小

  1. 运行以下命令来构建并烧录完整的镜像:

idf.py flash monitor

备注

这个命令不包括任何应该被写入 flash 分区的用户文件。请在运行此命令前手动写入这些文件,否则在写入前应单独对这些文件进行加密。

该命令将向 flash 写入未加密的镜像:固件引导加载程序、分区表和应用程序。烧录完成后,ESP32-C3 将复位。在下一次启动时,固件引导加载程序会加密:固件引导加载程序、应用程序分区和标记为 加密 的分区,然后复位。就地加密可能需要时间,对于大的分区来说可能耗时一分钟。之后,应用程序在运行时被解密并执行。

一旦在发布模式下启用 flash 加密,引导加载程序将写保护 SPI_BOOT_CRYPT_CNT eFuse。

请使用 OTA 方案 对字段中的明文进行后续更新。

备注

如果用户已经预先生成了 flash 加密密钥并存储了一个副本,并且 UART 下载模式没有通过 CONFIG_SECURE_UART_ROM_DL_MODE 永久禁用,那么可以通过使用 espsecure.py encrypt_flash_data --aes_xts 预加密文件,从而在在本地更新 flash,然后烧录密文。请参考 手动加密文件

最佳实践

在生产中使用 flash 加密时:

  • 不要在多个设备之间重复使用同一个 flash 加密密钥,这样攻击者就无法从一台设备上复制加密数据后再将其转移到第二台设备上。

  • 如果不需要 UART ROM 下载模式,则应完全禁用该模式,或者永久设置为“安全下载模式”。安全下载模式永久性地将可用的命令限制在更新 SPI 配置、更改波特率、基本的 flash 写入和使用 get_security_info 命令返回当前启用的安全功能摘要。默认在发布模式下第一次启动时设置为安全下载模式。要完全禁用下载模式,请选择 CONFIG_SECURE_UART_ROM_DL_MODE 为“永久禁用 ROM 下载模式(推荐)”或在运行时调用 esp_efuse_disable_rom_download_mode()

  • 启用 安全启动 作为额外的保护层,防止攻击者在启动前有选择地破坏 flash 中某部分。

可能出现的错误

一旦启用 flash 加密,SPI_BOOT_CRYPT_CNT 的 eFuse 值将设置为奇数位。这意味着所有标有加密标志的分区都会包含加密的密本。如果 ESP32-C3 错误地加载了明文数据,则会出现以下三种典型的错误情况:

  1. 如果通过 明文固件引导加载程序镜像 重新烧录了引导加载程序分区,则 ROM 加载器将无法加载固件引导加载程序,并会显示以下错误类型:

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
invalid header: 0xb414f76b
invalid header: 0xb414f76b
invalid header: 0xb414f76b
invalid header: 0xb414f76b
invalid header: 0xb414f76b
invalid header: 0xb414f76b
invalid header: 0xb414f76b

备注

不同应用程序中无效头文件的值不同。

备注

如果 flash 内容被擦除或损坏,也会出现这个错误。

  1. 如果固件的引导加载程序已加密,但通过 明文分区表镜像 重新烧录了分区表,引导加载程序将无法读取分区表,从而出现以下错误:

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:10464
ho 0 tail 12 room 4
load:0x40078000,len:19168
load:0x40080400,len:6664
entry 0x40080764
I (60) boot: ESP-IDF v4.0-dev-763-g2c55fae6c-dirty 2nd stage bootloader
I (60) boot: compile time 19:15:54
I (62) boot: Enabling RNG early entropy source...
I (67) boot: SPI Speed      : 40MHz
I (72) boot: SPI Mode       : DIO
I (76) boot: SPI Flash Size : 4MB
E (80) flash_parts: partition 0 invalid magic number 0x94f6
E (86) boot: Failed to verify partition table
E (91) boot: load partition table error!
  1. 如果引导加载程序和分区表已加密,但使用 明文应用程序镜像 重新烧录了应用程序,引导加载程序将无法加载应用程序,从而出现以下错误:

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:8452
load:0x40078000,len:13616
load:0x40080400,len:6664
entry 0x40080764
I (56) boot: ESP-IDF v4.0-dev-850-gc4447462d-dirty 2nd stage bootloader
I (56) boot: compile time 15:37:14
I (58) boot: Enabling RNG early entropy source...
I (64) boot: SPI Speed      : 40MHz
I (68) boot: SPI Mode       : DIO
I (72) boot: SPI Flash Size : 4MB
I (76) boot: Partition Table:
I (79) boot: ## Label            Usage          Type ST Offset   Length
I (87) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
I (94) boot:  1 phy_init         RF data          01 01 00010000 00001000
I (102) boot:  2 factory          factory app      00 00 00020000 00100000
I (109) boot: End of partition table
E (113) esp_image: image at 0x20000 has invalid magic byte
W (120) esp_image: image at 0x20000 has invalid SPI mode 108
W (126) esp_image: image at 0x20000 has invalid SPI size 11
E (132) boot: Factory app partition is not bootable
E (138) boot: No bootable app partitions in the partition table

ESP32-C3 flash 加密状态

  1. 确保您的 ESP32-C3 设备有 相关 eFuses 中所示的 flash 加密 eFuse 的默认设置。

要检查您的 ESP32-C3 设备上是否启用了 flash 加密,请执行以下操作之一:

在加密的 flash 中读写数据

ESP32-C3 应用程序代码可以通过调用函数 esp_flash_encryption_enabled() 来检查当前是否启用了 flash 加密。此外,设备可以通过调用函数 esp_get_flash_encryption_mode() 来识别 flash 加密模式。

一旦启用 flash 加密,使用代码访问 flash 内容时要更加小心。

flash 加密范围

SPI_BOOT_CRYPT_CNT eFuse 设置为奇数位的值,所有通过 MMU 的 flash 缓存访问的 flash 内容都将被透明解密。包括:

  • flash 中可执行的应用程序代码 (IROM)。

  • 所有存储于 flash 中的只读数据 (DROM)。

  • 通过函数 spi_flash_mmap() 访问的任意数据。

  • ROM 引导加载程序读取的固件引导加载程序镜像。

重要

MMU flash 缓存将无条件解密所有数据。flash 中未加密存储的数据将通过 flash 缓存“被透明解密”,并在软件中存储为随机垃圾数据。

读取加密的 flash

如果需要在不使用 flash 缓存 MMU 映射的情况下读取数据,推荐使用分区读取函数 esp_partition_read()。该函数只会解密从加密分区读取的数据。从未加密分区读取的数据不会被解密。这样,软件便能以相同的方式访问加密和未加密的 flash。

也可以使用以下 SPI flash API 函数:

使用非易失性存储器 (NVS) API 存储的数据始终从 flash 加密的角度进行存储和读取解密。如有需要,则由库提供加密功能。详情可参考 NVS 加密

写入加密的 flash

推荐使用分区写入函数 esp_partition_write()。此函数只会在将数据写入加密分区时加密数据,而写入未加密分区的数据不会被加密。通过这种方式,软件可以以相同的方式访问加密和非加密 flash。

也可以使用函数 esp_flash_write_encrypted() 预加密和写入数据。

此外,esp-idf 应用程序中存在但不支持以下 ROM 函数:

  • esp_rom_spiflash_write_encrypted 预加密并将数据写入 flash

  • SPIWrite 将未加密的数据写入 flash

由于数据是按块加密的,加密数据最小的写入大小为 16 字节,对齐也是 16 字节。

更新加密的 flash

OTA 更新

如果使用函数 esp_partition_write(),对加密分区的 OTA 更新将自动以加密形式写入。

在为已加密设备的 OTA 更新构建应用程序镜像之前,启用项目配置菜单中的 启动时使能 flash 加密 选项。

请参考 OTA 获取更多关于 ESP-IDF OTA 更新的信息。

通过串口更新加密 flash

通过串行引导加载程序烧录加密设备,需要串行引导加载程序下载接口没有通过 eFuse 被永久禁用。

在开发模式下,推荐的方法是 重新烧录更新后的分区

在发布模式下,如果主机上有存储在 eFuse 中的相同密钥的副本,那么就可以在主机上对文件进行预加密,然后进行烧录,具体请参考 手动加密文件

关闭 flash 加密

如果意外启用了 flash 加密,则明文数据的 flash 会使 ESP32-C3 无法正常启动。设备将不断重启,并报错 flash read err, 1000invalid header: 0xXXXXXX

对于开发模式下的 flash 加密,可以通过烧录 SPI_BOOT_CRYPT_CNT efuse 来关闭加密。每个芯片仅有 1 次机会,请执行以下步骤:

  1. 项目配置菜单 中,禁用 启动时使能 flash 加密 选项,然后保存并退出。

  2. 再次打开项目配置菜单,再次检查你是否已经禁用了该选项,如果这个选项仍被启用,引导加载程序在启动时将立即重新启用加密功能。

  3. 在禁用 flash 加密后,通过运行 idf.py flash 来构建和烧录新的引导加载程序和应用程序。

  4. 使用 espefuse.py (在 components/esptool_py/esptool 中)以关闭 SPI_BOOT_CRYPT_CNT,运行:

espefuse.py burn_efuse SPI_BOOT_CRYPT_CNT

重置 ESP32-C3,flash 加密应处于关闭状态,引导加载程序将正常启动。

flash 加密的要点

  • 使用 XTS-AES-128 加密 flash。 flash 加密密钥为 256 位,存储于芯片内部的 BLOCK_KEYN eFuse 中,并(默认)受保护,防止软件访问。

  • 通过 ESP32-C3 的 flash 缓存映射功能,flash 可支持透明访问——任何映射到地址空间的 flash 区域在读取时都将被透明地解密。

    为便于访问,某些数据分区最好保持未加密状态,或者也可使用对已加密数据无效的 flash 友好型更新算法。由于 NVS 库无法与 flash 加密直接兼容,因此无法加密非易失性存储器的 NVS 分区。详情可参见 NVS 加密

  • 如果以后可能需要启用 flash 加密,则编程人员在编写 使用加密 flash 代码时需小心谨慎。

  • 如果已启用安全启动,重新烧录加密设备的引导加载程序则需要“可重新烧录”的安全启动摘要(可参考 flash 加密与安全启动)。

启用 flash 加密将增大引导加载程序,因此可能需更新分区表偏移量。请参考 引导加载程序大小

重要

在首次启动加密过程中,请勿切断 ESP32-C3 的电源。如果电源被切断,flash 的内容将受到破坏,并需要重新烧录未加密数据。而这类重新烧录将不计入烧录限制次数。

flash 加密的局限性

flash 加密可以保护固件,防止未经授权的读取与修改。了解 flash 加密系统的局限之处亦十分重要:

  • flash 加密功能与密钥同样稳固。因而,推荐您首次启动设备时在设备上生成密钥(默认行为)。如果在设备外生成密钥,请确保遵循正确的后续步骤,不要在所有生产设备之间使用相同的密钥。

  • 并非所有数据都是加密存储。因而在 flash 上存储数据时,请检查您使用的存储方式(库、API 等)是否支持 flash 加密。

  • flash 加密无法防止攻击者获取 flash 的高层次布局信息。这是因为每对相邻的 16 字节 AES 块都使用相邻的 AES 密钥。当这些相邻的 16 字节块中包含相同内容时(如空白或填充区域),这些字节块将加密以产生匹配的加密块对。这让攻击者可在加密设备间进行高层次对比(例如,确认两设备是否可能运行相同的固件版本)。

  • 单独使用 flash 加密可能无法防止攻击者修改本设备的固件。为防止设备上运行未经授权的固件,可搭配 flash 加密使用 安全启动

flash 加密与安全启动

推荐 flash 加密与安全启动搭配使用。但是,如果已启用安全启动,则重新烧录设备时会受到其他限制:

  • 如果新的应用程序已使用安全启动签名密钥正确签名,则 OTA 更新 不受限制。

flash 加密的高级功能

以下部分介绍了 flash 加密的高级功能。

加密分区标志

部分分区默认为已加密。通过在分区的标志字段中添加 “encrypted” 标志,可在分区表描述中将其他分区标记为需要加密。在这些标记分区中的数据会和应用程序分区一样视为加密数据。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000
phy_init, data, phy,     0xf000,  0x1000
factory,  app,  factory, 0x10000, 1M
secret_data, 0x40, 0x01, 0x20000, 256K, encrypted

请参考 分区表 获取更多关于分区表描述的具体信息。

关于分区加密您还需要了解以下信息:

  • 默认分区表都不包含任何加密数据分区。

  • 启用 flash 加密后,"app" 分区一般都视为加密分区,因此无需标记。

  • 如果未启用 flash 加密,则 "encrypted" 标记无效。

  • 将可选 phy 分区标记为 "encrypted",可以防止物理访问读取或修改 phy_init 数据。

  • nvs 分区无法标记为 "encrypted" 因为 NVS 库与 flash 加密不直接兼容。

启用 UART 引导加载程序加密/解密

在第一次启动时,flash 加密过程默认会烧录以下 eFuse:

  • DIS_DOWNLOAD_MANUAL_ENCRYPT 在 UART 引导加载程序启动模式下运行时,禁止 flash 加密操作。

  • DIS_DOWNLOAD_ICACHE 在 UART 引导加载程序模式下运行时禁止整个 MMU flash 缓存。

  • DIS_PAD_JTAGDIS_USB_JTAG 禁用 JTAG。

  • DIS_DIRECT_BOOT``(即之前的 ``DIS_LEGACY_SPI_BOOT)禁用传统的 SPI 启动模式。

为了能启用这些功能,可在首次启动前仅烧录部分 eFuse,并用未设置值 0 写保护其他部分。例如:

espefuse.py --port PORT burn_efuse DIS_DOWNLOAD_MANUAL_ENCRYPT
espefuse.py --port PORT write_protect_efuse DIS_DOWNLOAD_MANUAL_ENCRYPT

备注

请注意在写保护前设置所有适当的位!

一个位可以控制三个 eFuse 的写保护,这意味着写保护一个 eFuse 位将写保护所有未设置的 eFuse 位。

由于 esptool.py 目前不支持读取加密 flash,所以对这些 eFuse 进行写保护从而使其保持未设置目前来说并不是很有用。

JTAG 调试

默认情况下,当启用 flash 加密(开发或发布模式)时,将通过 eFuse 禁用 JTAG 调试。引导加载程序在首次启动时执行此操作,同时启用 flash 加密。

请参考 JTAG 与 flash 加密和安全引导 了解更多关于使用 JTAG 调试与 flash 加密的信息。

手动加密文件

手动加密或解密文件需要在 eFuse 中预烧录 flash 加密密钥(请参阅 使用主机生成的密钥)并在主机上保留一份副本。 如果 flash 加密配置在开发模式下,那么则不需要保留密钥的副本或遵循这些步骤,可以使用更简单的 重新烧录更新后的分区 步骤。

密钥文件应该是单个原始二进制文件(例如:key.bin)。

例如,以下是将文件 build/my-app.bin 进行加密、烧录到偏移量 0x10000 的步骤。运行 espsecure.py,如下所示:

espsecure.py encrypt_flash_data --aes_xts --keyfile /path/to/key.bin --address 0x10000 --output my-app-ciphertext.bin build/my-app.bin

然后可以使用 esptool.py 将文件 my-app-ciphertext.bin 写入偏移量 0x10000。 关于为 esptool.py 推荐的所有命令行选项,请查看 idf.py build 成功时打印的输出。

备注

如果 ESP32-C3 在启动时无法识别烧录进去的密文文件,请检查密钥是否匹配以及命令行参数是否完全匹配,包括偏移量是否正确。

espsecure.py decrypt_flash_data 命令可以使用同样的选项(和不同的输入/输出文件)来解密 flash 密文或之前加密的文件。

技术细节

以下章节将提供 flash 加密操作的相关信息。

  • 有关在 Python 中实现的完整 flash 加密算法,可参见 espsecure.py 源代码中的函数 _flash_encryption_operation()

flash 加密算法

  • ESP32-C3 使用 XTS-AES 块密码模式进行 flash 加密,密钥大小为 256 位。

  • XTS-AES 是一种专门为光盘加密设计的块密码模式,它解决了其它潜在模式如 AES-CTR 在此使用情景下的不足。有关 XTS-AES 算法的详细描述,请参考 IEEE Std 1619-2007

  • flash 加密的密钥存储于一个 BLOCK_KEYN eFuse 中,默认受保护防止进一步写入或软件读取。

  • 有关在 Python 中实现的完整 flash 加密算法,可参见 espsecure.py 源代码中的函数 _flash_encryption_operation()