Skip to main content

CPS Firmware Signing and Secure Boot

This runbook covers how AuroraSOC signs firmware, gates uploads to signed images only, and the deliberate steps to enable hardware secure boot and flash encryption on the devices that support them. See ADR 052 for the rationale and the full algorithm set.

Irreversible steps

Enabling ESP32 Secure Boot v2 / flash encryption burns eFuses permanently, and STM32 RDP changes can mass-erase or permanently lock the debug port. None of this is wired into the normal build. Apply it only when you mean to, on the exact part, having read this page.

Algorithms

LayerAlgorithm
AuroraSOC firmware bundle / OTA signatureEd25519 (RFC 8032)
Firmware image hash / measured bootSHA-256
ESP32 Secure Boot v2 image signatureECDSA P-256 (or RSA-3072)
ESP32 flash encryptionAES-256-XTS
Device attestation identity (ATECC608A)ECDSA P-256
TransportTLS 1.3 mutual TLS

1. Generate the signing key (one-time)

just fw gen-signing-key # writes infra/keys/firmware/firmware_signing.{key,pub}

The private .key is secret: keep it in Vault or an HSM, never in git (infra/keys/ is git-ignored). Point the backend at the public key:

export FIRMWARE_SIGNING_PUBKEY_PATH=/path/to/firmware_signing.pub

Embed the matching raw public key into the ESP OTA path by replacing FIRMWARE_SIGNING_PUBKEY in firmware/embassy-esp/crates/ota-manager/src/lib.rs, so a device verifies an OTA image's Ed25519 signature before applying it (OtaManager::finalize_download_signed).

2. Build, sign, and upload

just fw bin-stm32f401-door # builds stm32f401-door-node.bin
just fw sign embassy-stm32/target/thumbv7em-none-eabihf/release/stm32f401-door-node.bin

This produces stm32f401-door-node.bin.sig (hex Ed25519 signature). In the console open Firmware → Upload FW, choose the .bin, attach the .bin.sig, set board family and version, and upload. The backend (POST /api/v1/firmware/upload) verifies the signature and rejects anything unsigned or tampered. The image SHA-256 becomes the approved golden hash; a device whose measured-boot hash matches it shows Approved.

3. Measured-boot attestation (STM32F401)

The door node hashes its own flash image at boot (SHA-256 over [0x08000000, end-of-image), which equals sha256(firmware.bin)) and reports it in its status payload. The backend records the first hash as the trusted baseline (trust-on-first-use), shows the device trusted, and raises a firmware_tampering alert flipping it to drift_detected if the hash ever changes. This is the mitigation for parts without a hardware root of trust.

4. STM32F401 readout protection (RDP)

The STM32F401 has no hardware secure boot. RDP blocks debug-port readback:

tools/scripts/firmware/stm32_rdp.sh status
tools/scripts/firmware/stm32_rdp.sh set-level1 --i-understand-this-erases-on-revert

Level 1 is reversible but reverting mass-erases the flash (secrets are wiped, not leaked). Level 2 is permanent and the script refuses to set it.

5. ESP32-S3 / C3 Secure Boot v2 + flash encryption

Use the config fragment firmware/embassy-esp/secure-boot/sdkconfig.secureboot (ECDSA P-256 secure boot v2 + AES-256-XTS flash encryption). The deliberate, eFuse-burning steps:

# Generate the secure-boot signing key (keep secret, like the Ed25519 key).
espsecure.py generate_signing_key --version 2 --scheme ecdsa256 secure_boot_signing_key.pem

# Sign the application image.
espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem \
--output app-signed.bin app.bin

# Burn secure boot + flash encryption eFuses (IRREVERSIBLE, per device).
espefuse.py burn_key BLOCK_KEY0 secure_boot_key_digest.bin SECURE_BOOT_DIGEST0
espefuse.py burn_efuse SECURE_BOOT_EN 1
espefuse.py burn_efuse SPI_BOOT_CRYPT_CNT 1

After this the part boots only AuroraSOC-signed images and its flash is AES-256-XTS encrypted. Validate on a sacrificial dev board before any fielded unit. Keep SECURE_BOOT_ALLOW_JTAG=n and ROM download mode disabled on production parts.