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.

I made many more trials !

Any idea how I can get this to work ?

Please note that I am on framework=arduino.

I am using the ESP32S3-WROOM-1U-16N (16MB Flash / no PSRAM / no OTA needed / with WiFi and Bluetooth).

Important: The WiFi module clearly needs phy_init.

I was able to find the phy_init_data here:

(so in my script, the file phy_multiple_init_data_v5.5.bin is a simple copy of the file found in the above URL).

I also tried the master branch version…

Also note that I also sign and encrypt the phy_ini_data.bin file in my script. Not sure if this is relevant.

I have the project sucessfully working for

WORKS: secure boot NO / flash encryption NO
(as long as I introduce phy_init, everything compiles and the app runs smoothly)

WORKS: secure boot YES / flash encryption NO
(as long as I introduce phy_init, everything compiles and the app runs smoothly)

DOES NOT WORK: secure boot YES / flash encryption YES

This script here shows my trial to do both (secure boot & flash encryption) :

#!/bin/bash
set -e

CHIP="esp32s3"
FLASH_MODE="dio"
FLASH_FREQ="80m"
FLASH_SIZE="16MB"
UPLOAD_BAUD="921600"

KEY_DIR="./secure"
BUILD_DIR=".pio/build/prod_secure"
BIN_DIR="./bin"
mkdir -p "$KEY_DIR" "$BIN_DIR"

BOOT_SIGN_KEY="$KEY_DIR/secure_boot_private_key.pem"
FLASH_ENC_KEY="$KEY_DIR/flash_encryption_key.bin"
BOOT_DIGEST="$KEY_DIR/secure_boot_public_key_digest.bin"

function get_port {
    echo >&2
    echo -e "${LIGHT_BLUE}------------------------------------------------------------------${NC}" >&2
    echo -e "${LIGHT_BLUE}ACTION REQUIRED: Please put the device into ROM Download Mode.${NC}" >&2
    echo -e "${LIGHT_BLUE}1. Hold the BOOT/IO0 button.${NC}" >&2
    echo -e "${LIGHT_BLUE}2. While holding BOOT, press and release the RESET/EN button.${NC}" >&2
    echo -e "${LIGHT_BLUE}3. Release the BOOT button.${NC}" >&2
    echo -e "${LIGHT_BLUE}Press Enter when the device is in Download Mode...${NC}" >&2
    read -r  # wait for Enter

    DEFAULT_PORT=$(ls /dev/cu.usbmodem* 2>/dev/null | head -n 1 || true)
    echo -n "Enter serial port (press Enter for $DEFAULT_PORT): " >&2
    read -r input_port
    if [ -n "$input_port" ]; then
        echo "$input_port"
    else
        echo "$DEFAULT_PORT"
    fi
}

echo "🔥 Clean build..."
rm -rf .pio
pio run --environment prod_secure

# Generate Secure Boot signing key if missing
if [ ! -f "$BOOT_SIGN_KEY" ]; then
    echo "🔑 Generating Secure Boot Primary Signing Key..."
    espsecure generate-signing-key --version 2 "$BOOT_SIGN_KEY"
else
    echo "🔑 Secure Boot signing key exists."
fi

# Generate flash encryption key if missing
if [ ! -f "$FLASH_ENC_KEY" ]; then
  echo "🔐 Generating flash encryption key..."
  espsecure generate-flash-encryption-key "$FLASH_ENC_KEY"
else
  echo "🔐 Flash encryption key already exists."
fi

# Generate Secure Boot public key digest
if [ ! -f "$BOOT_DIGEST" ]; then
  echo "📥 Generating secure boot public key digest..."
  espsecure digest-sbv2-public-key --keyfile $BOOT_SIGN_KEY --output $BOOT_DIGEST
fi

# Sign bootloader, partitions, phy_init_data and firmware binaries
echo "📝 Signing binaries with Secure Boot key..."
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/bootloader-signed.bin" "$BUILD_DIR/bootloader.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/partitions-signed.bin" "$BUILD_DIR/partitions.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/phy_init_data-signed.bin" "$KEY_DIR/phy_multiple_init_data_v5.5.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/firmware-signed.bin" "$BUILD_DIR/firmware.bin"

# Encrypt the binaries for flash encryption
echo "🔐 Encrypting binaries for Flash Encryption..."
espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x0      --output "$BIN_DIR/bootloader-signed-encrypted.bin"    "$BIN_DIR/bootloader-signed.bin"
espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x8000   --output "$BIN_DIR/partitions-signed-encrypted.bin"    "$BIN_DIR/partitions-signed.bin"
espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0xF000   --output "$BIN_DIR/phy_init_data-signed-encrypted.bin" "$BIN_DIR/phy_init_data-signed.bin"
espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x20000  --output "$BIN_DIR/firmware-signed-encrypted.bin"      "$BIN_DIR/firmware-signed.bin"

DOWNLOAD_PORT=$(get_port)
[ -n "$DOWNLOAD_PORT" ] || { echo "Port empty."; exit 1; }

esptool --chip "$CHIP" --port "$DOWNLOAD_PORT" --baud "$UPLOAD_BAUD" --no-stub write-flash -z \
    --flash-mode "$FLASH_MODE" --flash-freq "$FLASH_FREQ" --flash-size "$FLASH_SIZE" \
    0x0      "$BIN_DIR/bootloader-signed-encrypted.bin" \
    0x8000   "$BIN_DIR/partitions-signed-encrypted.bin" \
    0xF000   "$BIN_DIR/phy_init_data-signed-encrypted.bin" \
    0x20000  "$BIN_DIR/firmware-signed-encrypted.bin"

BOOT_DIGEST="$KEY_DIR/secure_boot_public_key_digest.bin"
espsecure digest-sbv2-public-key --keyfile "$BOOT_SIGN_KEY" --output "$BOOT_DIGEST"

DOWNLOAD_PORT=$(get_port)

# Burn Secure Boot digest
echo "🔥 Burning Secure Boot digest..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-key BLOCK_KEY2 "$BOOT_DIGEST" SECURE_BOOT_DIGEST0

# Enable Secure Boot
echo "🔒 Enabling Secure Boot..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-efuse SECURE_BOOT_EN

# Burn Flash Encryption key to BLOCK_KEY0 (only once!)
echo "🔥 Burning Flash Encryption key to BLOCK_KEY0..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-key BLOCK_KEY0 "$FLASH_ENC_KEY" XTS_AES_128_KEY

# Enable Flash Encryption
echo "🔒 Enabling Flash Encryption..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-efuse SPI_BOOT_CRYPT_CNT 1

echo "✅ Device is now production-secured with Secure Boot and Flash Encryption!"

My partitions.csv file:

