feat: OTA firmware updates (A/B partitions, signed manifests, wss) #11

Merged
troed merged 19 commits from devel into main 2026-10-10 17:35:30 +02:00
Owner

OTA firmware updates: A/B partitions, signed manifests, wss

Implements the communicator-esp32 side of the OTA design
(starfleet/crew:docs/specs/2026-09-10-ota-design.md, §7-8) — plan 3 of 4
(crew, computer, communicator-esp32, communicator-sailfish).

The device can now be updated over the air: it downloads a signed release
from crew over HTTPS, verifies an Ed25519 signature over the manifest and a
SHA-256 over the image, installs into the inactive A/B slot, and lets the
bootloader roll back if the new app does not confirm.

What this adds

  • A/B partition table (partitions_ota16m.csv): otadata + two 3 MB app
    slots (ota_0/ota_1) + the existing mww model partition (5000K). The
    previous single-slot table is kept as partitions_sr16m.csv for recovery.
    CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y.
  • Signed-manifest verification (main/ota.c): HTTPS fetch of
    manifest.json + manifest.sig; the Ed25519 signature is checked over the
    exact manifest bytes before any flash write. The vendored
    components/ed25519/ provides the verify (this ESP-IDF's mbedTLS has no
    Ed25519). The image is streamed in chunks into the update slot while a
    SHA-256 is accumulated; size + digest must match the signed manifest before
    the slot is marked bootable.
  • Anti-downgrade policy: an update is only installed when the target is
    newer than both the running firmware and an NVS high-water mark, and only
    when the URL is https://.
  • Boot confirmation: a PENDING_VERIFY app is marked valid on the first
    IDLE entry or after 120 s; otherwise the bootloader rolls back.
  • wss + device identity (main/ws_client.c): the realtime connection
    uses wss:// with the embedded local CA, and reports
    ?device_id=<mac>&device_type=esp32&firmware_version=<v> so crew can
    schedule updates.
  • Trust material tooling: tools/gen_ota_trust.sh (regenerates
    main/ota_trust.h from gitignored tools/ota-keys/),
    tools/release_firmware.sh (builds, signs, publishes to crew), and a
    CONFIG_WS_TLS toggle with CONFIG_APP_PROJECT_VER.

One-time migration (existing devices)

The partition layout changed, so each device needs a one-time USB reflash —
see the "One-time migration to A/B OTA" section added to README.md
(erase-flash + idf.py flash + tools/flash_models.sh). After that,
updates are over the air.

Notes

  • esp_app_update does not exist in this ESP-IDF; the install uses
    esp_ota_begin/write/end directly.
  • The mww model partition is untouched by app OTA and still updates via
    tools/flash_models.sh.
  • The plan/spec describe project(... VERSION ...); the implementation uses
    CONFIG_APP_PROJECT_VER (release-time override), recorded in
    sdkconfig.defaults.

Rollout

Keys, the USB baseline flash and the TLS cutover are operational steps from
spec §10 and happen after this lands.

# OTA firmware updates: A/B partitions, signed manifests, wss Implements the communicator-esp32 side of the OTA design (`starfleet/crew:docs/specs/2026-09-10-ota-design.md`, §7-8) — plan 3 of 4 (crew, computer, communicator-esp32, communicator-sailfish). The device can now be updated over the air: it downloads a signed release from crew over HTTPS, verifies an Ed25519 signature over the manifest and a SHA-256 over the image, installs into the inactive A/B slot, and lets the bootloader roll back if the new app does not confirm. ## What this adds - **A/B partition table** (`partitions_ota16m.csv`): `otadata` + two 3 MB app slots (`ota_0`/`ota_1`) + the existing `mww` model partition (5000K). The previous single-slot table is kept as `partitions_sr16m.csv` for recovery. `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`. - **Signed-manifest verification** (`main/ota.c`): HTTPS fetch of `manifest.json` + `manifest.sig`; the Ed25519 signature is checked over the exact manifest bytes **before** any flash write. The vendored `components/ed25519/` provides the verify (this ESP-IDF's mbedTLS has no Ed25519). The image is streamed in chunks into the update slot while a SHA-256 is accumulated; size + digest must match the signed manifest before the slot is marked bootable. - **Anti-downgrade policy**: an update is only installed when the target is newer than both the running firmware and an NVS high-water mark, and only when the URL is `https://`. - **Boot confirmation**: a `PENDING_VERIFY` app is marked valid on the first IDLE entry or after 120 s; otherwise the bootloader rolls back. - **wss + device identity** (`main/ws_client.c`): the realtime connection uses `wss://` with the embedded local CA, and reports `?device_id=<mac>&device_type=esp32&firmware_version=<v>` so crew can schedule updates. - **Trust material tooling**: `tools/gen_ota_trust.sh` (regenerates `main/ota_trust.h` from gitignored `tools/ota-keys/`), `tools/release_firmware.sh` (builds, signs, publishes to crew), and a `CONFIG_WS_TLS` toggle with `CONFIG_APP_PROJECT_VER`. ## One-time migration (existing devices) The partition layout changed, so each device needs a one-time USB reflash — see the "One-time migration to A/B OTA" section added to `README.md` (erase-flash + `idf.py flash` + `tools/flash_models.sh`). After that, updates are over the air. ## Notes - `esp_app_update` does not exist in this ESP-IDF; the install uses `esp_ota_begin/write/end` directly. - The `mww` model partition is untouched by app OTA and still updates via `tools/flash_models.sh`. - The plan/spec describe `project(... VERSION ...)`; the implementation uses `CONFIG_APP_PROJECT_VER` (release-time override), recorded in `sdkconfig.defaults`. ## Rollout Keys, the USB baseline flash and the TLS cutover are operational steps from spec §10 and happen after this lands.
- copy the manifest sha256 into a local buffer before cJSON_Delete so the
  comparison no longer reads freed memory; fail closed on a non-64-char sha
- reject OTA schedules whose url is not https, and warn when CONFIG_WS_TLS
  is off that plaintext transport is dev-only
- raise the ota task stack to 16384 for the install path's buffers + TLS
- guard the protocol OTA-schedule decode against a failed strdup
- derive the public key from signing.key and compare against signing.pub
  before building; a mismatch aborts the release
- build in a fresh build dir with a fresh SDKCONFIG and the defaults fragment
  (semicolon-separated, the form IDF v5.5 accepts) so a stale repo sdkconfig
  cannot leak in
- assert the generated sdkconfig carries the requested version, WS_TLS and the
  ota16m partition table, and that the linked image's app version matches
- use curl --fail-with-body so crew rejecting a release fails the script
- document why project(... VERSION ...) was not used
troed merged commit 9c52bb637f into main 2026-10-10 17:35:30 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
starfleet/communicator-esp32!11
No description provided.