feat(trust): verify release-root signature on the OTA manifest

check_for_updates now fetches the manifest as raw JSON and runs
trust::verify_detached before parsing: a tampered or wrong-signer
signature rejects the mirror outright, and unsigned manifests are
offered for MANUAL apply only — the 3 AM auto-apply scheduler refuses
them, closing the unattended remote-root hole (§A of the 1.8.0
hardening plan). UpdateState gains manifest_signed so the UI can
surface authenticity.

Publisher side: create-release.sh signs the manifest during the
release (ceremony, mnemonic via TTY/env only), publish-release-assets
hard-refuses to ship an unsigned manifest (grep + new 'ceremony
verify' cryptographic gate), and scripts/sign-manifest.sh covers
re-signing outside a release run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-02 12:33:01 -04:00
co-authored by Claude Fable 5
parent 1977bdefb5
commit 51647b21cd
6 changed files with 257 additions and 10 deletions
+10 -6
View File
@@ -48,12 +48,16 @@ arbitrary app catalog to the entire fleet — fully unattended under
pinned-anchor binary must actually be built + shipped for enforcement to be live on nodes;
(b) flip "accept unsigned" → "reject unsigned" only after the whole fleet is on the pinned
binary (`container/app_catalog.rs:397`, the `Unsigned` arm) — see the next item.
- [ ] 🔴 **Enforce a signature on the OTA manifest before trusting it.**
`update.rs:68` fetches `http://146.59.87.168:3000/.../manifest.json` over cleartext
and parses/trusts it with no `trust::verify_detached` call; component sha256/blake3
are only checked against that same unauthenticated manifest → remote root RCE.
Move to HTTPS + pinned cert, require an Ed25519 release-root signature, and
**refuse `auto_apply` until the anchor is pinned.**
- [~] 🔴 **Enforce a signature on the OTA manifest before trusting it.** Signature
verification LANDED 2026-07-02: `check_for_updates` now fetches raw JSON and runs
`trust::verify_detached` — a present-but-invalid/wrong-signer signature hard-rejects
the mirror; unsigned manifests are offered for MANUAL apply only (`manifest_signed`
surfaced in `UpdateState`) and **auto-apply refuses them**. Publisher side:
`create-release.sh` signs the manifest inline (ceremony), `publish-release-assets.sh`
hard-refuses to ship unsigned (grep + `ceremony verify` crypto gate), and
`scripts/sign-manifest.sh` exists for re-signs. **Still open:** move the mirror
to HTTPS + pinned cert (tracked with the next item); flip unsigned-manual-apply →
hard-reject once the fleet is on a pinned-anchor binary.
- [ ] 🔴 **Implement container image signature verification (cosign).**
`container/src/podman_client.rs:255``pull_image(.., _signature)` silently discards
the signature that the manifest threads all the way down