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..a1971ce --- /dev/null +++ b/docs/SECURE_BOOT.md @@ -0,0 +1,157 @@ +# 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 \ + 0x20000 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..e6902cd --- /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..6a1d68a --- /dev/null +++ b/scripts/sign_firmware.sh @@ -0,0 +1,142 @@ +#!/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 +} + +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" + + 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..." + (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." +} + +sign_app() { + check_espsecure + check_signing_key + + 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 + check_signing_key + + 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..042d053 --- /dev/null +++ b/sdkconfig.defaults.secureboot @@ -0,0 +1,34 @@ +# 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_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" + +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 + +# 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