Firmware Architecture¶
Espilon-Firmware (v0.2.1, ESP-IDF v5.3.2) is a hollow shell: the binary contains zero offensive capability at rest. WiFi credentials and the Management Protocol server address live encrypted in NVS, no module is stored in flash, and a power cycle wipes RAM back to a clean slate. Every capability arrives as a signed ELF module loaded into IRAM (or PSRAM on the S3) over an encrypted channel.
Components¶
| Component | Role |
|---|---|
main/espilon.c |
Boot sequence (NVS, crypto, ESPM, transport) |
components/crypto/ |
HKDF key derivation, ChaCha20-Poly1305 |
components/comm/ |
C2 transport, dispatch, command registries, WiFi, GPRS, OTA, loader |
components/espm/ |
The ESPM dynamic module loader (copied from espilon-test/ESPM) |
components/nanopb/ |
Protobuf C2 message schema and codec |
Boot sequence¶
flowchart TD
A["app_main()"] --> B["init_nvs()"]
B --> C["crypto_init()<br/>read master_key, derive 3 keys via HKDF,<br/>zeroize master_key from RAM"]
C --> D["nvs_config_load()<br/>decrypt WiFi + server config"]
D --> E["espm_registry_init()"]
E --> F["espilon_cmd_registry_init()<br/>static + dynamic command registry"]
F --> G["espm_persist_autoload()<br/>reload persisted modules"]
G --> H["delay 1200 ms<br/>radio stabilization"]
H --> I["com_init()<br/>WiFi STA or GPRS/PPP -> C2 client"]
crypto_init() reads the device master_key from the immutable fctry NVS partition, derives three keys with HKDF-SHA256, then immediately zeroizes master_key from RAM. See the security model for the key hierarchy.
Key hierarchy¶
A single per-device master_key derives three independent keys:
| Derived key | Algorithm | Use |
|---|---|---|
session_key |
HKDF-SHA256, info espilon-c2-v1 |
ChaCha20-Poly1305 transport |
signing_key |
HKDF-SHA256, info espm-sign |
HMAC-SHA256 module verification |
config_key |
HKDF-SHA256, info espilon-cfg-v1 |
Encrypted NVS config |
master_key is never transmitted and is wiped from RAM right after derivation.
Command registries¶
Commands are dispatched after a frame is decrypted and decoded. There are two registries:
- Static registry (11 built-ins):
status,reboot,cmds,ota_update,ota_status,mod_chunk,mod_load,mod_stop,mod_list,mod_purge,mod_unpersist. - Dynamic registry (up to 32 slots): commands registered by loaded modules via
cmd_register. Module commands cannot shadow static command names. Onmod_stop, a module's commands are removed automatically.
flowchart LR
RX["TCP line"] --> B64["base64 decode"] --> DEC["ChaCha20-Poly1305 decrypt + verify"]
DEC -->|"auth fail"| DROP["drop silently"]
DEC --> PB["protobuf decode"] --> DSP{"dispatch"}
DSP --> ST["static command"]
DSP --> DY["dynamic (module) command"]
DSP --> UNK["unknown -> error"]
Transports¶
- WiFi STA (
CONFIG_NETWORK_WIFI) - station mode, exponential-backoff reconnect, C2 client task starts onIP_EVENT_STA_GOT_IP. - GPRS/PPP (
CONFIG_NETWORK_GPRS) - one generic binary for all cellular boards; modem pins and APN read from NVS at runtime. C2 client task starts onIP_EVENT_PPP_GOT_IP. See GPRS boards.
Module loading¶
Modules load in two stages: mod_chunk streams the signed ELF into a staging buffer (supports out-of-order chunks), then mod_load verifies the HMAC, relocates the ELF for the chip architecture, allocates IRAM/PSRAM, and starts the module task. See the ESPM lifecycle.
BLE is on-demand¶
BLE is never initialized at boot. NimBLE starts the first time a module calls ble_controller_init(). Initializing it at boot would race the high-priority NimBLE task against command registration and corrupt the registry. Per-chip BLE availability is covered in supported boards.
Panic isolation¶
On Xtensa (ESP32, ESP32-S3) a module fault is caught, mapped to the owning module, and that module alone is killed; the device keeps running. On RISC-V (ESP32-C6, C3) an unhandled fault reboots the device.