Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ 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
Expand Down Expand Up @@ -271,6 +273,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
Expand Down
6 changes: 4 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ 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
```

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.
Expand Down
15 changes: 13 additions & 2 deletions scripts/assert-bootstrap-range-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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}"
32 changes: 28 additions & 4 deletions scripts/bootstrap-sfetch-verified.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,7 +53,17 @@ 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"

# 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"
Expand Down Expand Up @@ -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"
}

Expand Down
59 changes: 54 additions & 5 deletions scripts/install-sfetch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ 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"
# 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"

# -----------------------------------------------------------------------------
Expand All @@ -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=""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:-<empty>}"
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
Expand Down
4 changes: 2 additions & 2 deletions scripts/sfetch-minisign-anchor.pub
Original file line number Diff line number Diff line change
@@ -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
Loading