Enabling ESP32 Secure Boot V2 (SBV2): A Step-by-step Guide
We will assume the IDF bootloader and the associated application images, and use them as an example in this guide. For enabling SBV2 with the MCUboot bootloader and Zephyr application images, please refer to this document.
Useful References
Here are some useful references to help understand secure boot and secure boot enablement operations for ESP32 platforms
- Enabling Secure Boot V2 on ESP32 Platforms in Development
- Enabling Secure Boot V2 on ESP32 Platforms in Production
- Espressif DevCon23 - Enabling Secure Boot (V2) on ESP32 Platforms in Development and Production
Required Hardware
Two units of unfused ESP32 ECO3 and later: one for development fusing, the other for production fusing. Tested on the following development board
- ESP32-DevKitC-32E (Mouser link; Datasheet)
In Development
Steps to Enable SBV2 in Development
-
On a development machine running Linux, install the Docker engine.
-
Build Docker container images for secure boot fuse blowing and firmware signing on development machine.
git clone https://github.com/thistletech/esp32-devenvs.git cd esp32-devenvs/esp32 docker build -f Dockerfile.esp32_fuseblower -t esp32fb:dev \ --build-arg IDF_SDKCONFIG=sdkconfig.sbv2_nojtag \ --build-arg SBV2_PRIVATE_KEY=sbv2_private_dev.pem \ .
-
Connect an unfused ESP32 development board to development machine through USB, and run Docker container
mkdir -p shared chmod 777 -R shared/ # Modify the device node /dev/ttyUSB0 as needed for direct board interactions # from inside the container docker run --device=/dev/ttyUSB0 -v "$(pwd)/shared":/home/esp/shared -it esp32fb:dev
-
Inside the container, blow eFuses by flashing the prebuilt (and signed) “void app” firmware images
esp@c29e740b2630:~$ source ${HOME}/esp-idf/export.sh esp@c29e740b2630:~$ cd apps/void_app # Flash bootloader # Adjust device node (-p option) as needed. # ESP32's bootloader shall be flashed at offset 0x1000 esp@c29e740b2630:~/apps/void_app$ esptool.py --chip esp32 \ --port=/dev/ttyUSB0 \ --baud=460800 \ --before=default_reset \ --after=no_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x1000 build/bootloader/bootloader.bin # Flash partition table and app # Adjust device node (-p option) as needed esp@c29e740b2630:~/apps/void_app$ esptool.py -c esp32 \ -p /dev/ttyUSB0 \ -b 460800 \ --before=default_reset \ --after=hard_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x20000 build/void_app.bin \ 0x10000 build/partition_table/partition-table.bin # Should see "I'm the void app. I do nothing." in serial console output esp@c29e740b2630:~/apps/void_app$ idf.py -p /dev/ttyUSB0 monitor
Inspect the serial console output, and you should see log entries such as
I (206) secure_boot_v2: Verifying with RSA-PSS... I (212) secure_boot_v2: Signature verified successfully!
Now secure boot is enabled. From now on, any update in the application image will required the image to be signed for it to boot on this board. You may try to flash an unsigned image and observe the boot failure.
# Erase flash esp@c29e740b2630:~/apps/void_app$ esptool.py --chip esp32 erase_flash --force # Flash the bootloader image, with the --force option this time as required # by SBV2-enabled device esp@c29e740b2630:~/apps/void_app$ esptool.py --chip esp32 \ --port=/dev/ttyUSB0 \ --baud=460800 \ --before=default_reset \ --after=no_reset \ --no-stub \ write_flash \ --force \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x1000 build/bootloader/bootloader.bin # Flash the unsigned void app image as well as the partition table image esp@c29e740b2630:~/apps/void_app$ esptool.py -c esp32 \ -p /dev/ttyUSB0 \ -b 460800 \ --before=default_reset \ --after=hard_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x20000 build/void_app-unsigned.bin \ 0x10000 build/partition_table/partition-table.bin
If you run
idf.py monitor
now, you should be able to see the device entering a boot loop showing log entries as belowI (222) esp_image: Verifying image signature... I (223) secure_boot_v2: Verifying with RSA-PSS... No signature block magic byte found at signature sector (found 0xff not 0xe7). Image not V2 signed? E (231) secure_boot_v2: Secure Boot V2 verification failed. E (237) esp_image: Secure boot signature verification failed I (244) esp_image: Calculating simple hash to check for corruption... W (304) esp_image: image valid, signature bad
Steps to Build and Sign Custom Application Image in Development
-
On host machine, drop the application source folder into the
shared/
directory, and inside the container, copy the application source folder to/home/esp/app/
. We will use an ESP-IDF sample application as an example.Inside container
# Set up environment. PWD is /home/esp esp@c29e740b2630:~$ . esp-idf/export.sh # Get sample app - hello_world and build for esp32 target esp@c29e740b2630:~$ cp -r esp-idf/examples/get-started/hello_world/ apps/ esp@c29e740b2630:~$ cd apps/hello_world # Configure app. You may adjust the sdkconfig settings as needed for your app esp@c29e740b2630:~/apps/hello_world$ rm -rf sdkconfig sdkconfig.ci esp@c29e740b2630:~/apps/hello_world$ ln -s ../sbv2_private_pem.app sbv2_private.pem esp@c29e740b2630:~/apps/hello_world$ ln -s ../sdkconfig.apps sdkconfig.defaults esp@c29e740b2630:~/apps/hello_world$ idf.py set-target esp32 esp@c29e740b2630:~/apps/hello_world$ idf.py build
-
Inside the container, flash the signed app
esp@c29e740b2630:~/apps/hello_world$ idf.py flash # Display serial output. crtl+] to exit esp@c29e740b2630:~/apps/hello_world$ idf.py monitor
In Production
Steps to Enable SBV2 in Production
-
Connect a mew unfused ESP32 development board to development machine through USB, and run Docker container
mkdir -p shared chmod 777 -R shared/ # Modify the device node /dev/ttyUSB0 as needed for direct board interactions # from inside the container docker run --device=/dev/ttyUSB0 -v "$(pwd)/shared":/home/esp/shared -it esp32fb:dev
-
Copy the
void_app
firmware images from the container to host.Inside the container, under
/home/esp/
esp@c29e740b2630:~$ cp apps/void_app/build/partition_table/partition-table.bin shared/ esp@c29e740b2630:~$ cp apps/void_app/build/bootloader/bootloader.bin shared/ esp@c29e740b2630:~$ cp apps/void_app/build/void_app.bin shared/
-
On host machine, open the Thistle Control Center from a web browser, create a project, and go go the “Signed Firmware” menu. Click the “+Signed Firmware Bundle” button to add a new signed firmware bundle: pick a name for it, choose “ESP32” and “ESP-IDF” as the hardware type and firmware type, respectively, and upload
bootloader.bin
andvoid_app.bin
that were put in theshared/
folder.Click the “Create” button to sign the bootloader and application images using a production signing key managed in a cloud key management system. Download the production signed images suffixed with
.patched_<timestamp>
to the aforementionedshared/
folder. -
Inside the container, flash the production signed bootloader and application images
esp@c29e740b2630:~$ mkdir -p prod_signed/ esp@c29e740b2630:~$ mv shared/void_app.bin.patched_1742873559.3631885 shared/bootloader.bin.patched_1742873561.3099582 shared/partition-table.bin prod_signed/ esp@c29e740b2630:~$ source ${HOME}/esp-idf/export.sh esp@c29e740b2630:~$ cd prod_signed/ # Flash bootloader # Adjust device node (-p option) as needed. # ESP32's bootloader shall be flashed at offset 0x1000 esp@c29e740b2630:~/prod_signed $ esptool.py --chip esp32 \ --port=/dev/ttyUSB0 \ --baud=460800 \ --before=default_reset \ --after=no_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x1000 bootloader.bin.patched_1742873561.3099582 # Flash partition table and app # Adjust device node (-p option) as needed esp@c29e740b2630:~/prod_signed$ esptool.py -c esp32 \ -p /dev/ttyUSB0 \ -b 460800 \ --before=default_reset \ --after=hard_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x20000 void_app.bin.patched_1742873559.3631885 \ 0x10000 partition-table.bin # Should see "I'm the void app. I do nothing." in serial console output esp@c29e740b2630:~/prod_signed$ cd ${HOME}/apps/void_app/ esp@c29e740b2630:~/apps/void_app$ idf.py -p /dev/ttyUSB0 monitor
Steps to Sign Custom Application Image in Production
-
Connect the production fused ESP32 development board to development machine through USB, and run Docker container
mkdir -p shared chmod 777 -R shared/ # Modify the device node /dev/ttyUSB0 as needed for direct board interactions # from inside the container docker run --device=/dev/ttyUSB0 -v "$(pwd)/shared":/home/esp/shared -it esp32fb:dev
-
Inside the container, build the application image by following the steps described in Section “Steps to Build and Sign Custom Application Image in Development” above, and copy the development signed application image
apps/hello_world/build/hello_world.bin
and the partition table imageapps/hello_world/build/partition_table/partition-table.bin
to theshared/
folder. -
On host machine, create a new signed firmware bundle in TCC in the same project in which the
void_app
got signed, and upload the development signed application image to it (one can ignore the bootloader image if there’s no change in the bootloader). Pick a meaningful name for the signed firmware bundle, and choose “ESP32” and “ESP-IDF” as the hardware type and firmware type, respectively.Click the “Create” button to sign the application image using the production signing key managed in a cloud key management system. Download the production signed images suffixed with
.patched_<timestamp>
to theshared/
folder. -
Inside the container, flash the production signed application image
esp@c29e740b2630:~$ mkdir -p prod_signed/ esp@c29e740b2630:~$ mv shared/hello_world.bin.patched_<timestamp> shared/partition-table.bin prod_signed/ esp@c29e740b2630:~$ source ${HOME}/esp-idf/export.sh esp@c29e740b2630:~$ cd prod_signed/ # Flash partition table and app # Adjust device node (-p option) as needed esp@c29e740b2630:~/prod_signed$ esptool.py -c esp32 \ -p /dev/ttyUSB0 \ -b 460800 \ --before=default_reset \ --after=hard_reset \ --no-stub \ write_flash \ --flash_mode dio \ --flash_freq 80m \ --flash_size keep \ 0x20000 hello_world.bin.patched_<timestamp> \ 0x10000 partition-table.bin # Check if the application image boots. crtl+] to exit esp@c29e740b2630:~/prod_signed$ cd ${HOME}/apps/hello_world esp@c29e740b2630:~/apps/hello_world$ idf.py -p /dev/ttyUSB0 monitor
Enable SBV2 on Other ESP32 Platforms (ESP32-S2, ESP32-S3)
On other ESP32 platforms, e.g., ESP32-S2 and ESP32-S3, one can enable secure boot V2 in a similar manner, in development and in production. Please refer to our Github repository thistletech/esp32-devenvs for more detail.