# Name,     Type,   SubType,    Offset,     Size,     Flags
nvs,        data,   nvs,        0x9000,     0x6000,   encrypted
phy_init,   data,   phy,        0xF000,     0x1000,   
app0,       app,    ota_0,      0x20000,    0x7D0000, encrypted
spiffs,     data,   spiffs,     0x800000,   0x7E0000, encrypted
coredump,   data,   coredump,   0xFE0000,   0x20000,

And my platformio.ini file

[env:prod_secure]
platform = espressif32@6.7.0
framework = arduino
board = esp32s3-wroom-1u-n16
board_build.partitions = partitions.csv
board_build.bootloader_addr = 0x0
upload_speed = 921600
monitor_speed = 115200

lib_deps = 
    ESP32Time@^2.0.6    
    arduino-libraries/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

; PRODUCTION MODE — Secure Boot V2 + Flash Encryption + all keys burned
; Irreversible configuration — only signed/encrypted firmware will run
build_flags =
  ; -- USB Configuration --
  -D ARDUINO_USB_CDC_ON_BOOT=1
  -D ARDUINO_USB_DFU_ON_BOOT=0
  -D ARDUINO_USB_FW_MSC_ON_BOOT=0
  ; -- Hardware Configuration --
  -D CONFIG_JTAG_DISABLED=1
  -D CONFIG_ZIGBEE_ENABLED=0
  ; -- Secure Boot V2 Configuration --
  -D CONFIG_SECURE_BOOT_V2_ENABLED=1
  -D CONFIG_SECURE_SIGNED_BINARIES=1
  -D CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=0
  -D CONFIG_SECURE_BOOT_V2_EFUSE_KEY_BLOCK_KEY2=1
  ; -- Flash Encryption: ENABLED for development (Mode 3) --
  -D CONFIG_SECURE_FLASH_ENC_ENABLED=1
  -D CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=0
  -D CONFIG_FLASH_ENCRYPTION_ENABLED=1
  -D CONFIG_SPI_FLASH_ENCRYPTION_DEFAULT_KEY0=1
  ; For production (irreversible):
  -D CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=1
  -D CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=0
  ; -- Address Configuration --
  -D CONFIG_PARTITION_TABLE_OFFSET=0x8000
  -D CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0
  -D CONFIG_APP_OFFSET=0x20000
  ; -- Logging --
  -D CORE_DEBUG_LEVEL=4
  ; -- Performance --
  -D ARDUINO_RUNNING_CORE=1
  -D ARDUINO_EVENT_RUNNING_CORE=1

This won’t work!!! See the answer maxgerhard gave to you:

In short: The precompiled ESP-IDF in the Arduino framwork does not support secure boot and flash encryption! You have to use framework = arduino, espidf and a propper set up sdkconfig for this to work!

Thank you @sivar2311 I tried to follow what you wrote.

Do you have a concrete example with an sdkconfig and sdkconfig.defaults ??

Here my trial :

my platformio.ini file

Note how I added framework = arduino, espidf

[env:prod_secure]
platform = espressif32@6.7.0
framework = arduino, espidf
board = esp32s3-wroom-1u-n16
board_build.partitions = partitions.csv
board_build.bootloader_addr = 0x0
upload_speed = 921600
monitor_speed = 115200

lib_deps = 
    ESP32Time@^2.0.6    
    arduino-libraries/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

; PRODUCTION MODE — Secure Boot V2 + Flash Encryption + all keys burned
build_flags =
    -D SDKCONFIG="sdkconfig"
    -D SDKCONFIG_DEFAULTS="sdkconfig.defaults"
    ; --- Secure Boot V2 Flags ---
    -D CONFIG_SECURE_BOOT_ENABLED=y
    -D CONFIG_SECURE_BOOT_VERSION=2
    -D CONFIG_SECURE_SIGNED_ON_BOOT=y
    -D CONFIG_SECURE_BOOT_SIGNING_KEY="\"secure/secure_boot_private_key.pem\""
    ; --- Flash Encryption Flags ---    
    -D CONFIG_FLASH_ENCRYPTION_ENABLED=y    
    -D CONFIG_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y    
    -D CONFIG_SECURE_FLASH_ENC_KEY_SRC_BLOW_EFUSE=y 
    -D CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=n    
    -D CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y

My /boards/esp32s3-wroom-1u-n16.json file :

Note how I added "frameworks": ["arduino", "espidf"],

{
  "name": "Name of Board",
  "url": "https://mycompany.com",
  "vendor": "Company Name",
  "extra": {
    "fw_version": "1.0"
  },
  "build": {
    "arduino": {
      "ldscript": "esp32s3_out.ld"
    },
    "core": "esp32",
    "variant": "esp32s3",
    "f_cpu": "240000000L",
    "f_flash": "80000000L",
    "flash_mode": "dio",
    "mcu": "esp32s3"
  },
  "connectivity": ["wifi", "bluetooth"],
  "debug": {
    "openocd_target": "esp32s3.cfg"
  },
  "frameworks": ["arduino", "espidf"],
  "upload": {
    "flash_size": "16MB",
    "maximum_ram_size": 327680,
    "maximum_size": 16777216,
    "require_upload_port": true,
    "speed": 921600
  }
}

Here my CMakeLists.txt in the root directory of my project:

cmake_minimum_required(VERSION 3.16)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS components/arduino)
project(myproject)

Inside the src folder, I have another CMakeLists.txt file

/src/CMakeLists.txt

idf_component_register(SRCS
    "main.ccp"
    "crypto_utils.cpp"
INCLUDE_DIRS ".")

I executed the following cmds

cd ~/esp/esp-idf
./install.sh
. ./export.sh

My sdkconfig.defaults file

#################################################
# ESP32-S3 Dev Module Arduino + ESP-IDF defaults
#################################################

# CPU / Core
CONFIG_FREERTOS_UNICORE=n               # Use both cores
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240

# Memory
CONFIG_SPIRAM_SUPPORT=n                 # Most Dev Modules have no external PSRAM
# CONFIG_SPIRAM_USE_MALLOC=y            # Uncomment if your board has PSRAM
# CONFIG_SPIRAM_TYPE_AUTO=y

# FreeRTOS
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y

# Wi-Fi
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y
CONFIG_ESP32_WIFI_TX_BA_WIN=6

# Partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

# Logging / Console
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_UART_NUM=0

# Flash
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
CONFIG_ESPTOOLPY_FLASHMODE="dio"

# Watchdog
CONFIG_ARDUINO_ESP32_DISABLE_WDT=y

# Other
CONFIG_ESP32S3_ENABLE_COEX=y            # Bluetooth + Wi-Fi coexistence


#######################################
# Secure Boot V2 + Flash Encryption
#######################################

