Link Search Menu Expand Document

Enabling ESP32 Secure Boot V2 (SBV2): A Step-by-step Guide

ESP32

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

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

In Development

Steps to Enable SBV2 in Development

  1. On a development machine running Linux, install the Docker engine.

  2. 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 \
      .
    
  3. 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
    
  4. 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 below

    I (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

  1. 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
    
  2. 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

  1. 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
    
  2. 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/
    
  3. 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 and void_app.bin that were put in the shared/ folder.

    Signed Firmware Bundle View

    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 aforementioned shared/ folder.

    "Signed Firmware"

  4. 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

  1. 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
    
  2. 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 image apps/hello_world/build/partition_table/partition-table.bin to the shared/ folder.

  3. 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 the shared/ folder.

  4. 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.