DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
SECURE_BOOT

Secure Boot & Flash Encryption (ESP32 hardening)

This guide explains how to protect a device running DeterministicESPAsyncWebServer against a physically present attacker - one who can read the SPI flash, attach a debugger, or re-flash the chip - using the two ESP32 hardware features that close those gaps: Secure Boot v2 (only firmware you signed will run) and Flash Encryption (the flash is unreadable without the per-device key fused into the chip). The library's own crypto (TLS, SSH, SNMPv3, JWT/OIDC, the hash-chained audit log) defends the network; Secure Boot and Flash Encryption defend the silicon.

‍⚠️ Everything here burns eFuses, which is irreversible. A wrong key, a lost key, or the wrong mode permanently bricks the chip or locks you out. Practice on a sacrificial board, read the official docs end to end, and keep signing keys backed up offline. This is an ESP-IDF / espefuse.py / espsecure.py workflow; it is configured at the bootloader level, not from an Arduino sketch.

What is at risk without it

The library keeps these secrets/assets in flash; without Flash Encryption they can be dumped with esptool.py read_flash, and without Secure Boot the firmware can be replaced with a malicious build:

Asset Where it lives Protected by
Firmware image (your logic + keys) app partition Secure Boot + Flash Encryption
SSH RSA host private key NVS (ssh_host_key / priv_der) Flash Encryption (+NVS enc)
TLS server key & certificate flash / NVS (your storage) Flash Encryption
SNMPv3 USM auth/priv keys, JWT secret flash / NVS / build constants Flash Encryption
WiFi credentials, config blobs NVS (config_store) Flash Encryption (+NVS enc)
Audit-log chain (services/audit_log) RAM ring (+ your sink) see "Audit log" below

The SSH private key is already loaded to the stack and wiped after use (ssh_rsa.h), so it never rests in static RAM - Flash Encryption protects the at-rest NVS copy, completing the chain.

Secure Boot v2 - only your firmware runs

Secure Boot v2 makes the chip's ROM verify a digital signature on the bootloader, and the bootloader verify the signature on the app, on every boot. The public key's SHA-256 digest is fused into the chip; the private signing key stays on your build machine.

  • Algorithm: RSA-3072 (PSS) on all Secure-Boot-v2 chips; ECDSA (P-256/P-384) is also available on ESP32-S3/C-series. (Original ESP32 needs silicon revision v3 / ECO3 for v2; earlier revisions only have the older AES-based v1 - prefer a v2-capable chip.)
  • Up to two public-key digests can be fused, so you can rotate signing keys once before the slots are locked.
# 1. Generate a signing key (KEEP THIS OFFLINE AND BACKED UP).
espsecure.py generate_signing_key --version 2 secure_boot_signing_key.pem
# 2. Enable in the project config and build (ESP-IDF):
idf.py menuconfig # Security features -> Enable hardware Secure Boot in bootloader
idf.py build
# 3. First flash burns the digest eFuse; subsequent boots verify signatures.
idf.py flash

After Secure Boot is active the chip refuses any image not signed by your key - including a re-flashed development build - so you must sign every future update with the same key.

Flash Encryption - the flash is unreadable

Flash Encryption transparently encrypts the bootloader, app, and partitions marked encrypted with a key generated on the device and stored in a read/write protected eFuse block (it never leaves the chip). A flash dump is then ciphertext.

  • Cipher: AES-256-XTS (ESP32-S2/S3/C-series) or AES-256 (original ESP32).
  • Development mode: the device can still be re-flashed and the key is usable by the host tool - convenient for bring-up, not for production.
  • Release mode: plaintext flashing is permanently disabled and the key is read-protected. One-way. Use this only on units you are shipping.
idf.py menuconfig # Security features -> Enable flash encryption on boot
# (choose Release mode for production)
idf.py flash # first boot encrypts in place; thereafter flash is ciphertext

Encrypt NVS too (protects the stored keys)

Flash Encryption does not automatically encrypt the NVS partition. To protect the SSH host key and config_store blobs, enable NVS encryption: add an nvs_keys partition (itself protected by Flash Encryption) and initialize NVS with nvs_flash_secure_init(). See the ESP-IDF "NVS Encryption" guide.

Recommended production posture

  1. Secure Boot v2 + Flash Encryption (Release mode) together - neither is sufficient alone (Secure Boot still leaves secrets readable; Flash Encryption alone still runs unsigned code).
  2. NVS encryption for the SSH host key, TLS key, and credentials.
  3. Disable debug / recovery surfaces via eFuse: JTAG (espefuse.py burn_efuse DIS_PAD_JTAG / soft-disable), and UART download mode or Secure Download Mode (ENABLE_SECURITY_DOWNLOAD) so the bootloader cannot be used to read memory.
  4. Build-time: ship a release build with **DETWS_ENABLE_DIAG off** (it exposes the compile-time config), prefer HTTPS/WSS (DETWS_ENABLE_TLS) and mTLS (DETWS_ENABLE_MTLS) for control planes, and gate state-changing routes (auth + DETWS_ENABLE_CSRF).

Audit log under a physical attacker

The hash chain in services/audit_log makes on-device tampering detectable, but an attacker who holds the Flash Encryption key could in principle recompute the chain. For true off-device tamper-evidence, install a sink (detws_audit_set_sink) that forwards every record - with its chain hash - to an append-only remote store (syslog / an HTTP log service / a write-once SD file) the device cannot later rewrite. Secure Boot + Flash Encryption raise the bar to recover that key; the remote sink is what survives if it is ever breached.

Caveats with the Arduino core

The Arduino-ESP32 core is built on ESP-IDF, but Secure Boot and Flash Encryption are bootloader/eFuse settings, not sketch options. To use them you either build via ESP-IDF (with Arduino as a component) or supply a custom sdkconfig / partition + bootloader to your PlatformIO build, then burn eFuses with espefuse.py. The application code in this library is unchanged either way - these features protect the image and the flash beneath it.

References