Files
roro9stack/site/content/dev/decisions/0003-own-signature-check-not-secure-boot.md
T
twislaandClaude Sonnet 5.5 3b4100dc6a
CI / build (pull_request) Successful in 8m38s
Site / build (pull_request) Successful in 9s
Site: the developer docs (phase 4), with the Debug Builds and the Debug Console first
/dev/ has Debug Builds and the Debug Console (builds and the token, the
console and its protocol, files and screenshots, driving the UI, crashes and
Safe Mode, the command reference), Build, test and release (including how an
update works), the architecture decisions and the milestone plans.

Generated from the repository by site/tools/gen_dev_docs.py: the ADRs, the
milestones, the README's sections, and the command reference, read from the
firmware's own `help` text. The pages are committed (Zola cannot read outside
its folder); the Site workflow checks they are current, and now also runs
when src/main.cpp changes. M0, M1 and CONTEXT.md are not published.
README: the gnss commands that the table lacked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 21:25:33 +02:00

2.0 KiB

+++ title = "Signed Update Files checked by the firmware, not ESP32 Secure Boot" description = "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.…" weight = 3

[extra] docs = true source = "docs/adr/0003-own-signature-check-not-secure-boot.md" tag = "ADR 0003" +++ 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: the bootloader first, the firmware as a second line. Arduino-ESP32 marks a new image valid before setup() unless the sketch overrides verifyRollbackLater(), which once made every update look good and hid the bootloader's rollback (it had looked like the prebuilt bootloader ignored it). With the override, an image stays pending until Probation confirms it, and the bootloader reverts one that restarts unconfirmed, however early it crashes. The firmware also counts its own boots on Probation, very first thing in setup(), and reverts itself on the second unconfirmed start.