Repository navigation
Description
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 writesbuild/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-assetssetsprerelease: trueunconditionally, 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-provenanceon the archives and the manifest, sogh attestation verifyworks. 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*-esp32release from now on hasfirmware-manifest.jsonandfirmware-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.jsonintorelease-staging/and the innerSHA256SUMS.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-provenancefor archives and manifest -
prereleasedriven by an input or tag convention;channelin 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".
Activity
feat(firmware): revoke releases via REVOKED_VERSIONS and document fla…
feat(firmware): sign release manifests outside GitHub, mirroring rufl…