From af845d3e3209e1d754d43553a1292b54918ea4ea Mon Sep 17 00:00:00 2001 From: Dave Thompson Date: Wed, 16 Sep 2026 16:48:42 -0400 Subject: [PATCH 1/3] chore: recut org signing anchors to current keys Replace the minisign trust anchor and installer GPG fingerprint pins across the anchor SSOT, installer, bootstrap engine, Go embed, docs, and test fixtures. Generated by Muse Spark running Muse Code under supervision of [@3leapsdave](https://github.com/3leapsdave) Co-Authored-By: Muse Spark Role: devlead Committer-of-Record: Dave Thompson [@3leapsdave] --- README.md | 4 +++- docs/examples.md | 4 ++-- scripts/bootstrap-sfetch-verified.sh | 2 +- scripts/install-sfetch.sh | 4 ++-- scripts/sfetch-minisign-anchor.pub | 4 ++-- scripts/test-bootstrap-sfetch-verified.sh | 2 +- testdata/shellsentry/shellsentry-minisign.pub | 4 ++-- 7 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7edd654..f0f8612 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ INSTALL_BINDIR=~/bin make install # override install location TAG=v0.4.11 curl -fsSL "https://github.com/3leaps/sfetch/releases/download/${TAG}/install-sfetch.sh" -o install-sfetch.sh curl -fsSL "https://github.com/3leaps/sfetch/releases/download/${TAG}/install-sfetch.sh.minisig" -o install-sfetch.sh.minisig -minisign -Vm install-sfetch.sh -P RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC +minisign -Vm install-sfetch.sh -P RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 bash install-sfetch.sh --tag "$TAG" --yes # Or use the shared engine (CI/Makefile; refuses "latest"): @@ -271,6 +271,8 @@ The installer: #### Verify via signed SHA256SUMS (pins ≤ v0.4.10) +These pins predate the org key rotation and verify only under the previous key; the `-P` below is intentionally not the current key. + ```bash # Detect OS-appropriate SHA256 command (macOS uses shasum, Linux uses sha256sum) if command -v sha256sum &>/dev/null; then diff --git a/docs/examples.md b/docs/examples.md index bf45e54..2d595b1 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -146,8 +146,8 @@ sfetch --repo 3leaps/sfetch --latest \ **Public key** (3leaps/sfetch): ``` -untrusted comment: sfetch release signing key -RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC +untrusted comment: 3leaps org release signing +RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 ``` ## Minisign Workflow B: `jedisct1/minisign` diff --git a/scripts/bootstrap-sfetch-verified.sh b/scripts/bootstrap-sfetch-verified.sh index 9793f65..0f7478b 100755 --- a/scripts/bootstrap-sfetch-verified.sh +++ b/scripts/bootstrap-sfetch-verified.sh @@ -50,7 +50,7 @@ readonly SFETCH_MINISIG_SINCE="v0.4.11" # main.go (go:embed of that file), and scripts/install-sfetch.sh. Do NOT fetch # sfetch-minisign.pub from the release for authentication (circular). # The published .pub is for human out-of-band comparison only. -readonly SFETCH_MINISIGN_PUBKEY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" +readonly SFETCH_MINISIGN_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" readonly MINISIGN_VERSION_EXPECTED="0.12" readonly MINISIGN_WIN_URL="https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-win64.zip" diff --git a/scripts/install-sfetch.sh b/scripts/install-sfetch.sh index 469ffc6..11a185d 100755 --- a/scripts/install-sfetch.sh +++ b/scripts/install-sfetch.sh @@ -40,9 +40,9 @@ SFETCH_API="https://api.github.com/repos/${SFETCH_REPO}/releases" # IMPORTANT: This key must match scripts/sfetch-minisign-anchor.pub (SSOT) # and EmbeddedMinisignPubkey in main.go (go:embed of that file). # Update both when rotating keys (see docs/security/signing-runbook.md) -SFETCH_MINISIGN_PUBKEY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" +SFETCH_MINISIGN_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" # Pinned PGP fingerprint (for optional fallback) -SFETCH_PGP_FPR="94BB7811D4AD49B2310E0C08FA0651DE91B828ED" +SFETCH_PGP_FPR="0CACA49B3119B6BC12B2CA11B9B485F294B9FE07" TRUST_LEVEL="unknown" # ----------------------------------------------------------------------------- diff --git a/scripts/sfetch-minisign-anchor.pub b/scripts/sfetch-minisign-anchor.pub index 0f5dbd5..00d3703 100644 --- a/scripts/sfetch-minisign-anchor.pub +++ b/scripts/sfetch-minisign-anchor.pub @@ -1,2 +1,2 @@ -untrusted comment: sfetch release signing key (canonical trust anchor SSOT) -RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC +untrusted comment: 3leaps org release signing +RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 diff --git a/scripts/test-bootstrap-sfetch-verified.sh b/scripts/test-bootstrap-sfetch-verified.sh index 9cb3077..daab750 100755 --- a/scripts/test-bootstrap-sfetch-verified.sh +++ b/scripts/test-bootstrap-sfetch-verified.sh @@ -95,7 +95,7 @@ assert_early_version_reject() { assert_early_version_reject "v18446744073709551616.4.10" "huge-major-wrap-zero" assert_early_version_reject "v0.4.18446744073709551627" "huge-patch-wrap-into-range" -PROD_PUBKEY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" +PROD_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" # Build a temporary engine: optional test pubkey + fixed BASE_URL for sfetch assets. # Minisign always comes from the engine's pinned 0.12 download (not ambient PATH). diff --git a/testdata/shellsentry/shellsentry-minisign.pub b/testdata/shellsentry/shellsentry-minisign.pub index be9a885..706bf4f 100644 --- a/testdata/shellsentry/shellsentry-minisign.pub +++ b/testdata/shellsentry/shellsentry-minisign.pub @@ -1,2 +1,2 @@ -untrusted comment: minisign public key DE44B5D37442A1C0 -RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC +untrusted comment: minisign public key EA519FCFB667C06D +RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 From 6c0a7a3cf3b38caa0c67815f85e2c1c960c267a2 Mon Sep 17 00:00:00 2001 From: Dave Thompson Date: Wed, 16 Sep 2026 16:59:35 -0400 Subject: [PATCH 2/3] fix: use minisign display order for rotated key id comment minisign -G writes the key id byte-reversed in the untrusted comment (per the -W fixture); match that convention for the new key. Generated by Muse Spark running Muse Code under supervision of [@3leapsdave](https://github.com/3leapsdave) Co-Authored-By: Muse Spark Role: devlead Committer-of-Record: Dave Thompson [@3leapsdave] --- testdata/shellsentry/shellsentry-minisign.pub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/shellsentry/shellsentry-minisign.pub b/testdata/shellsentry/shellsentry-minisign.pub index 706bf4f..d0af167 100644 --- a/testdata/shellsentry/shellsentry-minisign.pub +++ b/testdata/shellsentry/shellsentry-minisign.pub @@ -1,2 +1,2 @@ -untrusted comment: minisign public key EA519FCFB667C06D +untrusted comment: minisign public key 6DC067B6CF9F51EA RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 From 65a80d1128f1462b2f8f3bd955e1693e5f5fd6f7 Mon Sep 17 00:00:00 2001 From: Dave Thompson Date: Wed, 16 Sep 2026 17:28:38 -0400 Subject: [PATCH 3/3] chore: select trust anchor by release pin Tags at or below v0.4.11 verify against the previous org keys; newer tags verify against the current keys. One key per tag, no fallback. Covers the bootstrap engine and installer (minisign and GPG fallback), with regression tests for selection both ways and a sunset assert on the cutoff constant. Verification: make precommit green; live v0.4.11/v0.4.10 bootstrap and installer dry-run pass via the legacy anchor. Generated by Muse Spark running Muse Code under supervision of [@3leapsdave](https://github.com/3leapsdave) Co-Authored-By: Muse Spark Role: devlead Committer-of-Record: Dave Thompson [@3leapsdave] --- Makefile | 6 +- README.md | 4 +- docs/examples.md | 2 + scripts/assert-bootstrap-range-release.sh | 15 ++- scripts/bootstrap-sfetch-verified.sh | 30 ++++- scripts/install-sfetch.sh | 55 ++++++++- scripts/test-bootstrap-sfetch-verified.sh | 131 ++++++++++++++++++++-- scripts/test-install-anchor-select.sh | 95 ++++++++++++++++ 8 files changed, 319 insertions(+), 19 deletions(-) create mode 100755 scripts/test-install-anchor-select.sh diff --git a/Makefile b/Makefile index 5086f84..5e0a89f 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ CORPUS_DEST ?= test-corpus .PHONY: release-verify-key release-verify-minisign-pubkey release-verify-keys release-verify-signatures release-verify .PHONY: release-clean bootstrap-script build-all gosec gosec-high update-scoop-manifest .PHONY: version-check version-set version-patch version-minor version-major -.PHONY: print-sfetch-version test-release-verify-checksums test-release-verify-signatures test-bootstrap-sfetch-verified test-bootstrap-range-release +.PHONY: print-sfetch-version test-release-verify-checksums test-release-verify-signatures test-bootstrap-sfetch-verified test-bootstrap-range-release test-install-anchor-select all: build @@ -213,6 +213,7 @@ precommit: ## Run pre-commit checks (goneat assess + Go tests + build) $(MAKE) test-release-verify-signatures $(MAKE) test-bootstrap-sfetch-verified $(MAKE) test-bootstrap-range-release + $(MAKE) test-install-anchor-select @echo "[ok] Pre-commit checks passed" prepush: precommit ## Run pre-push checks (same as precommit + security) @@ -291,6 +292,9 @@ test-bootstrap-sfetch-verified: ## Regression: dual-route bootstrap rejects + fa test-bootstrap-range-release: ## Assert MAX==v(VERSION) and MINISIG_SINCE in range (committed constants) @./scripts/assert-bootstrap-range-release.sh +test-install-anchor-select: ## Regression: installer pin-map anchor selection (unit + coherence) + @./scripts/test-install-anchor-select.sh + release-notes: ## Copy release notes into dist/release @if [ -z "$(RELEASE_TAG)" ]; then echo "error: RELEASE_TAG not set" >&2; exit 1; fi @mkdir -p $(DIST_RELEASE) diff --git a/README.md b/README.md index f0f8612..ed6db3d 100644 --- a/README.md +++ b/README.md @@ -210,12 +210,14 @@ INSTALL_BINDIR=~/bin make install # override install location **Recommended (v0.4.11+):** verify the installer before execution. +Match `-P` to the tag: tags ≤ v0.4.11 verify under the previous key; newer tags use the current key. + ```bash # Three-step path when install-sfetch.sh.minisig is published (v0.4.11+) TAG=v0.4.11 curl -fsSL "https://github.com/3leaps/sfetch/releases/download/${TAG}/install-sfetch.sh" -o install-sfetch.sh curl -fsSL "https://github.com/3leaps/sfetch/releases/download/${TAG}/install-sfetch.sh.minisig" -o install-sfetch.sh.minisig -minisign -Vm install-sfetch.sh -P RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 +minisign -Vm install-sfetch.sh -P RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC bash install-sfetch.sh --tag "$TAG" --yes # Or use the shared engine (CI/Makefile; refuses "latest"): diff --git a/docs/examples.md b/docs/examples.md index 2d595b1..5217209 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -150,6 +150,8 @@ untrusted comment: 3leaps org release signing RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1 ``` +Releases ≤ v0.4.11 publish the previous key asset instead; auto-detect selects the key from the release being verified. + ## Minisign Workflow B: `jedisct1/minisign` Per-asset signing - signature directly over each asset. No checksum file. diff --git a/scripts/assert-bootstrap-range-release.sh b/scripts/assert-bootstrap-range-release.sh index eb0dfb5..1164c75 100755 --- a/scripts/assert-bootstrap-range-release.sh +++ b/scripts/assert-bootstrap-range-release.sh @@ -65,8 +65,9 @@ extract_const() { MAX="$(extract_const SFETCH_BOOTSTRAP_MAX)" MIN="$(extract_const SFETCH_BOOTSTRAP_MIN)" SINCE="$(extract_const SFETCH_MINISIG_SINCE)" +PREVKEY="$(extract_const SFETCH_PREVKEY_MAX)" -echo "bootstrap-range assert: VERSION=${VER} MAX=${MAX} MIN=${MIN} MINISIG_SINCE=${SINCE}" +echo "bootstrap-range assert: VERSION=${VER} MAX=${MAX} MIN=${MIN} MINISIG_SINCE=${SINCE} PREVKEY_MAX=${PREVKEY}" [ "$MAX" = "$TAG" ] || fail "SFETCH_BOOTSTRAP_MAX (${MAX}) must equal v\$(cat VERSION) (${TAG}). Advance the constant (and boundary tests) before cutting this release — see RELEASE_CHECKLIST.md §1." @@ -137,4 +138,14 @@ semver_cmp_tag() { [ "$(semver_cmp_tag "$MIN" "$MAX")" != "1" ] || fail "SFETCH_BOOTSTRAP_MIN (${MIN}) is above SFETCH_BOOTSTRAP_MAX (${MAX})" -echo "[ok] bootstrap range coherent: MAX=${MAX} == VERSION tag; MINISIG_SINCE=${SINCE} in [${MIN}..${MAX}]" +# PREVKEY_MAX is the last tag signed with the previous org key: a historical +# fact, never advanced. Pins at or below it select the legacy anchor. +[ "$PREVKEY" = "v0.4.11" ] || + fail "SFETCH_PREVKEY_MAX (${PREVKEY}) must stay v0.4.11 (last previous-key tag)" +# Sunset forcing function: once MIN advances past the cutoff, the legacy branch +# is unreachable — delete it (and the LEGACY anchors) instead of leaving a dead +# second root. +[ "$(semver_cmp_tag "$PREVKEY" "$MIN")" != "-1" ] || + fail "SFETCH_PREVKEY_MAX (${PREVKEY}) is below SFETCH_BOOTSTRAP_MIN (${MIN}): sunset — remove the legacy anchor path" + +echo "[ok] bootstrap range coherent: MAX=${MAX} == VERSION tag; MINISIG_SINCE=${SINCE} in [${MIN}..${MAX}]; PREVKEY_MAX=${PREVKEY}" diff --git a/scripts/bootstrap-sfetch-verified.sh b/scripts/bootstrap-sfetch-verified.sh index 0f7478b..f965006 100755 --- a/scripts/bootstrap-sfetch-verified.sh +++ b/scripts/bootstrap-sfetch-verified.sh @@ -8,6 +8,9 @@ # Never falls back from a failed .minisig attempt to checksums. # Never accepts "latest", branches, or floating refs. # Trust anchor is embedded here — never fetched from the release being authenticated. +# Pin map (tag -> exactly one pubkey): tags at or below SFETCH_PREVKEY_MAX verify +# against the previous-key anchor; tags above verify against the current anchor. +# Never tries both keys for one tag. # # Usage: # bootstrap-sfetch-verified.sh --version v0.4.11 --dir ~/.local/bin @@ -52,6 +55,16 @@ readonly SFETCH_MINISIG_SINCE="v0.4.11" # The published .pub is for human out-of-band comparison only. readonly SFETCH_MINISIGN_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" +# Previous-key anchor, pin-scoped ONLY. SFETCH_PREVKEY_MAX is the last tag signed +# with the previous org key (historical fact, never advance it): pins at or below +# it verify against SFETCH_MINISIGN_PUBKEY_LEGACY; pins above verify against +# SFETCH_MINISIGN_PUBKEY. Never try both keys for one tag, and never treat the +# legacy anchor as a default trust anchor. Sunset: delete this constant and the +# legacy selection branch once nothing in-tree pins <= SFETCH_PREVKEY_MAX (the +# range-release assert fails once MIN advances past it, forcing the removal). +readonly SFETCH_PREVKEY_MAX="v0.4.11" +readonly SFETCH_MINISIGN_PUBKEY_LEGACY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" + readonly MINISIGN_VERSION_EXPECTED="0.12" readonly MINISIGN_WIN_URL="https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-win64.zip" readonly MINISIGN_WIN_SHA256="37b600344e20c19314b2e82813db2bfdcc408b77b876f7727889dbd46d539479" @@ -522,14 +535,25 @@ if semver_ge "$VERSION" "$SFETCH_MINISIG_SINCE"; then else ROUTE="sha256sums" fi -log "bootstrap-sfetch-verified: version=${VERSION} verify-route=${ROUTE} range=${SFETCH_BOOTSTRAP_MIN}..${SFETCH_BOOTSTRAP_MAX}" + +# Anchor selection: pin map, exactly one key per tag (VERSION is exact semver +# and range-checked above, so the comparison cannot fail open). +ANCHOR_GEN="current" +ACTIVE_MINISIGN_PUBKEY="$SFETCH_MINISIGN_PUBKEY" +if semver_le "$VERSION" "$SFETCH_PREVKEY_MAX"; then + ANCHOR_GEN="legacy" + ACTIVE_MINISIGN_PUBKEY="$SFETCH_MINISIGN_PUBKEY_LEGACY" +fi +log "bootstrap-sfetch-verified: version=${VERSION} verify-route=${ROUTE} anchor=${ANCHOR_GEN} range=${SFETCH_BOOTSTRAP_MIN}..${SFETCH_BOOTSTRAP_MAX}" write_pubkey() { local path="$1" - # minisign -p expects a file with optional comment + RW... key line + # minisign -p expects a file with optional comment + RW... key line. + # ACTIVE_MINISIGN_PUBKEY is the pin-selected anchor (never a second guess). + [ -n "${ACTIVE_MINISIGN_PUBKEY:-}" ] || die "internal error: anchor not selected" { echo "untrusted comment: sfetch release signing key (embedded trust anchor)" - echo "${SFETCH_MINISIGN_PUBKEY}" + echo "${ACTIVE_MINISIGN_PUBKEY}" } >"$path" } diff --git a/scripts/install-sfetch.sh b/scripts/install-sfetch.sh index 11a185d..1bd292f 100755 --- a/scripts/install-sfetch.sh +++ b/scripts/install-sfetch.sh @@ -43,6 +43,14 @@ SFETCH_API="https://api.github.com/repos/${SFETCH_REPO}/releases" SFETCH_MINISIGN_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" # Pinned PGP fingerprint (for optional fallback) SFETCH_PGP_FPR="0CACA49B3119B6BC12B2CA11B9B485F294B9FE07" +# Previous-key anchors, pin-scoped ONLY. SFETCH_PREVKEY_MAX is the last tag signed +# with the previous org keys (historical fact, never advance it): pins at or below +# it verify against the LEGACY anchors; pins above verify against the current ones. +# Never try both generations for one tag. Sunset: delete these constants and the +# legacy selection branch once nothing in-tree pins <= SFETCH_PREVKEY_MAX. +SFETCH_PREVKEY_MAX="v0.4.11" +SFETCH_MINISIGN_PUBKEY_LEGACY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" +SFETCH_PGP_FPR_LEGACY="94BB7811D4AD49B2310E0C08FA0651DE91B828ED" TRUST_LEVEL="unknown" # ----------------------------------------------------------------------------- @@ -62,6 +70,31 @@ need_cmd() { fi } +is_exact_semver_tag() { + [[ "$1" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] +} + +# semver_le_tag: true if tag a <= tag b (exact vX.Y.Z inputs, no leading zeros). +# Length-then-lexical per component under C locale: no machine arithmetic, so no +# wraparound on huge components. Mirrors the bootstrap engine's comparison class. +semver_le_tag() { + local LC_ALL=C + local a="${1#v}" b="${2#v}" + local a1 a2 a3 b1 b2 b3 + IFS=. read -r a1 a2 a3 <<<"$a" + IFS=. read -r b1 b2 b3 <<<"$b" + local x y + for pair in "$a1:$b1" "$a2:$b2" "$a3:$b3"; do + x="${pair%%:*}" + y="${pair##*:}" + if [ "${#x}" -lt "${#y}" ]; then return 0; fi + if [ "${#x}" -gt "${#y}" ]; then return 1; fi + if [[ "$x" < "$y" ]]; then return 0; fi + if [[ "$x" > "$y" ]]; then return 1; fi + done + return 0 +} + read_release_tag_name() { local release_json_file="$1" local version="" @@ -322,7 +355,7 @@ verify_signature() { if [ "$VERIFY_MINISIGN" = true ] && [ -f "${sums_file}.minisig" ]; then local pubkey_file="${tmpdir}/sfetch-minisign.pub" echo "untrusted comment: sfetch release signing key" >"$pubkey_file" - echo "$SFETCH_MINISIGN_PUBKEY" >>"$pubkey_file" + echo "$ACTIVE_MINISIGN_PUBKEY" >>"$pubkey_file" log "Verifying signature with minisign (embedded trust anchor)..." if minisign -Vm "$sums_file" -p "$pubkey_file" >/dev/null 2>&1; then @@ -349,8 +382,8 @@ verify_signature() { if [ -f "$gpg_key" ]; then local fpr fpr=$(gpg --with-colons --import-options show-only --fingerprint "$gpg_key" 2>/dev/null | awk -F: '/^fpr:/ {print $10; exit}') - if [ "$fpr" != "$SFETCH_PGP_FPR" ]; then - err "GPG key fingerprint mismatch (expected ${SFETCH_PGP_FPR}, got ${fpr:-unknown})" + if [ "$fpr" != "$ACTIVE_PGP_FPR" ]; then + err "GPG key fingerprint mismatch (expected ${ACTIVE_PGP_FPR}, got ${fpr:-unknown})" fi log "Verifying signature with gpg (pinned fingerprint)..." local gpg_home @@ -541,6 +574,22 @@ main() { version=$(read_release_tag_name "$release_json") log "Installing sfetch ${version}" + # Pin map: the resolved tag selects exactly one key generation. Tags at or + # below SFETCH_PREVKEY_MAX are previous-key-signed; tags above are + # current-key-signed. Refuse non-semver tags rather than guess a key. + if ! is_exact_semver_tag "$version"; then + err "cannot select trust anchor for non-semver tag: ${version:-}" + fi + ACTIVE_MINISIGN_PUBKEY="$SFETCH_MINISIGN_PUBKEY" + ACTIVE_PGP_FPR="$SFETCH_PGP_FPR" + local anchor_gen="current" + if semver_le_tag "$version" "$SFETCH_PREVKEY_MAX"; then + anchor_gen="legacy" + ACTIVE_MINISIGN_PUBKEY="$SFETCH_MINISIGN_PUBKEY_LEGACY" + ACTIVE_PGP_FPR="$SFETCH_PGP_FPR_LEGACY" + fi + log "trust anchor: ${anchor_gen} (pin ${version})" + # Determine archive name local archive_name="sfetch_${platform}" if [[ "$platform" == windows_* ]]; then diff --git a/scripts/test-bootstrap-sfetch-verified.sh b/scripts/test-bootstrap-sfetch-verified.sh index daab750..0906c46 100755 --- a/scripts/test-bootstrap-sfetch-verified.sh +++ b/scripts/test-bootstrap-sfetch-verified.sh @@ -96,13 +96,23 @@ assert_early_version_reject "v18446744073709551616.4.10" "huge-major-wrap-zero" assert_early_version_reject "v0.4.18446744073709551627" "huge-patch-wrap-into-range" PROD_PUBKEY="RWTqUZ/PtmfAbQ3RKIlp/YTKT6zFeNMAZ8iAMelhlmqEsVqjKj3ctSA1" +LEGACY_PUBKEY="RWTAoUJ007VE3h8tbHlBCyk2+y0nn7kyA4QP34LTzdtk8M6A2sryQtZC" + +# Harness pins must track the engine's anchors exactly; drift fails here, loudly. +grep -q "SFETCH_MINISIGN_PUBKEY=\"$PROD_PUBKEY\"" "$SCRIPT" || fail "harness PROD_PUBKEY drifted from engine" +grep -q "SFETCH_MINISIGN_PUBKEY_LEGACY=\"$LEGACY_PUBKEY\"" "$SCRIPT" || fail "harness LEGACY_PUBKEY drifted from engine" +grep -q 'SFETCH_PREVKEY_MAX="v0.4.11"' "$SCRIPT" || fail "engine PREVKEY cutoff moved; update pin-map tests" # Build a temporary engine: optional test pubkey + fixed BASE_URL for sfetch assets. # Minisign always comes from the engine's pinned 0.12 download (not ambient PATH). # CI runners often ship distro minisign 0.11; using ambient would fail the 0.12 assert # and is not how production runs. +# +# varname selects which anchor the test pubkey replaces: pin-map tests must patch +# exactly the anchor the requested pin selects, leaving the other production +# anchor intact (patching both would mask selection bugs). make_fixture_engine() { - local dest="$1" base_url="$2" pubkey="${3-}" + local dest="$1" base_url="$2" pubkey="${3-}" varname="${4-}" local src="$SCRIPT" cp "$src" "$dest" # Fixed download base → fixture URL (literal, no env). @@ -111,9 +121,25 @@ make_fixture_engine() { 's|BASE_URL="https://github.com/${REPO}/releases/download"|BASE_URL="'"${base_url}"'"|' \ "$dest" if [ -n "$pubkey" ]; then - sed -i.bak "s|${PROD_PUBKEY}|${pubkey}|g" "$dest" + [ -n "$varname" ] || fail "make_fixture_engine: varname required with pubkey" + local old other + case "$varname" in + SFETCH_MINISIGN_PUBKEY) + old="$PROD_PUBKEY" + other="$LEGACY_PUBKEY" + ;; + SFETCH_MINISIGN_PUBKEY_LEGACY) + old="$LEGACY_PUBKEY" + other="$PROD_PUBKEY" + ;; + *) + fail "make_fixture_engine: unknown anchor var $varname" + ;; + esac + sed -i.bak "s|${old}|${pubkey}|g" "$dest" grep -q "$pubkey" "$dest" || fail "patched engine missing test pubkey" - grep -q "$PROD_PUBKEY" "$dest" && fail "patched engine still has production pubkey" + grep -q "$old" "$dest" && fail "patched engine still has production $varname" + grep -q "$other" "$dest" || fail "patched engine lost the other anchor (must patch exactly one var)" fi rm -f "${dest}.bak" chmod +x "$dest" @@ -128,6 +154,7 @@ OUT1040="$WORKDIR/out410.txt" "$ROUTE_ENG" --version v0.4.10 --dir "$WORKDIR/d410" >"$OUT1040" 2>&1 set -e grep -Eq 'verify-route=sha256sums' "$OUT1040" || fail "v0.4.10 should select sha256sums (log: $(cat "$OUT1040"))" +grep -Eq 'anchor=legacy' "$OUT1040" || fail "v0.4.10 should select legacy anchor (log: $(cat "$OUT1040"))" pass "v0.4.10 → verify-route=sha256sums" set +e @@ -135,6 +162,7 @@ OUT411="$WORKDIR/out411.txt" "$ROUTE_ENG" --version v0.4.11 --dir "$WORKDIR/d411" >"$OUT411" 2>&1 set -e grep -Eq 'verify-route=minisig' "$OUT411" || fail "v0.4.11 should select minisig (log: $(cat "$OUT411"))" +grep -Eq 'anchor=legacy' "$OUT411" || fail "v0.4.11 should select legacy anchor (log: $(cat "$OUT411"))" pass "v0.4.11 → verify-route=minisig" # Production engine ignores inherited BASE_URL / SKIP env (no seams). @@ -166,8 +194,25 @@ minisign -G -W -p "$PUB" -s "$KEY" >/dev/null 2>&1 || TEST_PUBKEY="$(grep -E '^RW' "$PUB" | head -n1 | tr -d '\r\n')" [ -n "$TEST_PUBKEY" ] || fail "could not read test pubkey" +# Second ephemeral pair: post-cutover selection tests need distinct legacy/current keys. +OLDKEY="$WORKDIR/old.key" +OLDPUB="$WORKDIR/old.pub" +minisign -G -W -p "$OLDPUB" -s "$OLDKEY" >/dev/null 2>&1 || + minisign -G -n -p "$OLDPUB" -s "$OLDKEY" >/dev/null 2>&1 || + fail "keygen (legacy stand-in)" +NEWKEY="$WORKDIR/new.key" +NEWPUB="$WORKDIR/new.pub" +minisign -G -W -p "$NEWPUB" -s "$NEWKEY" >/dev/null 2>&1 || + minisign -G -n -p "$NEWPUB" -s "$NEWKEY" >/dev/null 2>&1 || + fail "keygen (current stand-in)" +OLD_EPHEMERAL_PUBKEY="$(grep -E '^RW' "$OLDPUB" | head -n1 | tr -d '\r\n')" +NEW_EPHEMERAL_PUBKEY="$(grep -E '^RW' "$NEWPUB" | head -n1 | tr -d '\r\n')" +[ -n "$OLD_EPHEMERAL_PUBKEY" ] || fail "could not read legacy stand-in pubkey" +[ -n "$NEW_EPHEMERAL_PUBKEY" ] || fail "could not read current stand-in pubkey" +[ "$OLD_EPHEMERAL_PUBKEY" != "$NEW_EPHEMERAL_PUBKEY" ] || fail "stand-in keys must differ" + SRV_ROOT="$WORKDIR/www" -mkdir -p "$SRV_ROOT/v0.4.11" "$SRV_ROOT/v0.4.10" +mkdir -p "$SRV_ROOT/v0.4.11" "$SRV_ROOT/v0.4.10" "$SRV_ROOT/v0.4.12" make_stub_installer() { local dest="$1" @@ -199,6 +244,7 @@ STUB make_stub_installer "$SRV_ROOT/v0.4.11/install-sfetch.sh" make_stub_installer "$SRV_ROOT/v0.4.10/install-sfetch.sh" +make_stub_installer "$SRV_ROOT/v0.4.12/install-sfetch.sh" minisign -S -s "$KEY" -t "test-v0.4.11" -m "$SRV_ROOT/v0.4.11/install-sfetch.sh" ( @@ -237,7 +283,7 @@ PORT="$(cat "$PORTFILE")" BASE="http://127.0.0.1:${PORT}" PATCHED="$WORKDIR/bootstrap-patched.sh" -make_fixture_engine "$PATCHED" "$BASE" "$TEST_PUBKEY" +make_fixture_engine "$PATCHED" "$BASE" "$TEST_PUBKEY" "SFETCH_MINISIGN_PUBKEY_LEGACY" # Positive: v0.4.11 minisig route GOOD411="$WORKDIR/good411" @@ -251,6 +297,7 @@ set -e ROUTE_LINES="$(awk 'BEGIN{c=0} /^route=/{c++} END{print c}' "$OUT_GOOD")" [ "$ROUTE_LINES" -eq 1 ] || fail "expected exactly one stdout route= field, got ${ROUTE_LINES}" grep -q '^route=minisig$' "$OUT_GOOD" || fail "positive minisig machine route missing" +grep -Eq 'anchor=legacy' "$OUT_GOOD" || fail "positive v0.4.11 should log anchor=legacy" [ -f "$GOOD411/.stub-ran" ] || fail "installer should execute after successful verify" [ -x "$GOOD411/sfetch" ] || fail "sfetch binary should be installed" version_output_matches_pin "$("$GOOD411/sfetch" --version 2>&1)" "v0.4.11" || fail "stub sfetch version" @@ -267,12 +314,78 @@ set -e [ "$RC" -eq 0 ] || fail "positive v0.4.10 sha256sums should succeed (log: $(cat "$OUT_GOOD410"))" [ "$(awk 'BEGIN{c=0} /^route=/{c++} END{print c}' "$OUT_GOOD410")" -eq 1 ] || fail "expected one route= field" grep -q '^route=sha256sums$' "$OUT_GOOD410" || fail "positive sha256sums machine route missing" +grep -Eq 'anchor=legacy' "$OUT_GOOD410" || fail "positive v0.4.10 should log anchor=legacy" [ -f "$GOOD410/.stub-ran" ] || fail "installer should execute after sha256sums verify" pass "positive v0.4.10 sha256sums route (patched ephemeral key)" -# Negative: wrong-key minisig (production pubkey engine against test-key sig) +# Negative: old pin must NOT use the current anchor (pin map, not fallback). +# Fixtures are TEST_PUBKEY-signed; patching only the current anchor leaves the +# selected legacy anchor at its production value, so verify must fail. +CURONLY_ENG="$WORKDIR/current-only-engine.sh" +make_fixture_engine "$CURONLY_ENG" "$BASE" "$TEST_PUBKEY" "SFETCH_MINISIGN_PUBKEY" +CURONLY_DIR="$WORKDIR/curonly" +mkdir -p "$CURONLY_DIR" +set +e +OUT_CURONLY="$WORKDIR/out-curonly.txt" +"$CURONLY_ENG" --version v0.4.11 --dir "$CURONLY_DIR" >"$OUT_CURONLY" 2>&1 +RC=$? +set -e +[ "$RC" -ne 0 ] || fail "v0.4.11 against current-only patched engine must fail" +[ ! -f "$CURONLY_DIR/.stub-ran" ] || fail "installer must not run when pin selects unpatched anchor" +[ ! -f "$CURONLY_DIR/sfetch" ] || fail "sfetch must not be installed on failed verify" +grep -Eq 'anchor=legacy' "$OUT_CURONLY" || fail "expected anchor=legacy selection for v0.4.11" +if grep -q '^route=' "$OUT_CURONLY"; then fail "failed run must not emit machine route="; fi +pass "old pin ignores current anchor (no fallback)" + +# Post-cutover selection: fake v0.4.12 pin (MAX patched in temp copy only). +# Stand-in keys prove the rule: new pins use current, previous-key sigs fail. +NEWENG="$WORKDIR/newpin-engine.sh" +make_fixture_engine "$NEWENG" "$BASE" "$OLD_EPHEMERAL_PUBKEY" "SFETCH_MINISIGN_PUBKEY_LEGACY" +sed -i.bak "s|${PROD_PUBKEY}|${NEW_EPHEMERAL_PUBKEY}|g" "$NEWENG" +grep -q "$NEW_EPHEMERAL_PUBKEY" "$NEWENG" || fail "new-pin engine missing current stand-in" +grep -q "$PROD_PUBKEY" "$NEWENG" && fail "new-pin engine still has production current key" +grep -q "$OLD_EPHEMERAL_PUBKEY" "$NEWENG" || fail "new-pin engine lost legacy stand-in" +sed -i.bak 's|SFETCH_BOOTSTRAP_MAX="v0.4.11"|SFETCH_BOOTSTRAP_MAX="v0.4.12"|' "$NEWENG" +grep -q 'SFETCH_BOOTSTRAP_MAX="v0.4.12"' "$NEWENG" || fail "new-pin engine MAX patch failed" +rm -f "${NEWENG}.bak" + +# Positive: post-cutover pin verifies against the current anchor. +minisign -S -s "$NEWKEY" -t "test-v0.4.12-new" -m "$SRV_ROOT/v0.4.12/install-sfetch.sh" +NEWGOOD_DIR="$WORKDIR/newgood" +mkdir -p "$NEWGOOD_DIR" +set +e +OUT_NEWGOOD="$WORKDIR/out-newgood.txt" +"$NEWENG" --version v0.4.12 --dir "$NEWGOOD_DIR" >"$OUT_NEWGOOD" 2>&1 +RC=$? +set -e +[ "$RC" -eq 0 ] || fail "positive v0.4.12 should succeed (log: $(cat "$OUT_NEWGOOD"))" +[ "$(awk 'BEGIN{c=0} /^route=/{c++} END{print c}' "$OUT_NEWGOOD")" -eq 1 ] || fail "expected one route= field" +grep -q '^route=minisig$' "$OUT_NEWGOOD" || fail "positive v0.4.12 machine route missing" +grep -Eq 'anchor=current' "$OUT_NEWGOOD" || fail "positive v0.4.12 should log anchor=current" +[ -f "$NEWGOOD_DIR/.stub-ran" ] || fail "installer should execute after successful verify" +[ -x "$NEWGOOD_DIR/sfetch" ] || fail "sfetch binary should be installed" +version_output_matches_pin "$("$NEWGOOD_DIR/sfetch" --version 2>&1)" "v0.4.12" || fail "stub sfetch version" +pass "post-cutover pin uses current anchor" + +# Negative: previous-key signature must fail for a post-cutover pin (no downgrade). +minisign -S -s "$OLDKEY" -t "test-v0.4.12-old" -m "$SRV_ROOT/v0.4.12/install-sfetch.sh" +NEWDOWNGRADE_DIR="$WORKDIR/newdowngrade" +mkdir -p "$NEWDOWNGRADE_DIR" +set +e +OUT_DOWNGRADE="$WORKDIR/out-downgrade.txt" +"$NEWENG" --version v0.4.12 --dir "$NEWDOWNGRADE_DIR" >"$OUT_DOWNGRADE" 2>&1 +RC=$? +set -e +[ "$RC" -ne 0 ] || fail "previous-key signature for v0.4.12 must fail" +[ ! -f "$NEWDOWNGRADE_DIR/.stub-ran" ] || fail "installer must not run on downgrade attempt" +[ ! -f "$NEWDOWNGRADE_DIR/sfetch" ] || fail "sfetch must not be installed on failed verify" +grep -Eq 'anchor=current' "$OUT_DOWNGRADE" || fail "expected anchor=current selection for v0.4.12" +if grep -q '^route=' "$OUT_DOWNGRADE"; then fail "failed run must not emit machine route="; fi +pass "post-cutover pin rejects previous-key signature (no downgrade)" + +# Negative: wrong-key minisig (production anchors engine against test-key sig) WRONG_ENG="$WORKDIR/wrong-key-engine.sh" -make_fixture_engine "$WRONG_ENG" "$BASE" # keep production pubkey +make_fixture_engine "$WRONG_ENG" "$BASE" # keep production anchors BAD_DIR="$WORKDIR/bad" mkdir -p "$BAD_DIR" set +e @@ -301,7 +414,7 @@ done [ -f "$PORTFILE2" ] || fail "second HTTP fixture server failed to start" PORT2="$(cat "$PORTFILE2")" NOSIG_ENG="$WORKDIR/nosig-engine.sh" -make_fixture_engine "$NOSIG_ENG" "http://127.0.0.1:${PORT2}" "$TEST_PUBKEY" +make_fixture_engine "$NOSIG_ENG" "http://127.0.0.1:${PORT2}" "$TEST_PUBKEY" "SFETCH_MINISIGN_PUBKEY_LEGACY" NOSIG_DIR="$WORKDIR/nosig" mkdir -p "$NOSIG_DIR" set +e @@ -455,7 +568,7 @@ STUB [ -f "$PORTFILE3" ] || fail "goneat HTTP fixture server failed to start" PORT3="$(cat "$PORTFILE3")" GONEAT_ENG="$WORKDIR/goneat-engine.sh" - make_fixture_engine "$GONEAT_ENG" "http://127.0.0.1:${PORT3}" "$TEST_PUBKEY" + make_fixture_engine "$GONEAT_ENG" "http://127.0.0.1:${PORT3}" "$TEST_PUBKEY" "SFETCH_MINISIGN_PUBKEY_LEGACY" GONEAT_DIR="$WORKDIR/goneat-offline" mkdir -p "$GONEAT_DIR" set +e diff --git a/scripts/test-install-anchor-select.sh b/scripts/test-install-anchor-select.sh new file mode 100755 index 0000000..77c4393 --- /dev/null +++ b/scripts/test-install-anchor-select.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Unit/coherence tests for install-sfetch.sh pin-map anchor selection. +# Sources installer functions from a temp copy with the `main "$@"` entry call +# stripped (same bytes as shipped; the bootstrap harness sources engine +# functions the same way). Never executes the installer. No network. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} +pass() { echo "PASS: $*"; } + +INSTALLER="${ROOT}/scripts/install-sfetch.sh" +ENGINE="${ROOT}/scripts/bootstrap-sfetch-verified.sh" +ANCHOR="${ROOT}/scripts/sfetch-minisign-anchor.pub" + +[ -f "$INSTALLER" ] || fail "installer missing" +[ -f "$ENGINE" ] || fail "engine missing" +[ -f "$ANCHOR" ] || fail "anchor SSOT missing" + +# Entry-call strip must match exactly one line; sourcing an unstripped copy +# would execute the installer. +[ "$(grep -c '^main "$@"$' "$INSTALLER")" -eq 1 ] || fail "installer entry line not exactly-once (refusing to source)" +SRC_COPY="$(mktemp "${TMPDIR:-/tmp}/sft-install-src.XXXXXX")" +trap 'rm -f "$SRC_COPY"' EXIT +grep -v '^main "$@"$' "$INSTALLER" >"$SRC_COPY" +grep -q '^main ' "$SRC_COPY" && fail "strip failed: main invocation remains" +# shellcheck source=scripts/install-sfetch.sh +# shellcheck disable=SC1090 +. "$SRC_COPY" + +command -v is_exact_semver_tag >/dev/null || fail "is_exact_semver_tag not loaded" +command -v semver_le_tag >/dev/null || fail "semver_le_tag not loaded" + +# --- tag shape: strict vMAJOR.MINOR.PATCH, no leading zeros --- +is_exact_semver_tag "v0.4.11" || fail "accept v0.4.11" +is_exact_semver_tag "v0.4.6" || fail "accept v0.4.6" +is_exact_semver_tag "v0.0.0" || fail "accept v0.0.0" +is_exact_semver_tag "v10.20.30" || fail "accept multi-digit" +if is_exact_semver_tag ""; then fail "reject empty"; fi +if is_exact_semver_tag "latest"; then fail "reject latest"; fi +if is_exact_semver_tag "v0.4.09"; then fail "reject leading zero v0.4.09"; fi +if is_exact_semver_tag "v0.4"; then fail "reject short v0.4"; fi +if is_exact_semver_tag "v1.2.3.4"; then fail "reject long v1.2.3.4"; fi +if is_exact_semver_tag "1.2.3"; then fail "reject missing v"; fi +if is_exact_semver_tag "v1.2.x"; then fail "reject non-numeric"; fi +pass "installer tag shape strict" + +# --- comparison boundaries against the v0.4.11 cutoff --- +semver_le_tag "v0.4.11" "v0.4.11" || fail "v0.4.11 <= cutoff" +semver_le_tag "v0.4.10" "v0.4.11" || fail "v0.4.10 <= cutoff" +semver_le_tag "v0.4.9" "v0.4.11" || fail "v0.4.9 <= cutoff" +semver_le_tag "v0.4.6" "v0.4.11" || fail "v0.4.6 <= cutoff (Intel recovery pin)" +semver_le_tag "v0.4.0" "v0.4.11" || fail "v0.4.0 <= cutoff" +semver_le_tag "v0.3.99" "v0.4.11" || fail "v0.3.99 <= cutoff" +if semver_le_tag "v0.4.12" "v0.4.11"; then fail "v0.4.12 above cutoff"; fi +if semver_le_tag "v0.4.100" "v0.4.11"; then fail "v0.4.100 above cutoff"; fi +if semver_le_tag "v0.5.0" "v0.4.11"; then fail "v0.5.0 above cutoff"; fi +if semver_le_tag "v1.0.0" "v0.4.11"; then fail "v1.0.0 above cutoff"; fi +if semver_le_tag "v10.0.0" "v0.4.11"; then fail "v10.0.0 above cutoff"; fi +# Huge components must not wrap into range (length-then-lexical, no arithmetic). +if semver_le_tag "v18446744073709551616.4.10" "v0.4.11"; then fail "huge major must not alias below cutoff"; fi +if semver_le_tag "v0.4.18446744073709551615" "v0.4.11"; then fail "huge patch must not alias below cutoff"; fi +if semver_le_tag "v0.18446744073709551616.0" "v0.4.11"; then fail "huge minor must not alias below cutoff"; fi +pass "installer pin-map comparison boundaries" + +# --- constants coherence across consumers --- +[ "${SFETCH_PREVKEY_MAX:-}" = "v0.4.11" ] || fail "installer PREVKEY cutoff must be v0.4.11" +ENG_PREVKEY="$(grep -E '^readonly SFETCH_PREVKEY_MAX=' "$ENGINE" | head -n1 | sed 's/^readonly SFETCH_PREVKEY_MAX="//; s/"$//')" +[ -n "$ENG_PREVKEY" ] || fail "engine PREVKEY cutoff missing" +[ "$ENG_PREVKEY" = "$SFETCH_PREVKEY_MAX" ] || fail "installer/engine PREVKEY cutoff drift ($SFETCH_PREVKEY_MAX vs $ENG_PREVKEY)" +ENG_LEGACY="$(grep -E '^readonly SFETCH_MINISIGN_PUBKEY_LEGACY=' "$ENGINE" | head -n1 | sed 's/^readonly SFETCH_MINISIGN_PUBKEY_LEGACY="//; s/"$//')" +[ -n "$ENG_LEGACY" ] || fail "engine legacy anchor missing" +[ "$ENG_LEGACY" = "${SFETCH_MINISIGN_PUBKEY_LEGACY:-}" ] || fail "installer/engine legacy anchor drift" +[ "${SFETCH_MINISIGN_PUBKEY_LEGACY:-}" != "${SFETCH_MINISIGN_PUBKEY:-}" ] || fail "legacy and current minisign anchors must differ" +[ "${SFETCH_PGP_FPR_LEGACY:-}" != "${SFETCH_PGP_FPR:-}" ] || fail "legacy and current GPG pins must differ" +case "${SFETCH_PGP_FPR_LEGACY:-}" in + "") fail "legacy GPG pin missing" ;; +esac +[[ "${SFETCH_PGP_FPR_LEGACY}" =~ ^[0-9A-F]{40}$ ]] || fail "legacy GPG pin shape (got: ${SFETCH_PGP_FPR_LEGACY})" +[[ "${SFETCH_PGP_FPR:-}" =~ ^[0-9A-F]{40}$ ]] || fail "current GPG pin shape (got: ${SFETCH_PGP_FPR:-})" +pass "installer/engine legacy constants coherent" + +# SSOT carries the current key only (legacy anchor is pin-scoped, never default). +ANCHOR_RW="$(grep -E '^RW' "$ANCHOR" | head -n1 | tr -d '\r\n')" +[ -n "$ANCHOR_RW" ] || fail "anchor has no RW line" +[ "$ANCHOR_RW" = "${SFETCH_MINISIGN_PUBKEY:-}" ] || fail "installer current anchor must equal SSOT" +if grep -q "${SFETCH_MINISIGN_PUBKEY_LEGACY}" "$ANCHOR"; then fail "SSOT must not carry the legacy anchor"; fi +pass "SSOT stays single-current" + +echo "[ok] install anchor-select regression harness complete"