Skip to content

Conversation

almir-okato
Copy link
Collaborator

@almir-okato almir-okato commented May 24, 2025

When hardware flash encryption is enabled, force expected erased value (0xFF) into flash when erasing a region, and also always do a real erase before writing data into flash.

This is handled on this implementation because MCUboot's state machine relies on erased valued data (0xFF) readed from a previously erased region that was not written yet, however when hardware flash encryption is enabled, the flash read always decrypts whats being read from flash, thus a region that was erased would not be read as what MCUboot expected (0xFF).

This PR also bumps used IDF code version to v5.1.6.

How to test it within Zephyr on ESP32-S3 using a "zeroed" key (DEVELOPMENT AND TEST ONLY, so the device will not be bricked) and updating through Wi-Fi:

Use Zephyr branch from this PR:
zephyrproject-rtos/zephyr#90442
Use hal_espressif branch from this PR:
zephyrproject-rtos/hal_espressif#445

Prepare and enable Flash Encryption on ESP32-S3:

  • Erase the flash:
esptool.py -p /dev/ttyUSB0 erase_flash --force
  • Create a "zeroed" key (TEST ONLY, so the device will not be bricked):
dd if=/dev/zero of=zero_key.bin bs=1 count=32
  • Burn the key into the eFuses:
espefuse.py -p /dev/ttyUSB0 burn_key BLOCK_KEY4 zero_key.bin XTS_AES_128_KEY
  • Burn the eFuse that enables Flash Encryption:
espefuse.py burn_efuse SPI_BOOT_CRYPT_CNT

Building the SMPMGR sample application on Zephyr:

cd <ZEPHYRPROJECT_DIR>/zephyr/samples/subsys/mgmt/mcumgr/smp_svr/
  • Create an overlay file for ESP32-S3 with the content:
    <ZEPHYRPROJECT_DIR>/zephyr/samples/subsys/mgmt/mcumgr/smp_svr/boards/esp32s3_devkitm_procpu.overlay
&flash0 {
       write-block-size = <32>;
};

  • Build the first image, replacing <IP_ADDRESS> with a valid IP on your network:
west build -b esp32s3_devkitm/esp32s3/procpu -p --build-dir build_1st -- -DEXTRA_CONF_FILE="overlay-udp.conf" -DCONFIG_MAIN_STACK_SIZE=8192 -DCONFIG_MCUMGR_TRANSPORT_UDP_STACK_SIZE=4096  -DCONFIG_NET_CONFIG_MY_IPV4_ADDR=\"<IP_ADDRESS>\" -DCONFIG_NET_CONFIG_MY_IPV4_NETMASK=\"255.255.255.0\" -DCONFIG_NET_CONFIG_MY_IPV4_GW=\"\" -DCONFIG_WIFI=y -DCONFIG_NET_SHELL=y -DCONFIG_NET_L2_WIFI_SHELL=y -DCONFIG_BOOTLOADER_MCUBOOT=y -DCONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS=\"--align\ 32\ --max-align\ 32\ --pad\" -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y
  • Flash the image (will be force encrypted, since the encryption is already enabled on the device and we are using the DEVELOPMENT configs):
esptool.py -p /dev/ttyUSB0 -b 2000000 --no-stub --after no_reset write_flash  --flash_mode dio  --flash_freq 40m --encrypt 0x0020000 build_1st/zephyr/zephyr.signed.bin --force
  • Build the update image, it can be the same sample on other build_dir:
west build -b esp32s3_devkitm/esp32s3/procpu -p --build-dir build_2nd -- -DEXTRA_CONF_FILE="overlay-udp.conf" -DCONFIG_MAIN_STACK_SIZE=8192 -DCONFIG_MCUMGR_TRANSPORT_UDP_STACK_SIZE=4096  -DCONFIG_NET_CONFIG_MY_IPV4_ADDR=\"<IP_ADDRESS>\" -DCONFIG_NET_CONFIG_MY_IPV4_NETMASK=\"255.255.255.0\" -DCONFIG_NET_CONFIG_MY_IPV4_GW=\"\" -DCONFIG_WIFI=y -DCONFIG_NET_SHELL=y -DCONFIG_NET_L2_WIFI_SHELL=y -DCONFIG_BOOTLOADER_MCUBOOT=y -DCONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS=\"--align\ 32\ --max-align\ 32\ --pad\" -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y

Building and flashing MCUboot:

cd <MCUBOOT_DIR>/boot/espressif

(MCUBOOT_DIR should be the directory from Zephyr environment: <ZEPHYRPROJECT_DIR>/bootloader/mcuboot)

  • Modify <MCUBOOT_DIR>/boot/espressif/port/esp32s3/bootloader.conf with:
CONFIG_ESP_FLASH_SIZE=8MB
CONFIG_ESP_BOOTLOADER_SIZE=0xF000
CONFIG_ESP_BOOTLOADER_OFFSET=0x0000
CONFIG_ESP_IMAGE0_PRIMARY_START_ADDRESS=0x20000
CONFIG_ESP_APPLICATION_SIZE=0x150000
CONFIG_ESP_IMAGE0_SECONDARY_START_ADDRESS=0x170000
CONFIG_ESP_MCUBOOT_WDT_ENABLE=y
CONFIG_ESP_SCRATCH_OFFSET=0x3E0000
CONFIG_ESP_SCRATCH_SIZE=0x1F000

CONFIG_ESP_CONSOLE_UART=y
CONFIG_ESP_CONSOLE_UART_NUM=0

# Hardware Flash Encryption related options
CONFIG_SECURE_FLASH_ENC_ENABLED=1
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=1
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_DEC=1
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=1
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=1
CONFIG_SECURE_BOOT_ALLOW_JTAG=1
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=1
  • Build:
rm -rf build/ && cmake -DCMAKE_TOOLCHAIN_FILE=tools/toolchain-esp32s3.cmake -DMCUBOOT_TARGET=esp32s3  -DMCUBOOT_FLASH_PORT=/dev/ttyUSB0 -DESP_HAL_PATH=<ZEPHYRPROJECT_DIR>/modules/hal/espressif -B build -GNinja && ninja -C build 
  • Flash:
esptool.py -p /dev/ttyUSB0 -b 2000000 --no-stub --after no_reset write_flash  --flash_mode dio  --flash_freq 40m --encrypt 0x0000 build/mcuboot_esp32s3.bin --force

Workaround for empty slot1 and scratch regions when testing:

Testing like described, as Flash Encryption would be already enabled, reading an empty region (physical 0xFF values) would result in garbage as the read decryption would decrypt the data unconditionally. So generate a 0xFFs binary file and flash it to the regions in order to avoid an unexpected behavior:

dd if=/dev/zero ibs=1 count=126976 | LC_ALL=C tr "\000" "\377" >ffff.bin
esptool.py -p /dev/ttyUSB0 -b 2000000 --no-stub --after no_reset write_flash  --flash_mode dio  --flash_freq 40m --encrypt 0x3e0000 ffff.bin --force
dd if=/dev/zero ibs=1 count=1376256 | LC_ALL=C tr "\000" "\377" >ffff_slot1.bin
esptool.py -p /dev/ttyUSB0 -b 2000000 --no-stub --after no_reset write_flash  --flash_mode dio  --flash_freq 40m --encrypt 0x170000 ffff_slot1.bin --force
  • Now open the serial monitor and press the devkit's reset button.

Upload the update image through wifi using smpmgr:

  • Using the serial monitor, after the Zephyr application boot and starts the shell, connect the device to the wifi:
wifi connect --key-mgmt 1 --ssid "<SSID>" --passphrase "<PASSWORD>"
  • After the connection, on another terminal, upload the update image to the device:
smpmgr --timeout 10 --ip <IP_ADDRESS> upgrade --slot 1 build_2nd/zephyr/zephyr.signed.bin
  • After upload completes, reset the device and wait the swap process finishes:
smpmgr --timeout 10 --ip <IP_ADDRESS> os reset
  • The new image can be confirmed through a smpmgr command. Connect the device to wifi again, fetch the image information and use its hash to confirm the image.
