From cc9e19589c08767619d9116e922b9f5d44007020 Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 3 Aug 2026 17:55:02 -0400 Subject: [PATCH] fix(release): refuse to commit an unsigned OTA manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every cycle has needed a manual check that releases/manifest.json got signed, because the script would happily commit and tag one that hadn't. The signing step is conditional: with no TTY and no RELEASE_MASTER_MNEMONIC it prints a warning and falls through. The commit at step 7 then ran regardless, so the release commit — and its tag — carried an unsigned manifest. publish-release-assets.sh already refuses to ship one, but that backstop arrives a step too late. Nodes fetch releases/manifest.json straight from branch `main` (the same URLs this script prints for verification), so the COMMIT is what exposes it to the fleet, not the publish. By the time publishing is refused, the unsigned manifest is already on main and nodes are already declining to auto-apply. So the same gate now runs before the commit: presence of a signature, signed_by matching the release root, and `ceremony verify` for the crypto. A release commit carrying a manifest no node will accept has no valid use, so this refuses to create one rather than leave a tag that has to be re-cut. The earlier warning is corrected too — it promised the run would continue, which is no longer true. Verified the predicate against three manifests: signed -> allow, signature stripped -> refuse, signed_by swapped to another DID -> refuse. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/create-release.sh | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/create-release.sh b/scripts/create-release.sh index fedd5c3c..c35ddb11 100755 --- a/scripts/create-release.sh +++ b/scripts/create-release.sh @@ -212,8 +212,10 @@ if [ -n "${RELEASE_MASTER_MNEMONIC:-}" ] || [ -t 0 ]; then "$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json" else echo "⚠ WARNING: no TTY and RELEASE_MASTER_MNEMONIC unset — manifest left UNSIGNED." - echo " Sign it before publishing: bash scripts/sign-manifest.sh" - echo " (publish-release-assets.sh refuses to ship an unsigned manifest)" + echo " This run will ABORT before committing (step 7 refuses an unsigned" + echo " manifest), because nodes read releases/manifest.json from branch main" + echo " and would refuse to auto-apply it." + echo " Sign it, then re-run: bash scripts/sign-manifest.sh" fi cp "$PROJECT_ROOT/releases/manifest.json" "$PROJECT_ROOT/release-manifest.json" @@ -225,6 +227,36 @@ install -m 0755 "$PROJECT_ROOT/core/target/release/archipelago" "$VERSION_DIR/ar install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz" "$SCRIPT_DIR/check-release-manifest.sh" +# §A supply-chain gate, mirroring publish-release-assets.sh — but EARLIER, +# because publishing is not the first way an unsigned manifest reaches the +# fleet. Nodes fetch releases/manifest.json straight from branch `main` +# (see the verification URLs printed below), so the COMMIT is what exposes +# it, not the publish. publish-release-assets.sh refusing to ship is a +# backstop that arrives one step too late: by then the unsigned manifest is +# already on main and the fleet is already refusing to auto-apply. +# +# This is why every cycle needed a manual catch. The signing block above is +# conditional — no TTY and no RELEASE_MASTER_MNEMONIC means it prints a +# warning and falls through — and the commit then happened anyway. A release +# commit carrying a manifest no node will accept has no valid use, so refuse +# to create one rather than leave a tag that has to be re-cut. +EXPECTED_DID="did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur" +if ! grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \ + || ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json"; then + echo "" >&2 + echo "Error: releases/manifest.json is NOT signed by the release root." >&2 + echo " Refusing to commit — nodes read this file from branch main and will" >&2 + echo " refuse to auto-apply it, so the release would be dead on arrival." >&2 + echo "" >&2 + echo " Sign it, then re-run this script:" >&2 + echo " bash scripts/sign-manifest.sh" >&2 + echo "" >&2 + echo " (Signing needs a TTY for the mnemonic prompt, or RELEASE_MASTER_MNEMONIC set.)" >&2 + exit 1 +fi +"$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \ + || { echo "Error: manifest signature failed cryptographic verification — refusing to commit" >&2; exit 1; } + echo "[7/8] Committing version bump..." git -C "$PROJECT_ROOT" add \ core/archipelago/Cargo.toml \