CONFIG_IDF_TARGET="esp32s3dev"
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y

CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_RSA=y
CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=n

CONFIG_SECURE_BOOT_VERSION=2
CONFIG_SECURE_SIGNED_ON_BOOT=y
CONFIG_SECURE_BOOT_SIGNING_KEY="\"secure/secure_boot_private_key.pem\""

CONFIG_FLASH_ENCRYPTION_ENABLED=y
CONFIG_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_FLASH_ENC_KEY_SRC_BLOW_EFUSE=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=n
CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y

Unfortunately, no. I never used flash encryption / secure bootm

See example Add initial support for security features · platformio/platform-espressif32@ef82ab6 · GitHub

1 Like

Yes, I advanced somewhat. see sdkconfig here further down…

Remember I am working on a ESP32S3-WROOM-1U-16N
(16MB Flash / no PSRAM / no OTA required / with WiFi and Bluetooth).

framework = arduino, espidf

(I finally got this to compile !!! :slight_smile: )

However I am struggling with sdkconfig flags that jeapardize my script workflow. Right now, I am heavily corrupting my devices. (i.e. burnt fuses - but non-working application yet).

  • step1: sign all necessary firmware chunks → works!

  • step2: write-flash the signed chunks to the corresponding addresses (that match the partitions.csv) → works!

  • step 3: burn and enable Secure Boot (i.e. lock BLOCK_KEY2 and burn efuse) —> seems to work

  • step 4: burn and enale Flash Encryption (lock BLOCK_KEY0 and burn efuse) → CRASH !

Here is my script to do all this :

#!/bin/bash
set -e

CHIP="esp32s3"
FLASH_MODE="dio"
FLASH_FREQ="80m"
FLASH_SIZE="16MB"
UPLOAD_BAUD="921600"

KEY_DIR="./secure"
BUILD_DIR=".pio/build/prod_secure"
BIN_DIR="./bin"
mkdir -p "$KEY_DIR" "$BIN_DIR"

BOOT_SIGN_KEY="$KEY_DIR/secure_boot_private_key.pem"
FLASH_ENC_KEY="$KEY_DIR/flash_encryption_key.bin"
BOOT_DIGEST="$KEY_DIR/secure_boot_public_key_digest.bin"

function get_port {
    echo >&2
    echo -e "${LIGHT_BLUE}------------------------------------------------------------------${NC}" >&2
    echo -e "${LIGHT_BLUE}ACTION REQUIRED: Please put the device into ROM Download Mode.${NC}" >&2
    echo -e "${LIGHT_BLUE}1. Hold the BOOT/IO0 button.${NC}" >&2
    echo -e "${LIGHT_BLUE}2. While holding BOOT, press and release the RESET/EN button.${NC}" >&2
    echo -e "${LIGHT_BLUE}3. Release the BOOT button.${NC}" >&2
    echo -e "${LIGHT_BLUE}Press Enter when the device is in Download Mode...${NC}" >&2
    read -r  # wait for Enter

    DEFAULT_PORT=$(ls /dev/cu.usbmodem* 2>/dev/null | head -n 1 || true)
    echo -n "Enter serial port (press Enter for $DEFAULT_PORT): " >&2
    read -r input_port
    if [ -n "$input_port" ]; then
        echo "$input_port"
    else
        echo "$DEFAULT_PORT"
    fi
}

echo "🔥 Clean build..."
rm -rf .pio
pio run --environment prod_secure

# Generate Secure Boot signing key if missing
if [ ! -f "$BOOT_SIGN_KEY" ]; then
    echo "🔑 Generating Secure Boot Primary Signing Key..."
    espsecure generate-signing-key --version 2 "$BOOT_SIGN_KEY"
else
    echo "🔑 Secure Boot signing key exists."
fi

# Generate flash encryption key if missing
if [ ! -f "$FLASH_ENC_KEY" ]; then
  echo "🔐 Generating flash encryption key..."
  espsecure generate-flash-encryption-key "$FLASH_ENC_KEY"
else
  echo "🔐 Flash encryption key already exists."
fi

# Generate Secure Boot public key digest
if [ ! -f "$BOOT_DIGEST" ]; then
  echo "📥 Generating secure boot public key digest..."
  espsecure digest-sbv2-public-key --keyfile $BOOT_SIGN_KEY --output $BOOT_DIGEST
fi

# Sign bootloader, partitions, phy_init_data and firmware binaries
echo "📝 Signing binaries with Secure Boot key..."
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/bootloader-signed.bin" "$BUILD_DIR/bootloader.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/partitions-signed.bin" "$BUILD_DIR/partitions.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/phy_init_data-signed.bin" "$KEY_DIR/phy_multiple_init_data_v5.5.bin"
espsecure sign-data --version 2 --keyfile "$BOOT_SIGN_KEY" --output "$BIN_DIR/firmware-signed.bin" "$BUILD_DIR/firmware.bin"

# Encrypt the binaries for flash encryption
# echo "🔐 Encrypting binaries for Flash Encryption..."
# espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x0      --output "$BIN_DIR/bootloader-signed-encrypted.bin"    "$BIN_DIR/bootloader-signed.bin"
# espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0xB000   --output "$BIN_DIR/partitions-signed-encrypted.bin"    "$BIN_DIR/partitions-signed.bin"  
# espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x13000  --output "$BIN_DIR/phy_init_data-signed-encrypted.bin" "$BIN_DIR/phy_init_data-signed.bin"
# espsecure encrypt-flash-data --keyfile "$FLASH_ENC_KEY" --address 0x20000  --output "$BIN_DIR/firmware-signed-encrypted.bin"      "$BIN_DIR/firmware-signed.bin"

# At this stop here, I put my device in BOOT-Mode for the first time

DOWNLOAD_PORT=$(get_port)
[ -n "$DOWNLOAD_PORT" ] || { echo "Port empty."; exit 1; }

esptool --chip "$CHIP" --port "$DOWNLOAD_PORT" --baud "$UPLOAD_BAUD" --no-stub write-flash -z \
    --flash-mode "$FLASH_MODE" --flash-freq "$FLASH_FREQ" --flash-size "$FLASH_SIZE" \
    0x0      "$BIN_DIR/bootloader-signed.bin" \
    0xB000   "$BIN_DIR/partitions-signed.bin" \
    0x13000  "$BIN_DIR/phy_init_data-signed.bin" \
    0x20000  "$BIN_DIR/firmware-signed.bin"

################ ################ ################
################ ################ ################
#  UNTIL HERE MY SCRIPT WORKS WELL
################ ################ ################
################ ################ ################

# At this stop here, I put my device back in BOOT-Mode

DOWNLOAD_PORT=$(get_port)
[ -n "$DOWNLOAD_PORT" ] || { echo "Port empty."; exit 1; }