smpmgr --timeout 10 --ip <IP_ADDRESS> image state-read
smpmgr --timeout 10 --ip <IP_ADDRESS> image state-write <SLOT0_IMAGE_HASH> true
  • If the new image isn't confirmed as above, the next boot will revert it and swap to the first one.

@almir-okato almir-okato force-pushed the fix_esp_mcuboot_flash_enc_flash_imp_layer branch from 64ec4e9 to cd51774 Compare June 20, 2025 19:39
@almir-okato almir-okato force-pushed the fix_esp_mcuboot_flash_enc_flash_imp_layer branch from cd51774 to b3b01c5 Compare July 30, 2025 19:35
@almir-okato almir-okato force-pushed the fix_esp_mcuboot_flash_enc_flash_imp_layer branch from b3b01c5 to c31bf4e Compare August 14, 2025 02:04
Also fix build if hal_espressif is used as IDF code layer (hal)
for Espressif Port

Signed-off-by: Almir Okato <[email protected]>
@almir-okato almir-okato force-pushed the fix_esp_mcuboot_flash_enc_flash_imp_layer branch from c31bf4e to 3eb4366 Compare August 21, 2025 20:11
Add cache flush after write/erase operations to avoid getting invalid
data when these are followed by read operation.

Signed-off-by: Almir Okato <[email protected]>
… encryption is enabled

When hardware flash encryption is enabled, force expected erased
value (0xFF) into flash when erasing a region, and also always
do a real erase before writing data into flash.

This is handled on this implementation because MCUboot's state
machine relies on erased valued data (0xFF) readed from a
previously erased region that was not written yet, however when
hardware flash encryption is enabled, the flash read always
decrypts whats being read from flash, thus a region that was
erased would not be read as what MCUboot expected (0xFF).

Signed-off-by: Almir Okato <[email protected]>
@almir-okato almir-okato force-pushed the fix_esp_mcuboot_flash_enc_flash_imp_layer branch from 3eb4366 to 8ef9137 Compare August 21, 2025 20:21
@almir-okato almir-okato changed the title DRAFT: ESP32-XX hardware flash encryption issue when updating images - "flash imp layer" solution ESP32-XX: fix hardware flash encryption issue when updating images Aug 22, 2025
@almir-okato almir-okato marked this pull request as ready for review August 22, 2025 22:58
@almir-okato almir-okato requested a review from d3zd3z as a code owner August 22, 2025 22:58
@almir-okato
Copy link
Collaborator Author

@utzig @marekmatej could you please take a look?

*libhal.a:rtc_time.*(.literal .text .literal.* .text.*)
*libhal.a:secure_boot_signatures_bootloader.*(.literal .text .literal.* .text.*)
*libhal.a:wdt_hal_iram.*(.literal .text .literal.* .text.*)
*libgcc.a:*.*(.literal .text .literal.* .text.*)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is libgcc.a entry really needed?

@@ -218,36 +237,55 @@ int flash_area_read(const struct flash_area *fa, uint32_t off, void *dst,
return 0;
}

static bool aligned_flash_write(size_t dest_addr, const void *src, size_t size)
static bool aligned_flash_write(size_t dest_addr, const void *src, size_t size, bool erase)
{
#ifdef CONFIG_SECURE_FLASH_ENC_ENABLED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esp_flash_encryption_enabled(); will return if encryption is enabled. Why the need of #ifdef CONFIG_SECURE_FLASH_ENC_ENABLED?

}
}

if(bytes < sizeof(write_data)) {
Copy link
Contributor

@sylvioalves sylvioalves Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check code-style as there are some missing spaces like in if(bytes < sizeof(write_data)) {


uint32_t offset = bytes;

while (bytes_remaining != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the while loop could start in L333 and grab everything related to the partial read-erase-write.

BOOT_LOG_ERR("%s: Flash erase failed", __func__);
return -1;
}
#if VALIDATE_PROGRAM_OP

flush_cache(start_addr, len);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aligned_flash_erase already flush_cache() at the end. Is this needed again?


if (bootloader_flash_erase_range(start_addr, len) != ESP_OK) {
if(!aligned_flash_erase(start_addr, len)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should #ifdef CONFIG_SECURE_FLASH_ENC_ENABLED be checked before aligned_flash_erase in here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants