Skip to content

Publish and serve frozen releases with separate signing custody - #201

Merged
andersalm merged 28 commits into
developfrom
fix/186-release-trust
Oct 2, 2026
Merged

andersalm merged 28 commits into
developfrom
fix/186-release-trust

Conversation

@andersalm

@andersalm andersalm commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Runtime imports a separately signed release set against the operator-approved public DID. Builders prepare inert files, the custodian's installed signer signs approved hashes, and the publication host serves the verified signed set through Carrier and the existing gateway routes. This supports #186 and the staged Mac install/update goal in #89.

  • For canary, the protected operator policy pins exact source commit/tree and the current canonical develop head. The signer verifies branch ancestry; the operator approves the merged PR, green CI and installed signer provenance. Stable and jetson-test keep version-tag-on-main admission under Anders's channel decision.
  • For M2, --reuse-support verifies the original M1 native input before Cargo, builds Runtime and keeps support, capsules, catalogue and component bytes fixed. support_origin binds the original support receipt.
  • Publication admits signatures and held files, freezes a bounded snapshot, verifies imported CIDs and requires full-DID confirmation for an approved saved-pin change. Public pin/receipt and files promote before the head. Complete rollback removes backup links and commits the unchanged prior head again; incomplete recovery retains its evidence.
  • Gateway reads admit one verified signed set per committed receipt/head, retain file identities, check artifact hashes and refuse changed or unsafe files. Queued reads and negative caching keep the serving boundary consistent with publication repair.
  • The installer and README carry the new public DID. Existing-source re-trust preserves channel, install path, Carrier ticket and gateways. The installer verifies exact version stdout and empty stderr before activation. M1/M2 keep the agreed metadata paths and one-writer flow.

Current-head CI passed all nine checks, including 37 signer tests within 57 release-input cases, 3755 workspace cases, 895 capsule cases, both actual browser regressions and all platform jobs. Independent development PASS and operator Opus PASS cover the accepted candidate. The remaining availability, full-account pin and live-promotion notes are recorded in #186.

#167 is closed with installed proof. The current-head installed Home journey passed on this exact candidate; its actual source, installation/provider and cleanup receipts were independently inspected. The handoff documents key-free builder version/output verification, reviewed absolute Runtime/helper/provider paths, explicit staging origin and holder stamps, and signer rotation in both dry-run and import. Stable-from-develop, local-only/off-branch source, moved refs and malformed/channel-mismatch refusals remain covered.

The Mac fixture contract binds immutable paths, hashes, CIDs, source records, receipts and frozen support. The approved fixture split assigns real-maintainer positive snapshots to the operator. Anders’s full rehearsal instruction also authorizes isolated CI to prove the complete positive install/update path and refusals with disposable fixture keys. The update session owns observer/client/workflow changes. The operator owns real signing, publication, Mac install/update, custody, recovery/key removal and person-run re-trust. Normal user install/Home acceptance includes #217's signed capsule delivery gate.

The accepted baseline is integrated; current CI, development PASS, different-family operator PASS and the exact-head installed Home journey have passed. The approved PR151 process-only base change is compatible, and its current entropy gate passes on the combined tree. This PR is merged into develop; main, tags, releases and live promotion stay with Anders. The M1 operator handoff is posted with the selected-input gates and reviewed procedure. Shared recovery and consumed-metadata changes follow the separate M2-in-Home gate in #89.

Validate one canonical public DID and confirm a signer replacement before writing. Keep omitted channel, install path, ticket, gateways and delivery metadata; update derived discovery while retaining custom URIs.

Verified: 10 source tests, source-add CLI parser test, server Clippy all targets, and the four basic gates. Tests use public verification fixtures; installed restart and person-run acceptance remain separate.
@andersalm andersalm changed the title Prepare explicit release re-trust and isolated custodian signing Prepare staged Mac releases with separate signing custody Oct 2, 2026
@andersalm andersalm changed the title Prepare staged Mac releases with separate signing custody Publish and serve frozen releases with separate signing custody Oct 2, 2026
@andersalm

Copy link
Copy Markdown
Contributor Author

Independent adversarial review (Claude Opus via elastos-pr-review, 2026-10-02), head 3072f64.
VERDICT: FAIL

Blocking findings:

  • elastos/crates/elastos-server/src/api/gateway_site.rs:150 and :161, with release_publication.rs:533: every public GET re-reads and re-hashes the full signed set, a 1 KB release-head.json included. That set holds Runtime binaries plus every provider, capsule, media-tools and llama artifact for each platform, so one request can mean hundreds of MB of hashing. All of this runs behind one try_acquire permit per router, and any overlapping request gets 503 at once. install.sh:877 (and lines 906, 952, 966) uses curl -f with no retry, so it stops with "Publisher gateway unreachable". Two people installing at the same time, a monitor, or one anonymous client looping requests can block every install and gateway update, and each request costs the server far more than it costs the client. The old handlers served plain reads concurrently. test_busy_release_read_refused_health_works_and_permit_restored treats this 503 as the intended result. To fix: verify once when the publication is committed (or cache by receipt plus inode/mtime), let requests wait in a queue instead of failing fast, or add retry to the clients.

Non-blocking notes:

  1. The pin in publish-state.json sits in the same folder, owned by the same uid, as the files it protects. A compromised Runtime account can rewrite the pin and a self-signed set together, which is the exact threat Replace the maintainer release key and keep signing off the public server #186 describes. The tamper test still passes, so the safeguard catches accidental or partial changes, not an attacker on that account.
  2. When an existing publisher host upgrades, its legacy set (no installer hash, no pin) starts returning 503 on every release route. Write this down before anyone deploys to live.
  3. Smoke scripts now default to the new DID but still point at the live gateway, which serves releases signed with the old key. Run against defaults, they will fail on a signer mismatch.

Not checked: no tests or builds were run. I did not check the release-signer.py GitHub tag check end to end, the export_release_publication rollback, the --reuse-support worker, CI results, or the installed Home journey.

@andersalm

Copy link
Copy Markdown
Contributor Author

Independent adversarial review (Claude Opus via elastos-pr-review, 2026-10-02), head 3072f64.
VERDICT: FAIL

Blocking findings:

  1. elastos/crates/elastos-server/src/api/gateway_site.rs:150 and :161, together with release_publication.rs:526-530. Every unauthenticated GET re-hashes the whole signed set, even a request for the 1 KB /release-head.json. That set is every platform binary, every provider binary from components.json, the media-tools tarballs and the catalogue. The whole router has one permit, and it uses try_acquire_owned. A second request that arrives during a scan gets an immediate 503 instead of waiting. A client that requests in a loop can keep the permit held and deny the release endpoint to everyone, with disk and CPU running continuously on the Runtime host. Two people installing at the same time also break each other. The installer only retries the binary download (--retry-all-errors, install.sh:944). The metadata, components and catalogue fetches (install.sh:877, :906, :966, :984) have no retry and a 30-second limit. A concurrent request or a slow full scan therefore ends the install with "Publisher gateway unreachable", which is wrong and confusing. The test at site_publication.rs:580 locks in this behaviour and has no concurrent-client case. Fix: verify once per committed publication and cache the result (re-check the file identity stamps per request), or queue requests instead of refusing them.

Non-blocking notes:

  • carrier.rs:1675-1721: the Carrier file and release_head operations still serve raw publisher files without this verification, and they follow symlinks. The PR says "Serve the signed set through existing routes", but that only holds for HTTP. Clients still check signatures.
  • sources.rs: the "old key refused" test uses a made-up envelope that fails the DID check before any signature check. It is not a real release signed by the old key, so the Done means test is only partly met.
  • Several smokes (home-frontdoor-smoke.sh:13, local-carrier-chat-smoke.sh:17) now pair the new DID with the live gateway, which is still signed by the old key. They will fail until promotion.

Not checked: I ran no tests and no CI. I did not run the installed Home journey. I did not check release-signer.py canonical JSON against Runtime verification, or the shell promote/restore steps when a crash happens partway through.

@andersalm

Copy link
Copy Markdown
Contributor Author

Independent adversarial review (Claude Opus via elastos-pr-review, 2026-10-02), head d631889.
VERDICT: PASS

