Skip to content

ESPM Security

ESPM's module security rests on four pillars: integrity verification before a module runs, isolation while it runs, deterministic erasure when it stops, and constrained capabilities throughout. Together they ensure unsigned code never executes and no module trace survives a power cycle.

Signature verification

Every module blob is [32-byte HMAC-SHA256][ELF]. Before relocation the firmware verifies the tag with a key derived from the device's master_key:

sign_key = HKDF-SHA256(master_key, info="espm-sign")
verify HMAC-SHA256(sign_key, elf) == tag

Because the signing key is the device key, a blob is bound to one device. Verification is controlled by CONFIG_ESPM_VERIFY_ENABLE (on by default) and must stay enabled in production. A signature mismatch returns ESPM_ERR_SIGNATURE and the module is never loaded.

ELF validation

After the signature passes, espm_validate parses the object and rejects anything malformed: bad ELF magic or class, wrong machine type (must be EM_XTENSA or EM_RISCV), wrong file type (must be ET_REL), too many sections, or code/data over the limits (64 KB .text, 32 KB .data+.bss). Unknown relocation types and unresolved symbols also fail the load.

Execution isolation

flowchart TB
    M["Module task"] --> WD["Per-module watchdog<br/>30 s, polled 500 ms"]
    M --> PANIC{"Fault?"}
    PANIC -->|"Xtensa"| KILL["Kill module only<br/>device keeps running"]
    PANIC -->|"RISC-V"| REBOOT["Device reboots"]
  • Per-module watchdog. Each module runs in its own FreeRTOS task with an independent watchdog (default 30 s, polled every 500 ms). A module that stops feeding it is killed.
  • Panic isolation (Xtensa). A module-aware exception handler maps a faulting program counter to the owning module and purges only that module. Firmware faults still restart the device. On RISC-V there is no exception interception, so a module fault reboots the device.

Capability constraints

  • Modules cannot allocate executable memory (MALLOC_CAP_EXEC is denied), so code can only enter IRAM through the verified ELF loader.
  • GPIO 6-11 are blocked on the ESP32 classic because they drive the internal SPI flash bus.
  • Modules reach firmware services only through the syscall table; they do not link against arbitrary ESP-IDF symbols.

Memory erasure

On unload (clean stop, watchdog timeout, or panic purge) the module's IRAM and PSRAM are overwritten with zeros before being freed. No payload bytes remain in RAM, and because nothing is written to flash, a power cycle returns the device to a clean hollow shell.

Persistence trade-off

If built with CONFIG_ESPM_PERSIST_ENABLE, a module can be saved to encrypted NVS and auto-loaded at boot. This is the one path by which a module survives a power cycle; leave it disabled unless persistence is a requirement, and remember that persisted modules are stored under the encrypted NVS config.

See also