################ ################ ################
#  This step works
################ ################ ################
# Burn Secure Boot digest
echo "🔥 Burning Secure Boot digest..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-key BLOCK_KEY2 "$BOOT_DIGEST" SECURE_BOOT_DIGEST0

################ ################ ################
#  This step works
################ ################ ################
# Enable Secure Boot
echo "🔒 Lock Secure Boot..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-efuse SECURE_BOOT_EN

################ ################ ################
#  THIS STEP CRASHES
################ ################ ################
# Burn Flash Encryption key to BLOCK_KEY0 (only once!)
echo "🔥 Burning Flash Encryption key to BLOCK_KEY0..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-key BLOCK_KEY0 "$FLASH_ENC_KEY" XTS_AES_128_KEY

# Enable Flash Encryption
echo "🔒 Lock Flash Encryption..."
espefuse --chip "$CHIP" --port "$DOWNLOAD_PORT" --do-not-confirm burn-efuse SPI_BOOT_CRYPT_CNT 1

echo "✅ Device is now production-secured with Secure Boot and Flash Encryption!"

Here is the error message

Hard resetting via RTS pin...

------------------------------------------------------------------
ACTION REQUIRED: Please put the device into ROM Download Mode.
1. Hold the BOOT/IO0 button.
2. While holding BOOT, press and release the RESET/EN button.
3. Release the BOOT button.
Press Enter when the device is in Download Mode...

Enter serial port (press Enter for /dev/cu.usbmodem213201): 
🔥 Burning Secure Boot digest...
espefuse v5.0.2
Connecting...

=== Run "burn-key" command ===
Sensitive data will be hidden (see --show-sensitive-info)
Burn keys to blocks:
 - BLOCK_KEY2 -> [?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??]
        'KEY_PURPOSE_2': 'USER' -> 'SECURE_BOOT_DIGEST0'.
        Disabling write to 'KEY_PURPOSE_2'...
        Disabling write to key block...


Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
[00] BLOCK0               is not empty
        (written ): 0x00000000000000020070000009c00000000f000000800100
        (to write): 0x000000000000000000000009000000000000000002000400
        (coding scheme = NONE)
[06] BLOCK_KEY2           is empty, will burn the new value
. 
This is an irreversible operation!
BURN BLOCK6  - OK (write block == read block)
BURN BLOCK0  - OK (all write block bits are set)
Reading updated eFuses...
Successful.
🔒 Lock Secure Boot...
espefuse v5.0.2
Connecting...

=== Run "burn-efuse" command ===
The eFuses to burn:
  from BLOCK0
     - SECURE_BOOT_EN

Burning eFuses:
    - 'SECURE_BOOT_EN' (Set this bit to enable secure boot) 0b1 -> 0b1
        The same value for SECURE_BOOT_EN is already burned. Do not change the eFuse.


Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
Nothing to burn, see messages above.
Checking eFuses...
Successful.
🔥 Burning Flash Encryption key to BLOCK_KEY0...
espefuse v5.0.2
Connecting...

=== Run "burn-key" command ===
Sensitive data will be hidden (see --show-sensitive-info)
Burn keys to blocks:
 - BLOCK_KEY0 -> [?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??]
        Reversing byte order for AES-XTS hardware peripheral...

A fatal error occurred:         New value contains some bits that cannot be cleared (value will be 0xaeef6fd7868ffa3f6febffff14fafeff5fdfbfffae6b67beb3789fdfb3fde7e5)(use '--force-write-always' option to ignore it)

Here is my sdkconfig.defaults

#################################################
# ESP32-S3-WROOM-1U-N16 Configuration
# 16MB Flash, No PSRAM, WiFi+BT, Arduino+ESP-IDF
#################################################

# === CPU CONFIGURATION ===
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
CONFIG_FREERTOS_UNICORE=n                   # Use both cores

# === MEMORY CONFIGURATION ===
CONFIG_SPIRAM_SUPPORT=n                     # No PSRAM on this variant
CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE_32KB=y
CONFIG_ESP32S3_DATA_CACHE_SIZE_64KB=y
CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE_64B=y

# === FREERTOS CONFIGURATION ===
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y

# === FLASH CONFIGURATION ===
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_ESPTOOLPY_AFTER_RESET=y

# === PARTITION TABLE ===
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0xB000

# === DISABLE OTA ===
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=n
CONFIG_APP_ROLLBACK_ENABLE=n

# === WIFI CONFIGURATION ===
CONFIG_ESP32_WIFI_ENABLED=y
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y
CONFIG_ESP32_WIFI_TX_BA_WIN=6
CONFIG_ESP32_WIFI_RX_BA_WIN=6
CONFIG_ESP32_WIFI_NVS_ENABLED=y
CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP32_WIFI_MGT_SBUF_NUM=32

# === BLUETOOTH CONFIGURATION ===
CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y

# === WIFI + BLUETOOTH COEXISTENCE ===
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE=n

# === MBEDTLS / TLS CONFIGURATION ===
CONFIG_MBEDTLS_TLS_CLIENT_MODE=y
CONFIG_MBEDTLS_TLS_SERVER_MODE=y
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y
CONFIG_MBEDTLS_PSK_MODES=y
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=16384

# === USB CONFIGURATION ===
# Note: ESP-IDF USB Console requires single core mode
# Choose ONE of the following options:

# OPTION 1: Dual Core + UART Console (Recommended)
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_USB_CDC=n
# Disable USB CDC to avoid single-core requirement
CONFIG_ARDUINO_USB_CDC_ON_BOOT=n

# OPTION 2: Single Core + USB Console (Uncomment if needed)
# CONFIG_FREERTOS_UNICORE=y
# CONFIG_ARDUINO_USB_CDC_ON_BOOT=y
# CONFIG_ESP_CONSOLE_USB_CDC=y

# === LOGGING ===
CONFIG_LOG_DEFAULT_LEVEL_ERROR=y
CONFIG_LOG_DEFAULT_LEVEL=1
CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y
CONFIG_LOG_COLORS=y
CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y

# === CONSOLE ===
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200

# === WATCHDOG TIMERS ===
CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_INIT=y
CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
CONFIG_ESP_INT_WDT=y
CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
CONFIG_ESP_INT_WDT_CHECK_CPU1=y

# === SECURITY (PRODUCTION RELEASE MODE) ===
# WARNING: Once enabled, these cannot be disabled without special recovery procedures!