Blocking findings: none. I found no bypass, no fail-open path, no leaked secrets and no way to serve unsigned bytes. The gateway refusal tests in site_publication.rs:673-935 would fail against the old code, which served raw bytes with a 200. The signer refusal suite (release-signer-test.py) covers a local-only or moved tag, off-main commits, tampered artifacts, links, template injection and cancellation. Signing format matches crypto.rs:14.

Non-blocking notes:

  1. gateway_site.rs:260 keeps a failed snapshot cached as refused until the receipt or head changes. A promotion that fails before the receipt moves still changes the live files' ctime and link count, because publish-release.sh:1354 runs ln on them. The restored set then returns 503 until a Runtime restart or a successful republish. The PR says the prior set is restored, but it is not served again, and no recovery step is documented.
  2. The trust pin is the receipt (publish-state.json), which sits in the same Runtime-writable directory as the files it protects. That stops partial or accidental changes, but not a writer with the Runtime account's access, which is the threat Replace the maintainer release key and keep signing off the public server #186 names. Also, the single permit (gateway_site.rs:208) makes every public metadata read wait while each artifact request hashes its full file.
  3. home-frontdoor-smoke.sh:13, home-demo-local.sh:10, local-identity-profile-smoke.sh:13 and local-carrier-chat-smoke.sh:17 now pin the new DID but still default to the live gateway, which serves old-key releases. just verify-release and the demos fail until live switches (M3). The just publish* recipes (justfile:289-298) still pass --key, which is now refused.

Not checked: I had no way to run commands in this review, so I compiled and ran nothing, including the nine new Rust cases. Also not checked: the --reuse-support and release-platform-input changes in depth, custody and the human re-trust or install steps, and the Mac M1/M2 journey.

@andersalm

Copy link
Copy Markdown
Contributor Author

Independent adversarial review (Claude Opus via elastos-pr-review, 2026-10-02), head cc4b910.
VERDICT: PASS

Blocking findings: none. I found no bypass, fail-open path or broken Home journey in the reviewed paths. Refused-case tests are present for each serving path: tamper, mixed set, wrong pin, wrong CID, wrong version, symlinks, hard links, unsafe mode, unadvertised files and nested paths (gateway_tests/site_publication.rs:873-1060). The signer has the same coverage: local-only or moved tag, off-main, tampered artifact, unbound artifact, cancel and tool pins (release-signer-test.py:154-460). Re-trust covers cancel, EOF, wrong DID, reload and old-anchor refusal (sources.rs tests). These tests check behaviour that the old code did not have, so they would fail without this change.

Non-blocking notes:

  1. The gateway takes its pin from publish-state.json (gateway_site.rs:186), which sits in the same Runtime-writable Publisher folder. A process with the Runtime account's access can write its own pin and a self-signed set. The guarantee holds only against partial edits. The README DID is the real trust anchor.
  2. Availability on the public routes: ReleaseReadGate is a single permit with no wait limit, and each artifact request re-hashes the full file. Anyone can repeatedly request the binary and delay install.sh and release-head.json for everyone. After deployment, old-format live sets with no receipt or installer_sha256 return 503, so the live seed must not run this build before M3.
  3. Carrier delivery (carrier.rs:1700-1716) still serves Publisher files without set verification. Clients verify signatures, but this path does not have the guarantee the HTTP gateway now has.

Not checked:

  • I ran no tests or CI.
  • The installed Home journey on the exact head, with the fix(gateway): let apps open inside the sandboxed home shell #202 baseline.
  • An end-to-end check that Rust's verify_release_envelope accepts envelopes from the Python signer.
  • Rollback in publish-release.sh (export_release_publication and restore_release_publication) and release-platform-input.py: I did not read these in full.
  • Custody, key removal and the re-trust step run by a person.

@andersalm
andersalm marked this pull request as ready for review October 2, 2026 21:16
@andersalm
andersalm requested a review from irzhywau October 2, 2026 21:16
@andersalm

Copy link
Copy Markdown
Contributor Author

Independent adversarial review (Claude Opus via elastos-pr-review, 2026-10-02), head 659e180.
VERDICT: PASS

Blocking findings: none.

