Skip to content

Firmware releases: publish a signed release manifest with flash offsets and a stable channel #2394

Open

Description

@proffesor-for-testing

Summary

The RuView mobile and Mac apps now flash ESP32 nodes from official v*-esp32 releases. They verify each archive against the release's SHA256SUMS.txt and each image against the archive's inner SHA256SUMS.txt (cognitum-one/ruview-mobile#94).

That proves integrity, not origin. A replaced release asset and its replaced checksum file would still pass. The apps also have to hardcode chip-specific flash offsets, because the bundles don't carry them.

This issue proposes one machine-readable, signed manifest per firmware release. With it, clients can accept any future release without an app update and get offsets, channel and revocations from the release itself.

Why now

  • Firmware ships often. v0.8.14 → v0.8.17 shipped in about three weeks. Clients can't pin versions or digests.
  • C5 needs different offsets. Its bootloader is at 0x2000. The build already writes build/flasher_args.json (firmware-ci.yml, step "Verify built application version, chip, and flash layout"), but the published bundle doesn't include it. Clients that infer offsets per chip are the exact failure mode the C5 release notes warn about.
  • Every release is a prerelease. publish-release-assets sets prerelease: true unconditionally, so clients can't offer a "stable" default.
  • No build provenance. Release assets have no GitHub artifact attestations (the attestations API returns 404 for the v0.8.16 C6 archive digest).

Proposal

1. Include flasher_args.json in every variant bundle

Copy build/flasher_args.json into release-staging/ next to the images and add it to the inner SHA256SUMS.txt. This is low risk and immediately useful, even before signing.

2. Publish firmware-manifest.json per release

Generate it in publish-release-assets from the exact-run artifacts:

{
  "schema": "ruview.firmware.release-manifest.v1",
  "version": "0.8.17",
  "tag": "v0.8.17-esp32",
  "channel": "prerelease",
  "sourceCommit": "<sha>",
  "workflowRun": "<run url>",
  "minClient": { "ruview-mobile": "<optional semver>" },
  "revokes": ["0.8.15"],
  "variants": [
    {
      "id": "c6-4mb",
      "chip": "esp32c6",
      "flashSizeMb": 4,
      "display": false,
      "archive": { "name": "esp32-csi-node-firmware-c6-4mb.tar.gz", "sha256": "<hex>", "bytes": 665867 },
      "images": [
        { "role": "bootloader", "file": "bootloader.bin", "offset": "0x0", "sha256": "<hex>", "bytes": 19456 },
        { "role": "partition-table", "file": "partition-table-c6.bin", "offset": "0x8000", "sha256": "<hex>", "bytes": 3072 },
        { "role": "ota-data", "file": "ota_data_initial.bin", "offset": "<from flasher_args>", "sha256": "<hex>", "bytes": 8192 },
        { "role": "application", "file": "esp32-csi-node-c6.bin", "offset": "<from flasher_args>", "sha256": "<hex>", "bytes": 1064048 }
      ],
      "qualification": "experimental | ci-only | hardware-qualified"
    }
  ]
}

Offsets come from each variant's flasher_args.json, never hand-written. (The offsets above are illustrative; the real values come from that file.) qualification lets clients say honestly what a variant has been tested on, e.g. C5 "experimental".

3. Sign it

  • Signature for clients: an Ed25519 detached signature, firmware-manifest.json.sig, made with a key held only as a release-environment secret (environment: with required reviewers on the publish job). Clients embed the public key, never a version. Key rotation is handled by publishing the next public key in a manifest signed by the current key.
  • Provenance for humans and CI: also run actions/attest-build-provenance on the archives and the manifest, so gh attestation verify works. Mobile clients won't verify Sigstore bundles in-app; that's why the Ed25519 signature exists.

4. Channel

Make prerelease follow the tag or a workflow input, instead of true unconditionally, and mirror it in channel. Clients default to the latest stable and treat prerelease as opt-in.

Done when

  • Every v*-esp32 release from now on has firmware-manifest.json and firmware-manifest.json.sig (plus attestations).
  • Every variant bundle contains flasher_args.json, and the manifest's offsets match it. A CI check compares them.
  • A verification script in the repo (e.g. scripts/verify-firmware-manifest.*) checks the signature, archive hashes, inner hashes and offsets against a downloaded release. It's run in the publish job before upload.
  • The channel can be stable, and the README documents how a release is promoted.
  • Docs: the manifest schema, key fingerprint, rotation procedure, and how to revoke a bad release.

Work items

  • Copy flasher_args.json into release-staging/ and the inner SHA256SUMS.txt
  • Manifest generator in publish-release-assets (from exact-run artifacts)
  • Release-environment Ed25519 key, signing step and published public key with fingerprint
  • actions/attest-build-provenance for archives and manifest
  • prerelease driven by an input or tag convention; channel in the manifest
  • Verification script, plus a CI check that it passes before publish
  • Schema and key docs

Client side (for context)

cognitum-one/ruview-mobile#94 / PR #100 already lists releases, downloads, checks the release SHA256SUMS.txt and the inner checksums, and extracts safely. It has an interface seam where a manifest verifier with the embedded public key replaces the checksum-only verifier. Until then the app labels downloads "checksum verified against the official GitHub release", never "signed".

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions