Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ sdkconfig.old
__pycache__/
.cache/

# Signing keys
keys/
*.pem

# Test binaries and build directories
test_kfp
test_native_frost
Expand Down
17 changes: 15 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
157 changes: 157 additions & 0 deletions docs/SECURE_BOOT.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions partitions.secureboot.csv
Original file line number Diff line number Diff line change
@@ -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,
142 changes: 142 additions & 0 deletions scripts/sign_firmware.sh
Original file line number Diff line number Diff line change
@@ -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 <command>"
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
34 changes: 34 additions & 0 deletions sdkconfig.defaults.secureboot
Original file line number Diff line number Diff line change
@@ -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