# === SECURE BOOT V2 ===
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOTLOADER_MODE_DEVELOPMENT=y
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
# FIXED: Updated paths to match script
CONFIG_SECURE_BOOT_SIGNING_KEY="./secure/secure_boot_private_key.pem"
CONFIG_SECURE_BOOT_VERIFICATION_KEY="./secure/secure_boot_public_key_digest.bin"
CONFIG_SECURE_BOOT_ENABLE_AGGRESSIVE_KEY_REVOKE=y  # Changed to 'y' for production
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=n               # Changed to 'n' for production
CONFIG_SECURE_BOOT_ALLOW_JTAG=n
CONFIG_SECURE_BOOT_ALLOW_EFUSE_RD_DIS=y

# === FLASH ENCRYPTION ===
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_AES_XBLOCK_512=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_DEC=n
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=y
CONFIG_SECURE_FLASH_SKIP_WRITE_PROTECTION_CACHE=n

# === EFUSE SETTINGS ===
CONFIG_EFUSE_SECURE_BOOT_KEY_DIGESTS=1
CONFIG_EFUSE_MAX_BLK_LEN=256

# === POWER MANAGEMENT ===
CONFIG_PM_ENABLE=y
CONFIG_PM_DFS_INIT_AUTO=y
CONFIG_PM_USE_RTC_TIMER_REF=y
CONFIG_PM_PROFILING=n
CONFIG_PM_TRACE=n

# === ARDUINO SPECIFIC ===
CONFIG_ARDUINO_RUNNING_CORE=1
CONFIG_ARDUINO_LOOP_STACK_SIZE=8192
CONFIG_ARDUINO_EVENT_RUNNING_CORE=1
CONFIG_ARDUINO_EVENT_STACK_SIZE=4096
CONFIG_ARDUINO_UDP_RUNNING_CORE=1
CONFIG_ARDUINO_UDP_TASK_PRIORITY=3
CONFIG_ARDUINO_ISR_IRAM=y

# === PERFORMANCE OPTIMIZATIONS ===
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=n
CONFIG_LWIP_IRAM_OPTIMIZATION=y
CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION=y

# === SYSTEM ===
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304
CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584
CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048

# === HTTP CLIENT/SERVER ===
CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y
CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y
CONFIG_HTTPD_MAX_REQ_HDR_LEN=512
CONFIG_HTTPD_MAX_URI_LEN=512

# === LWIP NETWORKING ===
CONFIG_LWIP_IP_FRAG=y
CONFIG_LWIP_IP_REASSEMBLY=y
CONFIG_LWIP_STATS=n
CONFIG_LWIP_ETHARP_TRUST_IP_MAC=y
CONFIG_LWIP_ESP_GRATUITOUS_ARP=y
CONFIG_LWIP_GARP_TMR_INTERVAL=60
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32
CONFIG_LWIP_DHCP_RESTORE_LAST_IP=y

# === FILE SYSTEM ===
CONFIG_FATFS_CODEPAGE_850=y
CONFIG_FATFS_LFN_STACK=y
CONFIG_FATFS_MAX_LFN=255

Here is my partitions.csv

# Name,    Type, SubType,  Offset,    Size,      Flags
nvs,       data, nvs,      0xC000,    0x6000,    encrypted
phy_init,  data, phy,      0x13000,   0x2000,
app0,      app,  factory,  0x20000,   0x140000,  encrypted
spiffs,    data, spiffs,   0x160000,  0xE60000,  encrypted
coredump,  data, coredump, 0xFC0000,  0x40000,   encrypted

Here my platformio.ini file

[env:prod_secure]
platform = espressif32@6.12.0
board = esp32s3-wroom-1u-n16
framework = arduino, espidf

upload_speed = 921600
monitor_speed = 115200
board_build.partitions = partitions.csv

; --- PRODUCTION SECURE BUILD FLAGS ---
build_flags =
    ; -- USB settings
    -D ARDUINO_USB_CDC_ON_BOOT=0
    -D ARDUINO_USB_MSC_ON_BOOT=0
    -D ARDUINO_USB_DFU_ON_BOOT=0    
    ; -- Security Indicators (for application code) --
    -D SECURE_BOOT_ENABLED=1
    -D FLASH_ENCRYPTION_ENABLED=1
    -D PRODUCTION_BUILD=1

Note the detail that my board is a custom definition:

/boards/esp32s3-wroom-1u-n16.json

{
    "name": "Board (ESP32-S3-WROOM-1U-N16)",
    "url": "https://mycompany.ch",
    "vendor": "Company Name",
    "extra": {
        "fw_version": "1.0"
    },
    "build": {
        "arduino": {
            "ldscript": "esp32s3_out.ld"
        },
        "core": "esp32",
        "variant": "esp32s3",
        "f_cpu": "240000000L",
        "f_flash": "80000000L",
        "flash_mode": "dio",
        "mcu": "esp32s3"
    },
    "connectivity": ["wifi", "bluetooth"],
    "debug": {
        "openocd_target": "esp32s3.cfg"
    },
    "frameworks": ["arduino", "espidf"],
    "upload": {
        "flash_size": "16MB",
        "maximum_ram_size": 409600,
        "maximum_size": 6291456,
        "require_upload_port": true,
        "speed": 921600
    }
}

After the crash, I get a summary like this:

espefuse --chip esp32s3 --port /dev/cu.usbmodem213201 summary                                         
espefuse v5.0.2
Connecting...

