From 7b5ebc126377c3a090805baa75e8d80f312b8c4a Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Thu, 22 Jan 2026 11:36:59 -0400 Subject: [PATCH 1/4] Add ESP-IDF Secure Boot v2 support --- .gitignore | 4 + SECURITY.md | 17 +++- docs/SECURE_BOOT.md | 158 ++++++++++++++++++++++++++++++++++ partitions.secureboot.csv | 8 ++ scripts/sign_firmware.sh | 145 +++++++++++++++++++++++++++++++ sdkconfig.defaults.secureboot | 23 +++++ 6 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 docs/SECURE_BOOT.md create mode 100644 partitions.secureboot.csv create mode 100755 scripts/sign_firmware.sh create mode 100644 sdkconfig.defaults.secureboot diff --git a/.gitignore b/.gitignore index 2af3b37..d61f598 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ sdkconfig.old __pycache__/ .cache/ +# Signing keys +keys/ +*.pem + # Test binaries and build directories test_kfp test_native_frost diff --git a/SECURITY.md b/SECURITY.md index 2ed8fd2..1f59314 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -189,14 +189,27 @@ Before submitting PRs that touch security-sensitive code: - [ ] No timing side channels in security-critical comparisons - [ ] Error messages do not leak sensitive information +## Secure Boot (Optional) + +ESP-IDF Secure Boot v2 can be enabled for production deployments: + +- RSA-3072/PSS signature verification of bootloader and application +- Anti-rollback protection via eFuse version counter +- Optional flash encryption for firmware at rest +- See `docs/SECURE_BOOT.md` for implementation details + +Build with secure boot: +```bash +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.secureboot" build +``` + ## Known Limitations -- No secure boot (not tamper-evident) -- No flash encryption at rest (relies on AES-256-GCM layer) - MAC address readable, used in key derivation - PIN not rate-limited at hardware level; a weak or short PIN provides no meaningful protection against offline brute-force when flash can be extracted - Single-threaded, no concurrent request handling +- Secure boot requires careful key management (key loss = bricked device) ## Reporting Vulnerabilities diff --git a/docs/SECURE_BOOT.md b/docs/SECURE_BOOT.md new file mode 100644 index 0000000..c24e955 --- /dev/null +++ b/docs/SECURE_BOOT.md @@ -0,0 +1,158 @@ +# Secure Boot v2 + +ESP-IDF Secure Boot v2 implementation for hardware wallet firmware verification. + +## Overview + +Secure Boot v2 provides: +- RSA-3072/PSS signature verification of bootloader and application +- Hardware-enforced boot chain verification +- Anti-rollback protection via eFuse version counter +- Optional flash encryption for data at rest + +## Quick Start + +### 1. Generate Signing Key + +```bash +./scripts/sign_firmware.sh generate-key +``` + +This creates `keys/secure_boot_signing_key.pem` (RSA-3072 private key). + +### 2. Build with Secure Boot + +```bash +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.secureboot" build +``` + +### 3. Sign Firmware + +```bash +./scripts/sign_firmware.sh sign-all +``` + +### 4. Flash to Device (First Time) + +First boot burns the public key hash to eFuse: + +```bash +esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash \ + 0x0 build/bootloader/bootloader-signed.bin \ + 0xc000 build/partition_table/partition-table.bin \ + 0xd000 build/ota_data_initial.bin \ + 0x10000 build/keep-signed.bin +``` + +## Key Management + +### Security Requirements + +- Store the signing key offline in multiple secure locations +- Use hardware security modules (HSM) for production +- Never commit keys to version control +- Loss of key = inability to update firmware on locked devices + +### Key Backup Procedure + +1. Generate key on air-gapped machine +2. Create encrypted backups to multiple USB drives +3. Store in geographically separate secure locations +4. Test recovery procedure before production deployment + +### Production Workflow + +For production builds: + +1. CI builds unsigned firmware +2. Signing occurs on secure, offline workstation with HSM +3. Signed binaries uploaded to release + +## Anti-Rollback + +The `CONFIG_BOOTLOADER_APP_SECURE_VERSION` setting tracks firmware version in eFuse. The bootloader refuses to boot firmware with a lower version number. + +To increment version for a security-critical update: + +``` +CONFIG_BOOTLOADER_APP_SECURE_VERSION=2 +``` + +Each increment burns an eFuse bit. ESP32-S3 supports 32 version increments. + +## Flash Encryption (Optional) + +For additional protection of firmware at rest, enable flash encryption in `sdkconfig.defaults.secureboot`: + +``` +CONFIG_SECURE_FLASH_ENC_ENABLED=y +CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y +``` + +Considerations: +- Encryption key burned to eFuse on first boot +- Cannot read flash contents without key +- Increases boot time slightly +- Requires all flash contents to be encrypted + +## Verification + +Verify signatures before flashing: + +```bash +./scripts/sign_firmware.sh verify +``` + +Verify eFuse configuration on device: + +```bash +espefuse.py --port /dev/ttyACM0 summary +``` + +## Recovery + +If secure boot is enabled and the signing key is lost: +- Device cannot be updated +- No recovery possible +- This is by design for security + +If bootloader is corrupted but key is available: +- Reflash signed bootloader via UART (if not disabled) +- Or use USB DFU if supported + +## CI Integration + +For automated builds without secure boot (development): + +```yaml +- name: Build firmware + run: idf.py build +``` + +For signed release builds: + +```yaml +- name: Build firmware (unsigned) + run: idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.secureboot" build + +- name: Sign firmware + run: ./scripts/sign_firmware.sh sign-all + env: + SIGNING_KEY: ${{ secrets.SECURE_BOOT_KEY_PATH }} +``` + +## Troubleshooting + +### "Secure boot key not found" + +Ensure key exists at `keys/secure_boot_signing_key.pem` or set `SIGNING_KEY` environment variable. + +### "Signature verification failed" + +- Key mismatch: firmware signed with different key than device expects +- Corrupted binary: re-download or rebuild +- Wrong version: anti-rollback may prevent older firmware + +### "eFuse already programmed" + +Secure boot configuration is permanent. The device is locked to the programmed key. diff --git a/partitions.secureboot.csv b/partitions.secureboot.csv new file mode 100644 index 0000000..8dc129a --- /dev/null +++ b/partitions.secureboot.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0xd000, 0x3000, +otadata, data, ota, 0x10000, 0x2000, +phy_init, data, phy, 0x12000, 0x1000, +factory, app, factory, 0x20000, 0x2F0000, +storage, data, 0x40, 0x310000, 0x10000, +policy, data, 0x41, 0x320000, 0x1000, +checkpoint,data,0x42, 0x321000, 0x7000, diff --git a/scripts/sign_firmware.sh b/scripts/sign_firmware.sh new file mode 100755 index 0000000..d86cfbd --- /dev/null +++ b/scripts/sign_firmware.sh @@ -0,0 +1,145 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="${PROJECT_DIR}/build" +KEYS_DIR="${PROJECT_DIR}/keys" +SIGNING_KEY="${SIGNING_KEY:-${KEYS_DIR}/secure_boot_signing_key.pem}" + +usage() { + echo "Usage: $0 " + echo "" + echo "Commands:" + echo " generate-key Generate new RSA-3072 signing key" + echo " sign-app Sign application binary" + echo " sign-bootloader Sign bootloader binary" + echo " sign-all Sign both bootloader and application" + echo " verify Verify signatures on built binaries" + echo "" + echo "Environment:" + echo " SIGNING_KEY Path to signing key (default: keys/secure_boot_signing_key.pem)" + exit 1 +} + +check_espsecure() { + if ! command -v espsecure.py &> /dev/null; then + echo "Error: espsecure.py not found. Ensure ESP-IDF is activated." + exit 1 + fi +} + +generate_key() { + check_espsecure + mkdir -p "$KEYS_DIR" + + if [ -f "$SIGNING_KEY" ]; then + echo "Error: Key already exists at $SIGNING_KEY" + echo "Remove it first if you want to generate a new key." + exit 1 + fi + + echo "Generating RSA-3072 signing key..." + espsecure.py generate_signing_key --version 2 "$SIGNING_KEY" + chmod 600 "$SIGNING_KEY" + + echo "Key generated: $SIGNING_KEY" + echo "CRITICAL: Back up this key securely. Loss means inability to update firmware." +} + +sign_app() { + check_espsecure + + if [ ! -f "$SIGNING_KEY" ]; then + echo "Error: Signing key not found at $SIGNING_KEY" + echo "Run '$0 generate-key' first." + exit 1 + fi + + local app_bin="${BUILD_DIR}/keep.bin" + local signed_bin="${BUILD_DIR}/keep-signed.bin" + + if [ ! -f "$app_bin" ]; then + echo "Error: Application binary not found at $app_bin" + echo "Build the project first with: idf.py build" + exit 1 + fi + + echo "Signing application..." + espsecure.py sign_data --version 2 --keyfile "$SIGNING_KEY" \ + --output "$signed_bin" "$app_bin" + + echo "Signed application: $signed_bin" +} + +sign_bootloader() { + check_espsecure + + if [ ! -f "$SIGNING_KEY" ]; then + echo "Error: Signing key not found at $SIGNING_KEY" + echo "Run '$0 generate-key' first." + exit 1 + fi + + local bootloader_bin="${BUILD_DIR}/bootloader/bootloader.bin" + local signed_bin="${BUILD_DIR}/bootloader/bootloader-signed.bin" + + if [ ! -f "$bootloader_bin" ]; then + echo "Error: Bootloader binary not found at $bootloader_bin" + echo "Build the project first with: idf.py build" + exit 1 + fi + + echo "Signing bootloader..." + espsecure.py sign_data --version 2 --keyfile "$SIGNING_KEY" \ + --output "$signed_bin" "$bootloader_bin" + + echo "Signed bootloader: $signed_bin" +} + +verify_signatures() { + check_espsecure + + local app_bin="${BUILD_DIR}/keep-signed.bin" + local bootloader_bin="${BUILD_DIR}/bootloader/bootloader-signed.bin" + + echo "Verifying signatures..." + + if [ -f "$app_bin" ]; then + echo "Verifying application..." + espsecure.py verify_signature --version 2 --keyfile "$SIGNING_KEY" "$app_bin" + else + echo "Warning: Signed application not found" + fi + + if [ -f "$bootloader_bin" ]; then + echo "Verifying bootloader..." + espsecure.py verify_signature --version 2 --keyfile "$SIGNING_KEY" "$bootloader_bin" + else + echo "Warning: Signed bootloader not found" + fi + + echo "Verification complete." +} + +case "${1:-}" in + generate-key) + generate_key + ;; + sign-app) + sign_app + ;; + sign-bootloader) + sign_bootloader + ;; + sign-all) + sign_bootloader + sign_app + ;; + verify) + verify_signatures + ;; + *) + usage + ;; +esac diff --git a/sdkconfig.defaults.secureboot b/sdkconfig.defaults.secureboot new file mode 100644 index 0000000..cc75a26 --- /dev/null +++ b/sdkconfig.defaults.secureboot @@ -0,0 +1,23 @@ +# Secure Boot v2 for ESP32-S3 +CONFIG_SECURE_BOOT=y +CONFIG_SECURE_BOOT_V2_ENABLED=y +CONFIG_SECURE_SIGNED_ON_BOOT=y +CONFIG_SECURE_SIGNED_APPS=y +CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME=y +CONFIG_SECURE_BOOT_SIGNING_KEY="keys/secure_boot_signing_key.pem" + +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_OFFSET=0xc000 +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.secureboot.csv" + +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y +CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y +CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 + +# CONFIG_SECURE_FLASH_ENC_ENABLED=y +# CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y + +CONFIG_SECURE_DISABLE_ROM_DL_MODE=y +CONFIG_SECURE_BOOT_INSECURE=n + +CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT=n From 4ad3a01e32d70386a161d8612ea181edb7830f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 22 Jan 2026 14:49:31 -0500 Subject: [PATCH 2/4] fix: address security review feedback --- scripts/sign_firmware.sh | 3 +-- sdkconfig.defaults.secureboot | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/sign_firmware.sh b/scripts/sign_firmware.sh index d86cfbd..e6f90ef 100755 --- a/scripts/sign_firmware.sh +++ b/scripts/sign_firmware.sh @@ -40,8 +40,7 @@ generate_key() { fi echo "Generating RSA-3072 signing key..." - espsecure.py generate_signing_key --version 2 "$SIGNING_KEY" - chmod 600 "$SIGNING_KEY" + (umask 077; espsecure.py generate_signing_key --version 2 "$SIGNING_KEY") echo "Key generated: $SIGNING_KEY" echo "CRITICAL: Back up this key securely. Loss means inability to update firmware." diff --git a/sdkconfig.defaults.secureboot b/sdkconfig.defaults.secureboot index cc75a26..7c7f34d 100644 --- a/sdkconfig.defaults.secureboot +++ b/sdkconfig.defaults.secureboot @@ -5,8 +5,10 @@ CONFIG_SECURE_SIGNED_ON_BOOT=y CONFIG_SECURE_SIGNED_APPS=y CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME=y CONFIG_SECURE_BOOT_SIGNING_KEY="keys/secure_boot_signing_key.pem" +CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=n CONFIG_PARTITION_TABLE_CUSTOM=y +# 0xc000 offset (vs default 0x8000) accommodates larger signed bootloader CONFIG_PARTITION_TABLE_OFFSET=0xc000 CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.secureboot.csv" @@ -14,6 +16,10 @@ CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 +# Flash encryption disabled: firmware already protects secrets with AES-256-GCM. +# Enabling adds complexity (encrypted OTA, recovery difficulty) with limited +# additional security benefit for this use case. Enable if threat model requires +# protection against flash readout attacks. # CONFIG_SECURE_FLASH_ENC_ENABLED=y # CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y From 61d0e8a32970c228df6de1acf5909ab718f57c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 22 Jan 2026 14:51:38 -0500 Subject: [PATCH 3/4] refactor: simplify signing script and fix docs --- docs/SECURE_BOOT.md | 3 +-- partitions.secureboot.csv | 2 +- scripts/sign_firmware.sh | 22 ++++++++++------------ sdkconfig.defaults.secureboot | 2 -- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/docs/SECURE_BOOT.md b/docs/SECURE_BOOT.md index c24e955..a1971ce 100644 --- a/docs/SECURE_BOOT.md +++ b/docs/SECURE_BOOT.md @@ -40,8 +40,7 @@ First boot burns the public key hash to eFuse: esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash \ 0x0 build/bootloader/bootloader-signed.bin \ 0xc000 build/partition_table/partition-table.bin \ - 0xd000 build/ota_data_initial.bin \ - 0x10000 build/keep-signed.bin + 0x20000 build/keep-signed.bin ``` ## Key Management diff --git a/partitions.secureboot.csv b/partitions.secureboot.csv index 8dc129a..e6902cd 100644 --- a/partitions.secureboot.csv +++ b/partitions.secureboot.csv @@ -5,4 +5,4 @@ phy_init, data, phy, 0x12000, 0x1000, factory, app, factory, 0x20000, 0x2F0000, storage, data, 0x40, 0x310000, 0x10000, policy, data, 0x41, 0x320000, 0x1000, -checkpoint,data,0x42, 0x321000, 0x7000, +checkpoint, data, 0x42, 0x321000, 0x7000, diff --git a/scripts/sign_firmware.sh b/scripts/sign_firmware.sh index e6f90ef..6a1d68a 100755 --- a/scripts/sign_firmware.sh +++ b/scripts/sign_firmware.sh @@ -29,6 +29,14 @@ check_espsecure() { fi } +check_signing_key() { + if [ ! -f "$SIGNING_KEY" ]; then + echo "Error: Signing key not found at $SIGNING_KEY" + echo "Run '$0 generate-key' first." + exit 1 + fi +} + generate_key() { check_espsecure mkdir -p "$KEYS_DIR" @@ -48,12 +56,7 @@ generate_key() { sign_app() { check_espsecure - - if [ ! -f "$SIGNING_KEY" ]; then - echo "Error: Signing key not found at $SIGNING_KEY" - echo "Run '$0 generate-key' first." - exit 1 - fi + check_signing_key local app_bin="${BUILD_DIR}/keep.bin" local signed_bin="${BUILD_DIR}/keep-signed.bin" @@ -73,12 +76,7 @@ sign_app() { sign_bootloader() { check_espsecure - - if [ ! -f "$SIGNING_KEY" ]; then - echo "Error: Signing key not found at $SIGNING_KEY" - echo "Run '$0 generate-key' first." - exit 1 - fi + check_signing_key local bootloader_bin="${BUILD_DIR}/bootloader/bootloader.bin" local signed_bin="${BUILD_DIR}/bootloader/bootloader-signed.bin" diff --git a/sdkconfig.defaults.secureboot b/sdkconfig.defaults.secureboot index 7c7f34d..2bc93a3 100644 --- a/sdkconfig.defaults.secureboot +++ b/sdkconfig.defaults.secureboot @@ -24,6 +24,4 @@ CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 # CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y CONFIG_SECURE_DISABLE_ROM_DL_MODE=y -CONFIG_SECURE_BOOT_INSECURE=n - CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT=n From 6bd6f5505f0383fbf054b82bbe510f1394bcaa01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 22 Jan 2026 15:03:34 -0500 Subject: [PATCH 4/4] docs: document ROM download mode options in secureboot config --- sdkconfig.defaults.secureboot | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdkconfig.defaults.secureboot b/sdkconfig.defaults.secureboot index 2bc93a3..042d053 100644 --- a/sdkconfig.defaults.secureboot +++ b/sdkconfig.defaults.secureboot @@ -23,5 +23,12 @@ CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 # CONFIG_SECURE_FLASH_ENC_ENABLED=y # CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y +# ROM Download Mode options (mutually exclusive, choose ONE): +# - DISABLE: Burns eFuse to permanently disable ROM download. Most secure but no recovery. +# - INSECURE_ALLOW: Keeps full esptool/ROM download access. Use for development only. +# - SECURE_ROM_DL: Enables secure download mode with restrictions. Middle ground if supported. CONFIG_SECURE_DISABLE_ROM_DL_MODE=y +# CONFIG_SECURE_INSECURE_ALLOW_DL_MODE=y +# CONFIG_SECURE_ENABLE_SECURE_ROM_DL_MODE=y + CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT=n