feat(release): sign release checksums (ed25519), enforce + bake pubkey
Releases were shipping UNSIGNED: ship.sh never invoked sign-checksums, the goreleaser pubkey ldflag defaulted to empty, and publish-cli-release.sh did not upload a .sig — so the self-updater's signature check was silently skipped (1.0.0-beta had no checksums.txt.sig). Make signing unconditional: - internal/upgrade/signature.go: bake the canonical release public key as the compiled-in default (public, safe to commit; removes the empty-env footgun). - .goreleaser.yml: drop the pubkey ldflag (committed default is authoritative) + add a signs: block that runs scripts/sign-checksums over checksums.txt. sign-checksums requires -key, so an unset RELEASE_SIGNING_KEY fails the build instead of shipping unsigned. - scripts/ship.sh: source RELEASE_SIGNING_KEY from ~/.config/unarr-release/signing.key (or the env), die if absent, and assert checksums.txt.sig was produced. Private key lives outside the repo (gitignored keyfile + operator's vault); public key verified to match (priv[32:] == baked pubkey).
This commit is contained in:
parent
547b0d4e37
commit
1757bdabf5
3 changed files with 59 additions and 18 deletions
|
|
@ -26,10 +26,10 @@ builds:
|
|||
- -s -w
|
||||
- -X github.com/torrentclaw/unarr/internal/cmd.Version={{.Version}}
|
||||
- -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }}
|
||||
# Release-signing public key — verified by the self-updater against
|
||||
# checksums.txt.sig. Empty when not configured; in that case
|
||||
# signature verification is skipped and a warning is logged.
|
||||
- -X github.com/torrentclaw/unarr/internal/upgrade.releasePubKeyBase64={{ .Env.RELEASE_SIGNING_PUBKEY }}
|
||||
# The release-signing PUBLIC key is compiled in as the canonical default
|
||||
# in internal/upgrade/signature.go (it's public — committing it removes
|
||||
# the "empty env var → unsigned binary" footgun). No ldflag override:
|
||||
# every build bakes the same key and verifies checksums.txt.sig.
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
|
|
@ -51,6 +51,28 @@ archives:
|
|||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
# Sign checksums.txt with the release ed25519 private key → checksums.txt.sig,
|
||||
# verified by the self-updater against the compiled-in public key. Releases are
|
||||
# signed UNCONDITIONALLY: sign-checksums requires -key, so an unset/empty
|
||||
# RELEASE_SIGNING_KEY makes this step (and the whole `goreleaser release`) fail
|
||||
# rather than silently shipping an unsigned release. ship.sh sources the key
|
||||
# from ~/.config/unarr-release/signing.key (or the RELEASE_SIGNING_KEY env).
|
||||
signs:
|
||||
- id: checksums
|
||||
cmd: go
|
||||
args:
|
||||
- run
|
||||
- ./scripts/sign-checksums
|
||||
- -key
|
||||
- "{{ .Env.RELEASE_SIGNING_KEY }}"
|
||||
- -in
|
||||
- "${artifact}"
|
||||
- -out
|
||||
- "${signature}"
|
||||
signature: "${artifact}.sig"
|
||||
artifacts: checksum
|
||||
output: true
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
|
|
|||
|
|
@ -14,17 +14,18 @@ import (
|
|||
// releasePubKeyBase64 is the base64-encoded ed25519 public key used to verify
|
||||
// `checksums.txt.sig` against `checksums.txt` during self-update.
|
||||
//
|
||||
// It is overridable at link time via ldflags so the same source compiles for
|
||||
// users who do not yet have a release-signing keypair in their CI:
|
||||
// It is the canonical release-signing public key, compiled in so every build
|
||||
// (local ship.sh and CI alike) verifies updates consistently — it is public, so
|
||||
// committing it is safe and removes any "forgot to set the env var → shipped an
|
||||
// unsigned/unverifying binary" failure mode. The matching PRIVATE key signs
|
||||
// checksums.txt during release (scripts/sign-checksums, driven by the
|
||||
// goreleaser `signs:` block); releases are signed unconditionally now.
|
||||
//
|
||||
// -X github.com/torrentclaw/unarr/internal/upgrade.releasePubKeyBase64=<base64-pubkey>
|
||||
//
|
||||
// When the variable is empty, signature verification is skipped and a warning
|
||||
// is logged — checksum-only verification remains in force. This is the
|
||||
// transitional default until the keypair is provisioned; flip to a non-empty
|
||||
// value (and enable the corresponding CI signing step) to make signature
|
||||
// verification mandatory.
|
||||
var releasePubKeyBase64 = ""
|
||||
// When this is empty, signature verification is skipped (a warning is logged).
|
||||
// Do NOT clear it — every release from v1.0.1-beta on ships a checksums.txt.sig
|
||||
// and clients built with this key require it. Rotating the key is a coordinated
|
||||
// change: clients on the old key must update before the signing key flips.
|
||||
var releasePubKeyBase64 = "X7EJVwAiIILs4EGaqp+YBsa4Q6HnKBB2J5FI4MIt+w0="
|
||||
|
||||
// ErrMissingSignature indicates the release does not ship a `.sig` file even
|
||||
// though signature verification is required by an embedded public key.
|
||||
|
|
|
|||
|
|
@ -117,16 +117,34 @@ if [ "$DRY_RUN" = false ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Release signing key — releases MUST be signed (the goreleaser `signs:` block
|
||||
# consumes RELEASE_SIGNING_KEY to produce checksums.txt.sig, verified by the
|
||||
# compiled-in public key). Prefer an explicit env var, else the local keyfile.
|
||||
SIGNING_KEY_FILE="${RELEASE_SIGNING_KEY_FILE:-$HOME/.config/unarr-release/signing.key}"
|
||||
if [ -z "${RELEASE_SIGNING_KEY:-}" ] && [ -f "$SIGNING_KEY_FILE" ]; then
|
||||
RELEASE_SIGNING_KEY="$(tr -d '\r\n' < "$SIGNING_KEY_FILE")"
|
||||
fi
|
||||
if [ -z "${RELEASE_SIGNING_KEY:-}" ]; then
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
warn "no signing key (RELEASE_SIGNING_KEY env or $SIGNING_KEY_FILE) — a real ship would FAIL: releases must be signed"
|
||||
else
|
||||
die "no release signing key: export RELEASE_SIGNING_KEY or create $SIGNING_KEY_FILE — releases MUST be signed"
|
||||
fi
|
||||
fi
|
||||
export RELEASE_SIGNING_KEY
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
ok "Dry run complete — no changes made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Build
|
||||
info "goreleaser build ($TAG)"
|
||||
SENTRY_DSN="${SENTRY_DSN:-}" RELEASE_SIGNING_PUBKEY="${RELEASE_SIGNING_PUBKEY:-}" \
|
||||
# 1. Build (+ sign checksums via the goreleaser `signs:` block, which consumes
|
||||
# RELEASE_SIGNING_KEY — exported above; missing key already aborted the run).
|
||||
info "goreleaser build + sign ($TAG)"
|
||||
SENTRY_DSN="${SENTRY_DSN:-}" RELEASE_SIGNING_KEY="$RELEASE_SIGNING_KEY" \
|
||||
goreleaser release --clean --skip=publish
|
||||
ok "dist/ ready"
|
||||
[ -f dist/checksums.txt.sig ] || die "checksums.txt.sig not produced — signing step did not run"
|
||||
ok "dist/ ready (checksums.txt + checksums.txt.sig)"
|
||||
|
||||
# 2. Hetzner
|
||||
if [ "$SKIP_HETZNER" != "1" ]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue