Files
roro9stack/docs/adr/0003-own-signature-check-not-secure-boot.md
T
twislaandClaude Opus 5.5 45542dcbff OTA: the firmware rolls itself back; the bootloader doesn't
A deliberately crashing update looped forever on the device: the
prebuilt bootloader ignores ESP_OTA_IMG_PENDING_VERIFY despite the
app-side rollback config. UpdateService::bootGuard() now runs first in
setup(): it counts starts on Probation in NVS and, on the second
unconfirmed start, marks the image invalid and reboots into the
previous one. Confirming (or the Wi-Fi rollback) resets the counter.
ADR 0003 records the limit: a crash in the first milliseconds still
needs USB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-03 21:48:39 +02:00

13 lines
1.5 KiB
Markdown

# Signed Update Files checked by the firmware, not ESP32 Secure Boot
Firmware Updates are accepted only when their Update File carries a valid ECDSA P-256 signature over the image's SHA-256. The firmware itself checks it, against a public key compiled into it, before switching the boot partition. The private key lives outside the repository, in `~/.config/roro9stack/ota-key.pem`.
We chose this over the ESP32's hardware Secure Boot. Secure Boot is enforced by the chip, but it burns eFuses one-way: a mistake bricks the device, and the device can never run unsigned firmware again, which makes recovery over USB harder. On a single development device, a software check that refuses unsigned pushes is enough, and it stays reversible: a new firmware can carry a new public key.
## Consequences
- Someone with physical USB access can still flash anything. Only Wi-Fi and SD card updates are guarded.
- **Losing the private key** means the next update has to go over USB, carrying a new public key.
- P-256 rather than Ed25519, because the firmware's TLS library (mbedTLS) already verifies it, so it costs no extra code.
- **Rollback is done by the firmware, not the bootloader.** On the device, the prebuilt bootloader that PlatformIO flashes did not roll back a crashing update, even though the app's configuration enables it. So the firmware counts its own boots on Probation, very first thing in `setup()`, and reverts itself on the second unconfirmed start. A crash before that counter is written (the first few milliseconds) would not be caught: recovery is then over USB.