■odorici.sys

$ cat ~/work/signed-firmware-updates.md

project · security · 2026

Fail-Safe Signed Firmware Updates

A secure bootloader and OTA path for an STM32 nobody can reach

A secure bootloader and fail-safe update system for an STM32G474: ECDSA-signed images, A/B flash banks executed in place, trial boot with automatic rollback, anti-rollback, and power-cut testing on a simulated flash. Boot, verification and updates over UART are working and verified on hardware; the CAN link and the gateway are next.

fig.01 · signature
kind
project
category
security
period
Sep 2026 - Present
status
in progress

head -n 20 README

Problem

Once the node is in the field you cannot patch it by hand. Power can die mid-flash, a signed image can still be broken, and the gateway or link can be hostile.

What's working

Bootloader verifies bounds, hash, key id, ECDSA P-256, and anti-rollback before every jump. A/B slots, trial boot with rollback, UART updates with resume. 13/13 bad images rejected on target. Host flash simulator plus power-fail sweeps. Fixed a flash read-while-write erratum; 150 clean updates after.

What's next

CAN at 500 kbit/s with ISO-TP, then the ESP32-S3 gateway (Wi-Fi fetch, staging, per-node state). Out of scope for now: debug lockdown, fault injection, encryption.

grep -n highlights

  1. 01Bootloader that verifies bounds, hash, key id, ECDSA P-256 signature and anti-rollback floor before every jump; 13 of 13 broken images rejected on target with the expected reason.
  2. 02A/B slots executed in place across the two flash banks, so a version switch is one boot-record write and the running image is never touched.
  3. 03Append-only, CRC-protected boot state designed around the ECC double-word limits of the G474 flash, with an exhaustive clean and torn power-cut sweep over every flash operation of an update.
  4. 04Trial boot with three recorded attempts, watchdog supervision of every task, confirmation only after a healthy status exchange, and a 60-second deadline; both rollback paths verified on the board.
  5. 05Updates over UART in 1 KB CRC-checked chunks with resume across a reset: a 30.5 KB image transfers in 6.0 s, verifies on the node in 2.0 s, and is confirmed about 16 s after the transfer starts.
  6. 06Found and fixed a one-in-twenty update failure by logging to CCM SRAM across resets: a documented silicon erratum in flash read-while-write, confirmed by 150 consecutive clean updates after the driver change.
  7. 07CI on GCC and Clang with and without sanitizers, a host verifier cross-checked against the signing tool on every image, and a job that fails the build if a private key is ever committed.

cat spec.toml

[threats]

  • -Power loss at any instant, including in the middle of an erase or a program operation
  • -Firmware that is correctly signed but does not work
  • -A compromised gateway, link, or both

[flash]

  • -60 KB bootloader region; 4 KB boot state as a ping-pong pair of pages
  • -Slot A and Slot B, 192 KB each, one per flash bank, executed in place
  • -512-byte image header, program linked at slot + 0x200, TLV trailer with SHA-256, key id and raw r||s ECDSA P-256 signature

[node]

  • -Bootloader: start the independent watchdog, verify, select a slot, jump; no transport code, no RTOS
  • -Boot record: 32-byte snapshot, CRC over the state, commit word programmed last, newest valid record wins
  • -Slot states EMPTY, PENDING, TRIAL, CONFIRMED and REJECTED; distinct rejection reasons for corruption, forgery, wrong slot and downgrade
  • -FreeRTOS application with static allocation; watchdog fed only by a supervisor task once every registered task has reported a heartbeat

[verification]

  • -Flash simulator: erase to 0xFF, 64-bit double-word programming, no reprogramming, torn writes fault on read
  • -Power cuts enumerated by operation number, clean and torn, with the state checked after every simulated reboot
  • -Negative tests: bad magic, four header variants, bad hash, four signature variants, wrong-slot image, downgrade; host and on-target
  • -On-target update tests: both directions with confirmation, refused downgrade, refused update during a trial, both rollback paths

[measured]

  • -ECDSA P-256 verify at the 16 MHz reset clock: 1.7 s; worst-case verification of a full 192 KB slot: 2.6 s
  • -Update over UART at 115200 baud: 30.5 KB image in 6.0 s, 2.0 s verification, about 16 s to a confirmed image
  • -150 consecutive updates, about 1.2 million flash operations, with no fault after the read-while-write fix

[status]

  • -Done and verified on hardware: host tooling and simulator, boot core, on-device verification, updates over UART with resume and anti-rollback
  • -Next: classic CAN at 500 kbit/s with ISO-TP on both ends, then the ESP32-S3 gateway with Wi-Fi fetch, staging and per-node state
  • -Out of scope by design: debug-port lockdown (a provisioning step), fault injection (deferred), firmware encryption (a stretch goal with known prerequisites)
■ 12 modules · 2 roles∿█