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:
co-authored by
Claude Fable 5
parent
1977bdefb5
commit
51647b21cd
@@ -177,9 +177,28 @@ fi
|
||||
echo "[6/8] Creating release manifest..."
|
||||
mkdir -p "$PROJECT_ROOT/releases"
|
||||
"$SCRIPT_DIR/create-release-manifest.sh" --version "$VERSION" --date "$RELEASE_DATE" --output "$PROJECT_ROOT/releases/manifest.json" 2>&1 | grep -v "^$"
|
||||
|
||||
# §A supply-chain: the OTA manifest must carry the release-root signature.
|
||||
# Nodes refuse to AUTO-apply unsigned manifests, and publish-release-assets.sh
|
||||
# hard-refuses to ship one. The mnemonic is read interactively (or from
|
||||
# RELEASE_MASTER_MNEMONIC) — it must never land in files or shell history.
|
||||
SIGNER="$PROJECT_ROOT/core/target/release/archipelago"
|
||||
if [ ! -x "$SIGNER" ]; then
|
||||
echo "Error: release binary not found at $SIGNER — cannot sign manifest" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "${RELEASE_MASTER_MNEMONIC:-}" ] || [ -t 0 ]; then
|
||||
echo "[6b/8] Signing release manifest (paste the release master mnemonic when prompted)..."
|
||||
"$SIGNER" ceremony sign "$PROJECT_ROOT/releases/manifest.json"
|
||||
"$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)"
|
||||
fi
|
||||
cp "$PROJECT_ROOT/releases/manifest.json" "$PROJECT_ROOT/release-manifest.json"
|
||||
|
||||
echo "[6b/8] Staging release artifacts for validation..."
|
||||
echo "[6c/8] Staging release artifacts for validation..."
|
||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
||||
mkdir -p "$VERSION_DIR"
|
||||
|
||||
@@ -25,6 +25,19 @@ fail() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
||||
|
||||
# §A supply-chain gate: never publish an unsigned OTA manifest. Fleet nodes
|
||||
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
|
||||
# and enforcement will tighten to hard-reject — an unsigned publish would
|
||||
# strand them. Grep proves presence; ceremony verify proves the crypto.
|
||||
EXPECTED_DID="did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur"
|
||||
grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|
||||
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| fail "releases/manifest.json is not signed by the release root — run: bash scripts/sign-manifest.sh"
|
||||
if [ -x "$PROJECT_ROOT/core/target/release/archipelago" ]; then
|
||||
"$PROJECT_ROOT/core/target/release/archipelago" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| fail "manifest signature failed cryptographic verification"
|
||||
fi
|
||||
|
||||
remote_url=$(git -C "$PROJECT_ROOT" remote get-url "$REMOTE")
|
||||
case "$remote_url" in
|
||||
http://*@*) ;;
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-step OTA-manifest signer (counterpart to sign-catalog.sh).
|
||||
#
|
||||
# Run: bash scripts/sign-manifest.sh
|
||||
# Then: paste your 24-word release master mnemonic, press Enter, then Ctrl-D.
|
||||
#
|
||||
# Signs releases/manifest.json in place and cryptographically verifies the
|
||||
# result against the pinned release-root anchor. The mnemonic is read from the
|
||||
# terminal only (never stored, never in shell history, never passed to Claude).
|
||||
#
|
||||
# Normally create-release.sh signs the manifest inline; this script exists for
|
||||
# re-signing (e.g. a manifest edited after creation) or signing on a box where
|
||||
# the release run was non-interactive.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MANIFEST="$REPO/releases/manifest.json"
|
||||
|
||||
# Use ONLY a prebuilt signer — never compile here (compiling caused hangs in
|
||||
# the earlier catalog ceremony). Prefer the repo's release build.
|
||||
BIN=""
|
||||
for candidate in "$REPO/core/target/release/archipelago" /tmp/archy-sign-bin/release/archipelago; do
|
||||
if [[ -x "$candidate" ]]; then BIN="$candidate"; break; fi
|
||||
done
|
||||
if [[ -z "$BIN" ]]; then
|
||||
echo "⏳ No prebuilt signer found. Build one first:"
|
||||
echo " (cd core && cargo build --release -p archipelago)"
|
||||
echo " Nothing was changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo " Paste your 24-word release master mnemonic below, press Enter,"
|
||||
echo " then press Ctrl-D on a new line."
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
"$BIN" ceremony sign "$MANIFEST"
|
||||
|
||||
echo
|
||||
if "$BIN" ceremony verify "$MANIFEST"; then
|
||||
echo "✅ SUCCESS — manifest signed by the pinned release root."
|
||||
echo " Commit + push releases/manifest.json (and release-manifest.json if present)."
|
||||
cp "$MANIFEST" "$REPO/release-manifest.json" 2>/dev/null || true
|
||||
else
|
||||
echo "❌ Signature did NOT verify against the pinned release-root anchor."
|
||||
echo " Do NOT commit. Check the mnemonic and re-run."
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user