=== Run "summary" command ===
EFUSE_NAME (Block) Description  = [Meaningful Value] [Readable/Writeable] (Hex Value)
----------------------------------------------------------------------------------------
Calibration fuses:
K_RTC_LDO (BLOCK1)                                 BLOCK1 K_RTC_LDO                                   = -8 R/W (0b1000010)
K_DIG_LDO (BLOCK1)                                 BLOCK1 K_DIG_LDO                                   = 36 R/W (0b0001001)
V_RTC_DBIAS20 (BLOCK1)                             BLOCK1 voltage of rtc dbias20                      = 8 R/W (0x02)
V_DIG_DBIAS20 (BLOCK1)                             BLOCK1 voltage of digital dbias20                  = 68 R/W (0x11)
DIG_DBIAS_HVT (BLOCK1)                             BLOCK1 digital dbias when hvt                      = -28 R/W (0b10111)
ADC2_CAL_VOL_ATTEN3 (BLOCK1)                       ADC2 calibration voltage at atten3                 = 120 R/W (0b011110)
TEMP_CALIB (BLOCK2)                                Temperature calibration data                       = -14.0 R/W (0b110001100)
OCODE (BLOCK2)                                     ADC OCode                                          = 69 R/W (0x45)
ADC1_INIT_CODE_ATTEN0 (BLOCK2)                     ADC1 init code at atten0                           = 492 R/W (0x7b)
ADC1_INIT_CODE_ATTEN1 (BLOCK2)                     ADC1 init code at atten1                           = 112 R/W (0b011100)
ADC1_INIT_CODE_ATTEN2 (BLOCK2)                     ADC1 init code at atten2                           = 120 R/W (0b011110)
ADC1_INIT_CODE_ATTEN3 (BLOCK2)                     ADC1 init code at atten3                           = 96 R/W (0b011000)
ADC2_INIT_CODE_ATTEN0 (BLOCK2)                     ADC2 init code at atten0                           = -68 R/W (0x91)
ADC2_INIT_CODE_ATTEN1 (BLOCK2)                     ADC2 init code at atten1                           = -20 R/W (0b100101)
ADC2_INIT_CODE_ATTEN2 (BLOCK2)                     ADC2 init code at atten2                           = 56 R/W (0b001110)
ADC2_INIT_CODE_ATTEN3 (BLOCK2)                     ADC2 init code at atten3                           = 112 R/W (0b011100)
ADC1_CAL_VOL_ATTEN0 (BLOCK2)                       ADC1 calibration voltage at atten0                 = 348 R/W (0x57)
ADC1_CAL_VOL_ATTEN1 (BLOCK2)                       ADC1 calibration voltage at atten1                 = 396 R/W (0x63)
ADC1_CAL_VOL_ATTEN2 (BLOCK2)                       ADC1 calibration voltage at atten2                 = 320 R/W (0x50)
ADC1_CAL_VOL_ATTEN3 (BLOCK2)                       ADC1 calibration voltage at atten3                 = 376 R/W (0x5e)
ADC2_CAL_VOL_ATTEN0 (BLOCK2)                       ADC2 calibration voltage at atten0                 = 480 R/W (0x78)
ADC2_CAL_VOL_ATTEN1 (BLOCK2)                       ADC2 calibration voltage at atten1                 = 252 R/W (0b0111111)
ADC2_CAL_VOL_ATTEN2 (BLOCK2)                       ADC2 calibration voltage at atten2                 = 248 R/W (0b0111110)

Config fuses:
WR_DIS (BLOCK0)                                    Disable programming of individual eFuses           = 41944320 R/W (0x02800500)
RD_DIS (BLOCK0)                                    Disable reading from BlOCK4-10                     = 0 R/W (0b0000000)
DIS_ICACHE (BLOCK0)                                Set this bit to disable Icache                     = False R/W (0b0)
DIS_DCACHE (BLOCK0)                                Set this bit to disable Dcache                     = False R/W (0b0)
DIS_TWAI (BLOCK0)                                  Set this bit to disable CAN function               = False R/W (0b0)
DIS_APP_CPU (BLOCK0)                               Disable app cpu                                    = False R/W (0b0)
DIS_DIRECT_BOOT (BLOCK0)                           Disable direct boot mode                           = True R/W (0b1)
UART_PRINT_CONTROL (BLOCK0)                        Set the default UART boot message output mode      = Enable R/W (0b00)
PIN_POWER_SELECTION (BLOCK0)                       Set default power supply for GPIO33-GPIO37; set wh = VDD3P3_CPU R/W (0b0)
                                                   en SPI flash is initialized                       
PSRAM_CAP (BLOCK1)                                 PSRAM capacity                                     = None R/W (0b00)
PSRAM_TEMP (BLOCK1)                                PSRAM temperature                                  = None R/W (0b00)
PSRAM_VENDOR (BLOCK1)                              PSRAM vendor                                       = None R/W (0b00)
PSRAM_CAP_3 (BLOCK1)                               PSRAM capacity bit 3                               = False R/W (0b0)
BLOCK_USR_DATA (BLOCK3)                            User data                                         
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK_SYS_DATA2 (BLOCK10)                          System data part 2 (reserved)                     
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 

Flash fuses:
FLASH_TPUW (BLOCK0)                                Configures flash waiting time after power-up; in u = 0 R/W (0x0)
                                                   nit of ms. If the value is less than 15; the waiti
                                                   ng time is the configurable value.  Otherwise; the
                                                    waiting time is twice the configurable value     
FLASH_ECC_MODE (BLOCK0)                            Flash ECC mode in ROM                              = 16to18 byte R/W (0b0)
FLASH_TYPE (BLOCK0)                                SPI flash type                                     = 4 data lines R/W (0b0)
FLASH_PAGE_SIZE (BLOCK0)                           Set Flash page size                                = 0 R/W (0b00)
FLASH_ECC_EN (BLOCK0)                              Set 1 to enable ECC for flash boot                 = False R/W (0b0)
FORCE_SEND_RESUME (BLOCK0)                         Set this bit to force ROM code to send a resume co = False R/W (0b0)
                                                   mmand during SPI boot                             
FLASH_CAP (BLOCK1)                                 Flash capacity                                     = None R/W (0b000)
FLASH_TEMP (BLOCK1)                                Flash temperature                                  = None R/W (0b00)
FLASH_VENDOR (BLOCK1)                              Flash vendor                                       = None R/W (0b000)

Identity fuses:
DISABLE_WAFER_VERSION_MAJOR (BLOCK0)               Disables check of wafer version major              = False R/W (0b0)
DISABLE_BLK_VERSION_MAJOR (BLOCK0)                 Disables check of blk version major                = False R/W (0b0)
WAFER_VERSION_MINOR_LO (BLOCK1)                    WAFER_VERSION_MINOR least significant bits         = 2 R/W (0b010)
PKG_VERSION (BLOCK1)                               Package version                                    = 0 R/W (0b000)
BLK_VERSION_MINOR (BLOCK1)                         BLK_VERSION_MINOR                                  = 3 R/W (0b011)
WAFER_VERSION_MINOR_HI (BLOCK1)                    WAFER_VERSION_MINOR most significant bit           = False R/W (0b0)
WAFER_VERSION_MAJOR (BLOCK1)                       WAFER_VERSION_MAJOR                                = 0 R/W (0b00)
OPTIONAL_UNIQUE_ID (BLOCK2)                        Optional unique 128-bit ID                        
   = 3d a0 3d eb 22 55 22 26 20 ce 12 71 58 06 72 50 R/W 
BLK_VERSION_MAJOR (BLOCK2)                         BLK_VERSION_MAJOR of BLOCK2                        = ADC calib V1 R/W (0b01)
WAFER_VERSION_MINOR (BLOCK0)                       calc WAFER VERSION MINOR = WAFER_VERSION_MINOR_HI  = 2 R/W (0x2)
                                                   << 3 + WAFER_VERSION_MINOR_LO (read only)         
PSRAM_CAPACITY (BLOCK0)                            calc as = PSRAM_CAP_3 << 2 + PSRAM_CAP (read only) = 0 R/W (0b000)

Jtag fuses:
SOFT_DIS_JTAG (BLOCK0)                             Set these bits to disable JTAG in the soft way (od = 7 R/W (0b111)
                                                   d number 1 means disable ). JTAG can be enabled in
                                                    HMAC module                                      