I found no bypass, unguarded release path or fail-open branch in the changed code. The refused-case tests would fail against the old code:

  • Tampered metadata or artifacts get 503, and stale or nested artifact names are refused (site_publication.rs:921-990); the old handlers returned these files with 200.
  • 0.7.10 and rc/build prefix overlaps (install-bootstrap-test.py) would pass the old grep -F check.
  • Re-trust keeps the Carrier ticket and gateways (sources.rs tests); the old code cleared them.

Non-blocking notes:

  1. gateway_site.rs:182-183: the gateway pins against publisher_did from publish-state.json. That file sits in the same Runtime-owned folder (only uid == euid and no group/world write are checked). Anyone who can write as the Runtime account can sign their own set and rewrite the receipt, and the gateway will serve an installer with their MAINTAINER_DID. This is the actor named in Replace the maintainer release key and keep signing off the public server #186. The PR shows the "edit install.sh on disk" test, but the threat stays open, so Replace the maintainer release key and keep signing off the public server #186 must not be closed on this alone.
  2. gateway_site.rs:350-352, negative cache: any SERVICE_UNAVAILABLE turns the publication off until the receipt or head changes. That includes a temporary I/O error or EMFILE. One such error can take /install.sh offline until a republish or restart. All release reads also share one semaphore permit, and each artifact read hashes the whole file. Anonymous requests for the largest artifact can therefore keep /release-head.json waiting.
  3. The gateway now needs the new receipt, source and installer_sha256. If this develop build goes to the current public seed before a republish, public install.sh returns 503. The live promotion gate (M3) must cover this.

Not checked: I ran no tests or CI. I did not fully review the publish-release.sh rewrite (about 1,900 lines), the --reuse-support/release-platform-input.py changes, how release-signer.py handles OpenSSL, or real macOS install/update behaviour.

@andersalm

Copy link
Copy Markdown
Contributor Author

Exact installed acceptance for the reviewed candidate, tree 9893e03fc02b319a30e42c4b830342ecfa7ec1ed: run37062770547 passed. Artifact11252961772 ZIP SHA-256 is 8580b3f340582abfa66f1900b93b99d48af028692072a6b36a37730be61b9c32 and matches GitHub’s digest.
Built/installed Runtime SHA-256 matches ab53f1844a203e8849fd79b7a68d881a47ac4fbc3800a348a478ac81442ca931; model provider matches 4a913f0b27329bd76a4af696a6680baba82395a43262eb91801a3f3898c79d01. Installation receipt SHA-256 is 2decf9bf0ede911409240de6d62c8bb6c227ea55b41d46079083bf8107bda3b7.
The actual artifact records clean candidate source, installation parity and three passing checks in159.2seconds. Signed-in Home controls opened Marketplace and Assistant; their app frames admitted the pinned model and completed a34-character reply under the eight-token request. Desktop and phone screenshots were inspected.
All four pinned harness hashes match their source; fixture cleanup has empty remaining/forced-stop lists, and the tampered Kubo archive was refused before executable installation. CI source tests passed separately in run37062775415, with current Opus PASS.
This proves the bounded installed Home journey. Real-key M1/M2, person-run re-trust, full install/prompt controls and normal signed-capsule setup remain with their acceptance gates in #89, #153, #186 and #217.

@andersalm

Copy link
Copy Markdown
Contributor Author

Independent development review (gpt-6.1-sol high, fork_turns none): VERDICT: PASS for the current candidate. Blocking findings: none.
The review covered signer admission, inert-input signing, snapshot/import, head-last promotion/recovery, signed gateway serving, frozen support reuse, exact installer version validation and re-trust. Canary admission matches Anders’s approved merged-develop source/tool policy; stable and jetson-test retain main version tags.
The reviewer ran all37 signer tests and diff checks, checked all nine CI results, and independently read the installed proof artifact: exact source/tree binding, Runtime/provider parity, three journey checks and empty cleanup.
Approved PR151’s four process/checker files have zero overlap with the candidate. Root’s clean merged-tree compatibility check passes the current entropy gate; security and installed-product files remain identical to the reviewed candidate.
This complements the current Opus adversarial PASS. Custody, real-key staged Mac install/update, person-run re-trust and live promotion retain their operator gates; open review observations remain in #186.

@andersalm
andersalm merged commit 3f67281 into develop Oct 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant