Secure Boot and Flash Encryption on ESP32S3 WROOM 1U N16

Hi,
I despereately try to get “secure boot” and “flash encryption” working on my ESP32S3 WROOM 1U N16.

There is this “egg and chicken” problem:
I am only able to test this bye “enable” and “burn” the efuses for
a) secure boot
b) flash encryption

But by enabling and burning the efuses with a false encryption, then I am stuck and the device is “kaput” !!

When I run the project without secure boot and flash encryption, then everyting works fine.

Only when I try to introduce secure-boot and flash-encryption, things get broken!

I have established all this (see code below):

  • script 1: does the key generation for secure-boot and flash-encryption
  • script 2: does the flash burning and after asking, does the efuse enable and burn
  • platformio.ini file ready

Here are the Arduino IDE settings of my board:

Note: The ESP32S3 WROOM 1U N16 device has WiFi and Bluetooth.

   Board:               ESP32S3 Dev Module
   Upload Speed:        921600
   USB Mode:            Hardware CDC and JTAG
   USB CDC On Boot:     Enabled (connects Serial-Port automatically)
   USB FW MSC on Boot:  Disabled
   USB DFU On Boot:     Disabled
   Upload Mode:         UART0 / Hardware CDC
   CPU Frequency:       240MHz (WiFi)
   Flash Mode:          QIO 80MHy
   Flash Size:          16MB (128 Mb)
   Partition Scheme:    No OTA (2M APP, 2M SPIFFS) // too tight: Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
   Core Debug Level:    None
   PSRAM:               Disabled
   Arduino Runs On:     Core 1
   Events Run On:       Core 1
   Erase All Flash Before Sketch Upload: Enabled
   JTAG Adapter:        Disabled
   Zigbee Mode:         Disabled

Here is the platformio.ini file:

[env:esp32s3dev]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

upload_speed = 921600
monitor_speed = 115200

board_build.mcu = esp32s3
board_build.cpu = esp32s3
board_build.f_cpu = 240000000L

board_build.flash_mode = qio
board_build.flash_freq = 80m
board_build.flash_size = 16MB

board_build.partitions = partitions.csv

lib_ldf_mode = deep+
lib_deps = 
    ESP32Time@^2.0.6
    ArduinoJson@^7.4.1
    GxEPD2@^1.6.4
    U8g2_for_Adafruit_GFX@^1.8.0
    Adafruit GFX Library@^1.12.1

build_flags =
  ; ---- Arduino USB Config ----
  -D ARDUINO_USB_CDC_ON_BOOT=1
  -D ARDUINO_USB_MODE=1                 ; USB Mode: CDC + JTAG
  -D CONFIG_ARDUINO_RUNNING_CORE=1
  -D CONFIG_ARDUINO_EVENT_RUNNING_CORE=1
   ; Debug level: none
  -D CORE_DEBUG_LEVEL=0  
  ; ---- Secure Boot V2 ----
  -D CONFIG_SECURE_BOOT=y
  -D CONFIG_SECURE_BOOT_V2_ENABLED=y
  -D CONFIG_SECURE_SIGNED_BINARIES=y
  -D CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y
  -D CONFIG_SECURE_BOOT_V2_EFUSE_KEY_BLOCK_KEY0=y
  -D CONFIG_SECURE_BOOT_V2_EFUSE_KEY_DIS_WRITE_PROTECT=y
  ; ---- Flash Encryption ----
  -D CONFIG_FLASH_ENCRYPTION_ENABLED=y
  -D FLASH_ENCRYPTION_ENABLED=1
    ; ---- NVS Encryption ----
  -D CONFIG_NVS_ENCRYPTION=n
  ; ---- Bootloader Config ----
  -D CONFIG_BOOTLOADER_LOG_LEVEL=0

upload_protocol = esptool

Here is my partitions.csv file:

# Name,     Type, SubType, Offset,   Size
nvs,        data, nvs,     0x9000,   0x6000
phy_init,   data, phy,     0xf000,   0x1000
nvs_key,    data, nvs_keys,0x10000,  0x1000
app0,       app,  factory, 0x20000,  4M
spiffs,     data, spiffs,  0x420000, 2M

I don’t have any sdkconfig file!

Here is script 1: key generation:

#!/bin/bash
set -e

# === USB PORT ===
PORT="/dev/cu.usbmodem213101"

# === ESP-IDF PATH ===
KEY_DIR="./secure"
BUILD_DIR=".pio/build/esp32s3dev"
NVS_KEY="$(pwd)/$KEY_DIR/nvs_encryption_key.bin"
NVS_KEY_FILE="$KEY_DIR/nvs_encryption_key.bin"
BOOT_APP0="/Users/myapp/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin"
FLASH_KEY="$KEY_DIR/flash_encryption_key.bin"
BOOT_SIGN_KEY="$KEY_DIR/secure_boot_private_key.pem"
BOOT_DIGEST="$KEY_DIR/secure_boot_public_key_digest.bin"
OUTPUT_DIR="./bin"
mkdir -p "$OUTPUT_DIR"

# === Step 1: Build firmware ===
echo "🔧 Clean target..."
pio run --target clean
echo "🔧 Building firmware..."
pio run

# /////////////////// SECURE BOOT ///////////////////
echo "🔐 Secure boot setup..."

# === Step 1: Generate secure boot signing key if missing ===
if [ ! -f "$BOOT_SIGN_KEY" ]; then
  echo "🔏 Generating secure boot signing key..."
  espsecure.py generate_signing_key --version 2 $BOOT_SIGN_KEY
else
  echo "🔏 Secure boot signing key already exists."
fi

# === Step 2: Sign bootloader ===
echo "✍️  Signing bootloader..."
espsecure.py sign_data \
  --version 2 \
  --keyfile "$BOOT_SIGN_KEY" \
  --output "$OUTPUT_DIR/bootloader-signed.bin" \
  "$BUILD_DIR/bootloader.bin"

# === Step 3: Create public key digest for eFuse if not exists ===
if [ ! -f "$BOOT_DIGEST" ]; then
  echo "📥 Generating secure boot public key digest..."
  espsecure.py digest_sbv2_public_key \
  --key $BOOT_SIGN_KEY \
  --output $BOOT_DIGEST
fi

# # === Step 3b: Copy signed bootloader to output directory ===
echo "📁 Copying bootloader..."
cp "$BUILD_DIR/bootloader.bin" "$OUTPUT_DIR/"

# === Step 4: Copy partitions.bin ===
echo "📁 Copying required binaries..."
cp "$BUILD_DIR/partitions.bin" "$OUTPUT_DIR/"

# === Step 5: Copy bootapp0.bin ===
if [ -f "$BOOT_APP0" ]; then
  echo "📁 Copying boot_app0.bin... "
  cp "$BOOT_APP0" "$OUTPUT_DIR/boot_app0.bin"
else
  echo "⚠️ Warning: boot_app0.bin not found at $BOOT_APP0. Skipping copy."
fi

# /////////////////// FLASH ENCRYPTION ///////////////////
echo "🔒 After eFuses are burned, the hardware will automatically encrypt the firmware in flash on first boot !"

# === Step 6: Copy firmware.bin ===
echo "📁 Therefore copying firmware is enough..."
cp "$BUILD_DIR/firmware.bin" "$OUTPUT_DIR/"

echo "🔒 However, we still need the Flash Encryption Key since used when burning the eFuse..."

# === Step 7: Generate flash encryption key if missing ===
if [ ! -f "$FLASH_KEY" ]; then
  echo "🔐 Generating flash encryption key..."
  espsecure.py generate_flash_encryption_key "$FLASH_KEY"
else
  echo "🔐 Flash encryption key already exists."
fi

# === Step 7b: Encript firmware ===
echo "🔐 Encrypting firmware..."
espsecure.py encrypt_flash_data \
  --keyfile "$FLASH_KEY" \
  --address 0x10000 \
  --output $OUTPUT_DIR/firmware-encrypted.bin \
  $BUILD_DIR/firmware.bin

# === Step 7c: Sign encripted firmware ===

# Sign encrypted firmware
echo "🔐 Signing encrypted firmware..."
espsecure.py sign_data \
  --version 2 \
  --keyfile $BOOT_SIGN_KEY \
  --output $OUTPUT_DIR/firmware-encrypted-signed.bin \
  $OUTPUT_DIR/firmware-encrypted.bin  


# === Step 8: Copy partitions.bin ===
echo "📁 Copying required binaries..."
cp "$BUILD_DIR/partitions.bin" "$OUTPUT_DIR/"

# === Final info ===
echo "✅ Done. Files ready in $OUTPUT_DIR:"
ls -lh "$OUTPUT_DIR"

echo
echo "🧩 Flash Map:"
echo "  0x0000   bootloader-signed.bin  (encrypted secure boot !)"
echo "  0x8000   partitions.bin         (not encrypted)"
echo "  0xe000   boot_app0.bin          (not encrypted)"
echo "  0x10000  firmware.bin           (encrypted on first boot !)"
echo
echo "  NOTE: Be sure to burn eFuses only after verifying successful app!"

Here is my script 2: flash download and efuse-enable / efuse-burn

#!/bin/bash
set -e

# === CONFIGURATION ===
PORT="/dev/cu.usbmodem213101"
FLASH_MODE="dio"        # typical value for QIO flash
FLASH_FREQ="80m"        # typical value for QIO flash
FLASH_SIZE="16MB"       # adjust if needed
BAUD="115200"           # typical value for ESP32-S3
CHIP="esp32s3"          # specify the chip type

BUILD_DIR=".pio/build/esp32s3dev"
BIN_DIR="./bin"
KEY_DIR="./secure"

BOOTLOADER_BIN="$BUILD_DIR/bootloader.bin"
BOOTLOADER_ENCR_BIN="$BIN_DIR/bootloader-signed.bin"
PARTITIONS_BIN="$BIN_DIR/partitions.bin"
BOOT_APP0="$BIN_DIR/boot_app0.bin"
# NVS_BIN="$BIN_DIR/nvs.bin"
# FIRMWARE_ENCR_BIN="$BIN_DIR/firmware-encrypted.bin"
FIRMWARE_ENCR_AND_SIGNED_BIN="$BIN_DIR/firmware-encrypted-signed.bin"

BOOT_SIGN_KEY="$KEY_DIR/secure_boot_private_key.pem"
BOOT_DIGEST_FILE="$KEY_DIR/secure_boot_public_key_digest.bin"
FLASH_KEY_FILE="$KEY_DIR/flash_encryption_key.bin"
NVS_KEY_FILE="$KEY_DIR/nvs_encryption_key.bin"

# Create directories
mkdir -p "$BIN_DIR"


# # === Step 1: Sign encrypted firmware also with the same key as the bootloader ===
# # The ESP32-S3 ROM bootloader encrypts flash contents on first boot after flash encryption efuse is set.
# # This happens automatically if the bootloader is not already encrypted.
# echo "🔧 Sign unencrypted firmware with the same key as the bootloader..."
# echo "🔧 It is enough to flash the unencrypted, but signed! firmware"
# echo "🔧 # The ESP32-S3 ROM bootloader encrypts flash contents on first boot after flash encryption efuse is set"
# espsecure.py sign_data \
#   --version 2 \
#   --keyfile $BOOT_SIGN_KEY \
#   --output $FIRMWARE_SIGNED_BIN \
#   $BUILD_DIR/firmware.bin

# espsecure.py sign_data \
#   --version 2 \
#   --keyfile $BOOT_SIGN_KEY \
#   --output $BIN_DIR/firmware-encrypted-signed.bin \
#   $BIN_DIR/firmware-encrypted.bin

# === Step 1: Erase entire flash before flashing ===
echo "🧹 Erasing entire flash..."
esptool.py --chip $CHIP --port $PORT erase_flash

# === Step 2: Flash bootloader, partitions, nvs and firmware ===
echo "⚡ Flashing encrypted bootloader, partitions, encrypted nvs and encrypted firmware..."

# === Step 3: write entire firmware to flash ===
esptool.py \
  --chip "$CHIP" \
  --port "$PORT" \
  --baud "$BAUD" \
  --before default_reset \
  --after hard_reset \
  write_flash -z \
  --flash_mode "$FLASH_MODE" \
  --flash_freq "$FLASH_FREQ" \
  --flash_size "$FLASH_SIZE" \
  0x0000 $BOOTLOADER_ENCR_BIN \
  0x8000 $PARTITIONS_BIN \
  0xe000 $BOOT_APP0 \
  0x10000 $FIRMWARE_ENCR_AND_SIGNED_BIN  

# esptool.py \
#   --chip "$CHIP" \
#   --port "$PORT" \
#   --baud "$BAUD" \
#   --before default_reset \
#   --after hard_reset \
#   write_flash -z \
#   --flash_mode "$FLASH_MODE" \
#   --flash_freq "$FLASH_FREQ" \
#   --flash_size "$FLASH_SIZE" \
#   0x0000 "$BOOTLOADER_BIN" \
#   0x8000 $BUILD_DIR/partitions.bin \
#   0xe000 $BOOT_APP0 \
#   0x10000 $BUILD_DIR/firmware.bin



# espefuse.py --port /dev/cu.usbmodem213101 summary

# espsecure.py verify_signature \
#   --version 2 \
#   --keyfile $KEY_DIR/secure_boot_private_key.pem \
#   $BIN_DIR/bootloader-signed.bin

    
# === Step 4: Burn eFuses for secure boot, flash encryption, and security config ===
echo "🔥 WARNING: About to burn eFuses - this is IRREVERSIBLE!"
echo "🔥 Make sure the device boots successfully before proceeding!"
echo
read -p "Continue with eFuse burning? (y/n): " confirm
if [ "$confirm" != "y" ]; then
    echo "❌ eFuse burning cancelled."
    exit 1
fi

echo "🔥 Burning eFuses for Secure Boot, Flash Encryption, and Security Configuration..."

# Burn secure boot public key digest
echo "🔐 Burning secure boot public key digest..."
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_key \
    BLOCK_KEY2 "$BOOT_DIGEST_FILE" \
    SECURE_BOOT_DIGEST0

# Enable secure boot v2
echo "🔐 Enabling Secure Boot v2..."
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    SECURE_BOOT_EN 1

# 🔒 Burn flash encryption key
echo "🔒 Burning flash encryption key..."
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_key  \
    BLOCK_KEY0 "$FLASH_KEY_FILE" \
    XTS_AES_256_KEY_1

# ✅ Optional: Write-protect key block
espefuse.py --chip $CHIP --port $PORT --do-not-confirm write_protect_efuse \
    BLOCK_KEY0

# 🔒 Enable flash encryption via counter (burning 1 bit of SPI_BOOT_CRYPT_CNT)
echo "🔒 Enabling Flash Encryption..."
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    SPI_BOOT_CRYPT_CNT 0x1

# Security configuration eFuses
echo "🛡️  Burning security configuration eFuses..."

# Disable USB JTAG
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_USB_JTAG 1

# Hard disable JTAG
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    HARD_DIS_JTAG 1

# Soft disable JTAG (set to 7)
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    SOFT_DIS_JTAG 7

# Disable USB OTG download mode
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_USB_OTG_DOWNLOAD_MODE 1

# Disable direct boot
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_DIRECT_BOOT 1

# Disable download mode instruction cache
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_DOWNLOAD_ICACHE 1

# Disable download mode data cache
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_DOWNLOAD_DCACHE 1

# Disable download mode manual encryption
espefuse.py --chip $CHIP --port $PORT --do-not-confirm burn_efuse \
    DIS_DOWNLOAD_MANUAL_ENCRYPT 1

echo "✅ Flashing and eFuse burning complete."
echo "🇨🇭🇨🇭🇨🇭🇨🇭🇨🇭 Your device is now locked-down and secure !! 🇨🇭🇨🇭🇨🇭🇨🇭🇨🇭"

Correct. Because for framework = arduino, you have a precompiled version of ESP-IDF, which you can’t just change the ESP-IDF config flag afterwards. You have to configure it to compile ESP-IDF from source and the Arduino framework as a component on top. In that ESP-IDF project, you then have a sdkconfig to configure.

For ESP-IDF 4.4.7 + Arduino-ESP32 2.0.17. use platform = espressif32@6.11.0 and the example from https://github.com/platformio/platform-espressif32/tree/develop/examples/espidf-arduino-blink

For ESP-IDF 5.4.1 + Arduino-ESP32 3.2.0 use platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip and the example from https://github.com/pioarduino/platform-espressif32/tree/54.03.20/examples/espidf-arduino-blink.

Thank you Max.

I tried the second option… However, the referenced platform-zip leads to an error when compiling with my code.

The error

fatal error: Network.h: No such file or directory

More in detail:

Compiling .pio/build/esp32s3dev/liba2a/WiFi/WiFiSTA.cpp.o
In file included from /Users/myname/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiSTA.h:30,
                 from /Users/myname/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFi.h:34,
                 from /Users/myname/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/AP.cpp:7:
/Users/myname/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiGeneric.h:44:10: fatal error: Network.h: No such file or directory

It seems to me that your platform-zip has some “issue” added concerning WiFi module (i.e. the Network.h is included and it should not !!)

This is my idf_component file

dependencies:
  idf:
    version: ">=5.0"
  arduino: "*"

  # External Arduino libraries
  ESP32Time:
    git: https://github.com/fbiego/ESP32Time.git
    version: ^2.0.6

  bblanchon/ArduinoJson:
    git: https://github.com/bblanchon/ArduinoJson.git
    version: ^7.4.1

  ZinggJM/GxEPD2:
    git: https://github.com/ZinggJM/GxEPD2.git
    version: ^1.6.4

  olikraus/U8g2_for_Adafruit_GFX:
    git: https://github.com/olikraus/U8g2_for_Adafruit_GFX.git
    version: ^1.8.0

  adafruit/Adafruit-GFX-Library:
    git: https://github.com/adafruit/Adafruit-GFX-Library.git
    version: ^1.12.1

  arduino/ArduinoHttpClient:
    git: https://github.com/arduino-libraries/ArduinoHttpClient
    version: ^0.6.1

This is my platformio.ini file

[env:esp32s3dev]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip
board = esp32-s3-devkitc-1
framework = arduino

upload_speed = 921600
monitor_speed = 115200

board_build.mcu = esp32s3
board_build.cpu = esp32s3
board_build.f_cpu = 240000000L

board_build.flash_mode = dio
board_build.flash_freq = 80m
board_build.flash_size = 16MB

board_build.partitions = partitions.csv

lib_ldf_mode = deep+
lib_deps = 
    ESP32Time@^2.0.6
    ArduinoHttpClient@^0.6.1
    ArduinoJson@^7.4.1
    GxEPD2@^1.6.4
    U8g2_for_Adafruit_GFX@^1.8.0
    Adafruit GFX Library@^1.12.1

build_flags =
  ; ---- Arduino USB Config ----
  -D ARDUINO_USB_CDC_ON_BOOT=1
  -D ARDUINO_USB_MODE=1                 ; USB Mode: CDC + JTAG
  -D CONFIG_ARDUINO_RUNNING_CORE=1
  -D CONFIG_ARDUINO_EVENT_RUNNING_CORE=1
   ; Debug level: none
  -D CORE_DEBUG_LEVEL=0  
  ; ---- Secure Boot V2 ----
  -D CONFIG_SECURE_BOOT=y
  -D CONFIG_SECURE_BOOT_V2_ENABLED=y
  -D CONFIG_SECURE_SIGNED_BINARIES=y
  -D CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y
  -D CONFIG_SECURE_BOOT_V2_EFUSE_KEY_BLOCK_KEY0=y
  -D CONFIG_SECURE_BOOT_V2_EFUSE_KEY_DIS_WRITE_PROTECT=y
  ; ---- Flash Encryption ----
  -D CONFIG_FLASH_ENCRYPTION_ENABLED=n
  -D FLASH_ENCRYPTION_ENABLED=0
    ; ---- NVS Encryption ----
  -D CONFIG_NVS_ENCRYPTION=n
  ; ---- Bootloader Config ----
  ; -D CONFIG_BOOTLOADER_LOG_LEVEL=0

upload_protocol = esptool

is always a bad choice. It generates more issues than it solve. Change your libs and setup to work in mode chain

Adding in the libraries as both idf_component and lib_deps is weird, try chosing one or the other.