DIS_PAD_JTAG (BLOCK0)                              Set this bit to disable JTAG in the hard way. JTAG = True R/W (0b1)
                                                    is disabled permanently                          
STRAP_JTAG_SEL (BLOCK0)                            Set this bit to enable selection between usb_to_jt = False R/W (0b0)
                                                   ag and pad_to_jtag through strapping gpio3 when bo
                                                   th reg_dis_usb_jtag and reg_dis_pad_jtag are equal
                                                    to 0                                             

Mac fuses:
MAC (BLOCK1)                                       MAC address                                       
   = 30:ed:a0:00:ea:54 (OK) R/W 
CUSTOM_MAC (BLOCK3)                                Custom MAC                                        
   = 00:00:00:00:00:00 (OK) R/W 

Security fuses:
DIS_DOWNLOAD_ICACHE (BLOCK0)                       Set this bit to disable Icache in download mode (b = False R/W (0b0)
                                                   oot_mode[3:0] is 0; 1; 2; 3; 6; 7)                
DIS_DOWNLOAD_DCACHE (BLOCK0)                       Set this bit to disable Dcache in download mode (  = False R/W (0b0)
                                                   boot_mode[3:0] is 0; 1; 2; 3; 6; 7)               
DIS_FORCE_DOWNLOAD (BLOCK0)                        Set this bit to disable the function that forces c = False R/W (0b0)
                                                   hip into download mode                            
DIS_DOWNLOAD_MANUAL_ENCRYPT (BLOCK0)               Set this bit to disable flash encryption when in d = False R/W (0b0)
                                                   ownload boot modes                                
SPI_BOOT_CRYPT_CNT (BLOCK0)                        Enables flash encryption when 1 or 3 bits are set  = Disable R/W (0b000)
                                                   and disabled otherwise                            
SECURE_BOOT_KEY_REVOKE0 (BLOCK0)                   Revoke 1st secure boot key                         = False R/W (0b0)
SECURE_BOOT_KEY_REVOKE1 (BLOCK0)                   Revoke 2nd secure boot key                         = True R/W (0b1)
SECURE_BOOT_KEY_REVOKE2 (BLOCK0)                   Revoke 3rd secure boot key                         = True R/W (0b1)
KEY_PURPOSE_0 (BLOCK0)                             Purpose of Key0                                    = SECURE_BOOT_DIGEST0 R/- (0x9)
KEY_PURPOSE_1 (BLOCK0)                             Purpose of Key1                                    = USER R/W (0x0)
KEY_PURPOSE_2 (BLOCK0)                             Purpose of Key2                                    = SECURE_BOOT_DIGEST0 R/- (0x9)
KEY_PURPOSE_3 (BLOCK0)                             Purpose of Key3                                    = USER R/W (0x0)
KEY_PURPOSE_4 (BLOCK0)                             Purpose of Key4                                    = USER R/W (0x0)
KEY_PURPOSE_5 (BLOCK0)                             Purpose of Key5                                    = USER R/W (0x0)
SECURE_BOOT_EN (BLOCK0)                            Set this bit to enable secure boot                 = True R/W (0b1)
SECURE_BOOT_AGGRESSIVE_REVOKE (BLOCK0)             Set this bit to enable revoking aggressive secure  = True R/W (0b1)
                                                   boot                                              
DIS_DOWNLOAD_MODE (BLOCK0)                         Set this bit to disable download mode (boot_mode[3 = False R/W (0b0)
                                                   :0] = 0; 1; 2; 3; 6; 7)                           
ENABLE_SECURITY_DOWNLOAD (BLOCK0)                  Set this bit to enable secure UART download mode   = False R/W (0b0)
SECURE_VERSION (BLOCK0)                            Secure version (used by ESP-IDF anti-rollback feat = 0 R/W (0x0000)
                                                   ure)                                              
BLOCK_KEY0 (BLOCK4)
  Purpose: SECURE_BOOT_DIGEST0
  Key0 or user data                                 
   = 60 a7 7c b1 47 9d 70 02 2e 46 2b aa 7e 3d 1a 1b 67 0e d2 14 57 dc c9 6d 3b da 05 02 c4 0f a0 04 R/- 
BLOCK_KEY1 (BLOCK5)
  Purpose: USER
               Key1 or user data                                 
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK_KEY2 (BLOCK6)
  Purpose: SECURE_BOOT_DIGEST0
  Key2 or user data                                 
   = 60 a7 7c b1 47 9d 70 02 2e 46 2b aa 7e 3d 1a 1b 67 0e d2 14 57 dc c9 6d 3b da 05 02 c4 0f a0 04 R/- 
BLOCK_KEY3 (BLOCK7)
  Purpose: USER
               Key3 or user data                                 
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK_KEY4 (BLOCK8)
  Purpose: USER
               Key4 or user data                                 
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
BLOCK_KEY5 (BLOCK9)
  Purpose: USER
               Key5 or user data                                 
   = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 

Spi Pad fuses:
SPI_PAD_CONFIG_CLK (BLOCK1)                        SPI_PAD_configure CLK                              = 0 R/W (0b000000)
SPI_PAD_CONFIG_Q (BLOCK1)                          SPI_PAD_configure Q(D1)                            = 0 R/W (0b000000)
SPI_PAD_CONFIG_D (BLOCK1)                          SPI_PAD_configure D(D0)                            = 0 R/W (0b000000)
SPI_PAD_CONFIG_CS (BLOCK1)                         SPI_PAD_configure CS                               = 0 R/W (0b000000)
SPI_PAD_CONFIG_HD (BLOCK1)                         SPI_PAD_configure HD(D3)                           = 0 R/W (0b000000)
SPI_PAD_CONFIG_WP (BLOCK1)                         SPI_PAD_configure WP(D2)                           = 0 R/W (0b000000)
SPI_PAD_CONFIG_DQS (BLOCK1)                        SPI_PAD_configure DQS                              = 0 R/W (0b000000)
SPI_PAD_CONFIG_D4 (BLOCK1)                         SPI_PAD_configure D4                               = 0 R/W (0b000000)
SPI_PAD_CONFIG_D5 (BLOCK1)                         SPI_PAD_configure D5                               = 0 R/W (0b000000)
SPI_PAD_CONFIG_D6 (BLOCK1)                         SPI_PAD_configure D6                               = 0 R/W (0b000000)
SPI_PAD_CONFIG_D7 (BLOCK1)                         SPI_PAD_configure D7                               = 0 R/W (0b000000)

Usb fuses:
DIS_USB_OTG (BLOCK0)                               Set this bit to disable USB function               = False R/W (0b0)
USB_EXCHG_PINS (BLOCK0)                            Set this bit to exchange USB D+ and D- pins        = False R/W (0b0)
USB_EXT_PHY_ENABLE (BLOCK0)                        Set this bit to enable external PHY                = False R/W (0b0)
DIS_USB_JTAG (BLOCK0)                              Set this bit to disable function of usb switch to  = True R/W (0b1)
                                                   jtag in module of usb device                      
DIS_USB_SERIAL_JTAG (BLOCK0)                       Set this bit to disable usb device                 = False R/W (0b0)
USB_PHY_SEL (BLOCK0)                               This bit is used to switch internal PHY and extern
   = internal PHY is assigned to USB Device while external PHY is assigned to USB OTG R/W (0b0)
                                                   al PHY for USB OTG and USB Device                 
DIS_USB_SERIAL_JTAG_ROM_PRINT (BLOCK0)             USB printing                                       = Enable R/W (0b0)
DIS_USB_SERIAL_JTAG_DOWNLOAD_MODE (BLOCK0)         Set this bit to disable UART download mode through = False R/W (0b0)
                                                    USB                                              
DIS_USB_OTG_DOWNLOAD_MODE (BLOCK0)                 Set this bit to disable download through USB-OTG   = False R/W (0b0)

Vdd fuses:
VDD_SPI_XPD (BLOCK0)                               SPI regulator power up signal                      = False R/W (0b0)
VDD_SPI_TIEH (BLOCK0)                              If VDD_SPI_FORCE is 1; determines VDD_SPI voltage 
   = VDD_SPI connects to 1.8 V LDO R/W (0b0)
VDD_SPI_FORCE (BLOCK0)                             Set this bit and force to use the configuration of = False R/W (0b0)
                                                    eFuse to configure VDD_SPI                       

Wdt fuses:
WDT_DELAY_SEL (BLOCK0)                             RTC watchdog timeout threshold; in unit of slow cl = 40000 R/W (0b00)
                                                   ock cycle                                         

Flash voltage (VDD_SPI) determined by GPIO45 on reset (GPIO45=High: VDD_SPI pin is powered from internal 1.8V LDO
GPIO45=Low or NC: VDD_SPI pin is powered directly from VDD3P3_RTC_IO via resistor Rspi. Typically this voltage is 3.3 V).

Any help highly appreciated. It is a real pain to burn and corrupt device after device.

Very good news: I have it fully working now !

The biggest misunderstandings I had were:

  • I had to sign AND encrypt each fw-section on the computer first
  • INCLUDING the bootloader at address 0x0 (for an esp32s3 it is 0x0)
  • then write everyting to the device’s flash (write-flash cmd with carefully paying attention to the correct fw-section addresses)
  • then burn the efuses (KEY_BLOCK0 for flash-encryption and KEY_BLOCK2 for secure-boot-digest-v2 plus many more efuses depending on the desired level of protection)

Very important: all of the above steps needed to be done with the device in the ROM-Boot-Download-Mode. And since my USB-connection only has D+ and D- I needed a bit of a “tweek” until I was able to bring my device in that state. Once in that state, the device must remain in that boot-download-mode.

Also very important: since platformio, arduino and espidf don’t like each other particularly (remember I am working with framework = arduino, espidf, I never knew if sdkconfig.defaults apply or if the build_flags in platformio.ini apply. (They fight each other). So I put all the necessary flags in BOTH.

This is how my sdkconfig.defaults finally looks (and as just said, the very same flags I also placed under build_flags inside platformio.ini file:

# My sdkconfig.defaults (and platformio.ini build_flags)
#
# to set for Secure-Boot and Flash-Encryption in Release Mode
#
# for the ESP32S3-WROOM-1U-16N (16MB Flash / no PSRAM / no OTA / with Wifi and Bluetooth)
#
#

# Core
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384

# Partition Table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0xB000

# NVS Configuration - DISABLE NVS ENCRYPTION
CONFIG_NVS_ENCRYPTION=n

# TLS (WiFiClientSecure)
CONFIG_MBEDTLS_TLS_CLIENT_MODE=y
CONFIG_MBEDTLS_TLS_SERVER_MODE=y
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y
CONFIG_MBEDTLS_PSK_MODES=y
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=16384

# Dual-core
CONFIG_FREERTOS_UNICORE=n

# UART logging
CONFIG_ARDUINO_USB_CDC_ON_BOOT=n
CONFIG_ESP_CONSOLE_USB_CDC=n
CONFIG_ESP_CONSOLE_UART_DEFAULT=y

# Secure Boot V2
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOTLOADER_MODE_RELEASE=y
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
CONFIG_SECURE_BOOT_SIGNING_KEY="${PROJECT_DIR}/secure/secure_boot_private_key.pem"
CONFIG_SECURE_BOOT_ENABLE_AGGRESSIVE_KEY_REVOKE=y
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=n
CONFIG_SECURE_BOOT_ALLOW_JTAG=n

# Flash Encryption (Release)
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y
CONFIG_SECURE_FLASH_ENCRYPTION_AES256=y
CONFIG_SECURE_DISABLE_ROM_DL_MODE=y
CONFIG_ESP32_REV_MIN_3=y
CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_AES_XBLOCK_512=y

The partitions.csv file needed the encryption flag at neuralgic position only. For example, the nvs I left it out without the flag (even tough one could also set it and have an even bigger AES-256 security. But since my flash-encryption already does AES-256 I figured, I don’t need anymore for nvs_keys. The nvs_keys are already included in that AES-256 protection !

partitions.csv

# Name,   Type, SubType,  Offset,   Size,     Flags
nvs,      data, nvs,      0xC000,   0x6000,
phy_init, data, phy,      0x13000,  0x2000,
app0,     app,  factory,  0x20000,  0x140000, encrypted
spiffs,   data, spiffs,   0x160000, 0xE60000, encrypted
coredump, data, coredump, 0xFC0000, 0x40000,

Here as a summary:

  • the firmware is signed and encrypted on the computer and then written to the esp32s3
  • after the firmware write step, the key-block efuses BLOCK_KEY0 and BLOCK_KEY2 are written (cannot be undonw)
  • after the key-blocks, more efuse are burnt (cannot be undone)
  • the key-blocks and efuses cause
    • secure boot enabled (i.e. verifies bootloader signature)
    • flash encryption enabled
    • USB JTAG interface disabled
    • JTAG via GPIOs disabled
    • Manual flash encryption blocked
    • UART download mode disabled
    • Write protection enabled (against physical attacks on device)
    • Read protection enabled (against physical attacks on device)
1 Like

Congratulations, and thanks for sharing this!

Do I understand correctly that this requires all the steps discussed above, e.g. you also have to build your own ESP-IDF?