banger/scripts/publish-banger-release.sh
Thales Maciel 3d748b87c8
publish-script: fix pubkey extraction and cosign v3 compatibility
Two bugs found while dry-running the publish flow end-to-end:

1. The awk pipeline that pulled BangerReleasePublicKey out of
   verify_signature.go didn't strip Go's raw-string-literal wrapping
   (`var ... = ` + backtick on the BEGIN line, trailing backtick on
   the END line). The "verify against embedded pub key" step thus
   compared sigs against a malformed PEM. Replaced with a sed pair
   that yields a clean PEM block byte-identical to cosign.pub.

2. cosign v3.x defaults sign-blob to a new bundle format and
   pushes signatures to Rekor; both are incompatible with banger's
   "embedded pub key, raw ASN.1 DER signature" trust model.
   Add --use-signing-config=false / --tlog-upload=false /
   --new-bundle-format=false to opt out, and --insecure-ignore-tlog
   on verify-blob. These flags also work on cosign v2.x, so the
   script is forward- and backward-compatible across the v2→v3
   boundary.

Validated by an end-to-end dry-run on this machine: built binaries,
tarred, sha256summed, cosign-signed, verified against the embedded
pub key, then re-verified through internal/updater's
crypto/ecdsa.VerifyASN1 path — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:23:09 -03:00

195 lines
7.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# publish-banger-release.sh
#
# Cut and publish a banger release tarball + SHA256SUMS + cosign
# signature to the R2 bucket consumed by `banger update`.
#
# Usage:
# scripts/publish-banger-release.sh v0.1.0
#
# Environment overrides:
# COSIGN_KEY path to the cosign private key (default: cosign.key)
# RCLONE_REMOTE rclone remote name (default: releases)
# BUCKET_PATH object-key prefix in the bucket (default: banger)
# BASE_URL public URL prefix for objects (default: https://releases.thaloco.com)
# SKIP_UPLOAD set to 1 to stage everything locally without rclone upload
#
# Prerequisites:
# * cosign in PATH (https://github.com/sigstore/cosign)
# * rclone in PATH, configured with a remote named ${RCLONE_REMOTE}
# pointing at the R2 bucket served at ${BASE_URL}.
# * A cosign keypair already generated. The public key MUST already
# be embedded in internal/updater/verify_signature.go's
# BangerReleasePublicKey constant — running this script with a
# placeholder key would publish a release no installed banger can
# verify.
#
# Output (under build/release/<version>/):
# banger-<version>-linux-amd64.tar.gz
# SHA256SUMS
# SHA256SUMS.sig
# manifest.json (the freshly-mutated copy uploaded to the bucket)
set -euo pipefail
log() { printf '[publish-banger-release] %s\n' "$*" >&2; }
die() { log "$*"; exit 1; }
if [[ $# -lt 1 ]]; then
die "usage: $0 <version> (e.g. $0 v0.1.0)"
fi
VERSION="$1"
case "$VERSION" in
v*.*.*) ;;
*) die "version must look like vX.Y.Z, got $VERSION" ;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COSIGN_KEY="${COSIGN_KEY:-cosign.key}"
RCLONE_REMOTE="${RCLONE_REMOTE:-releases}"
BUCKET_PATH="${BUCKET_PATH:-banger}"
BASE_URL="${BASE_URL:-https://releases.thaloco.com}"
SKIP_UPLOAD="${SKIP_UPLOAD:-0}"
command -v cosign >/dev/null || die "cosign not in PATH"
command -v rclone >/dev/null || die "rclone not in PATH"
command -v sha256sum >/dev/null || die "sha256sum not in PATH"
command -v jq >/dev/null || die "jq not in PATH"
[[ -f "$COSIGN_KEY" ]] || die "cosign key not found at $COSIGN_KEY (override with COSIGN_KEY=...)"
cd "$REPO_ROOT"
OUT_DIR="$REPO_ROOT/build/release/$VERSION"
TARBALL_NAME="banger-$VERSION-linux-amd64.tar.gz"
TARBALL_PATH="$OUT_DIR/$TARBALL_NAME"
log "preparing $OUT_DIR"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
log "building binaries with version=$VERSION"
COMMIT="$(git rev-parse HEAD)"
BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LDFLAGS="-X banger/internal/buildinfo.Version=$VERSION \
-X banger/internal/buildinfo.Commit=$COMMIT \
-X banger/internal/buildinfo.BuiltAt=$BUILT_AT"
BUILD_STAGE="$OUT_DIR/stage"
mkdir -p "$BUILD_STAGE"
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger" ./cmd/banger
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/bangerd" ./cmd/bangerd
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger-vsock-agent" \
./cmd/banger-vsock-agent
log "tarring → $TARBALL_PATH"
# -C into the stage dir so the tarball's root entries are bare
# basenames (banger, bangerd, banger-vsock-agent) — the updater's
# StageTarball validator rejects anything else.
tar -czf "$TARBALL_PATH" -C "$BUILD_STAGE" \
banger bangerd banger-vsock-agent
log "computing SHA256SUMS"
(
cd "$OUT_DIR"
sha256sum "$TARBALL_NAME" > SHA256SUMS
cat SHA256SUMS
) >&2
log "cosign sign-blob → SHA256SUMS.sig"
# Flag rationale (cosign v3.x):
# --use-signing-config=false bypasses the new signing-config flow that
# otherwise insists on bundle output + Rekor.
# --tlog-upload=false skip the public transparency log; banger's
# trust model is "embedded public key", not
# "Rekor lookup", so the log adds nothing.
# --new-bundle-format=false emit a bare base64 ASN.1 DER signature,
# which is what internal/updater consumes
# via crypto/ecdsa.VerifyASN1.
# These flags also work on cosign v2.x, so the script is forward- and
# backward-compatible across the v2→v3 boundary.
COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" \
cosign sign-blob --yes \
--key "$COSIGN_KEY" \
--use-signing-config=false \
--tlog-upload=false \
--new-bundle-format=false \
--output-signature "$OUT_DIR/SHA256SUMS.sig" \
"$OUT_DIR/SHA256SUMS"
log "verifying signature against the embedded public key"
EMBEDDED_PUB="$OUT_DIR/embedded-pubkey.pem"
# verify_signature.go embeds the PEM inside a Go raw-string literal, so the
# BEGIN line is prefixed with `var ... = ` + backtick and the END line has a
# trailing backtick. Strip those so the result is a clean PEM.
sed -n '/-----BEGIN PUBLIC KEY-----/,/-----END PUBLIC KEY-----/p' \
"$REPO_ROOT/internal/updater/verify_signature.go" \
| sed -E 's/.*(-----BEGIN PUBLIC KEY-----)/\1/; s/(-----END PUBLIC KEY-----).*/\1/' \
> "$EMBEDDED_PUB"
if grep -q PLACEHOLDER "$EMBEDDED_PUB"; then
die "BangerReleasePublicKey is the placeholder in verify_signature.go; replace it with cosign.pub before publishing"
fi
cosign verify-blob \
--key "$EMBEDDED_PUB" \
--insecure-ignore-tlog \
--signature "$OUT_DIR/SHA256SUMS.sig" \
"$OUT_DIR/SHA256SUMS"
# Build the manifest. Pull the existing manifest from the bucket so
# we don't lose previous release entries, append this one, bump
# latest_stable, write back.
log "fetching existing manifest"
PREV_MANIFEST="$OUT_DIR/manifest.previous.json"
if curl -fsSL "$BASE_URL/$BUCKET_PATH/manifest.json" -o "$PREV_MANIFEST" 2>/dev/null; then
log " found previous manifest"
else
log " no previous manifest (first release); seeding"
printf '{"schema_version":1,"latest_stable":"","releases":[]}' > "$PREV_MANIFEST"
fi
NEW_MANIFEST="$OUT_DIR/manifest.json"
RELEASED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
jq --arg version "$VERSION" \
--arg tarball_url "$BASE_URL/$BUCKET_PATH/$VERSION/$TARBALL_NAME" \
--arg sums_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS" \
--arg sig_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS.sig" \
--arg released_at "$RELEASED_AT" \
'
.schema_version = 1
| .latest_stable = $version
| .releases = (
(.releases // [])
| map(select(.version != $version))
| . + [{
"version": $version,
"tarball_url": $tarball_url,
"sha256sums_url": $sums_url,
"sha256sums_sig_url": $sig_url,
"released_at": $released_at
}]
)
' "$PREV_MANIFEST" > "$NEW_MANIFEST"
log "manifest:"
jq '.' "$NEW_MANIFEST" >&2
if [[ "$SKIP_UPLOAD" == "1" ]]; then
log "SKIP_UPLOAD=1, not uploading. Artifacts staged under $OUT_DIR"
exit 0
fi
log "uploading to $RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$TARBALL_PATH" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS.sig" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
log "uploading manifest"
rclone copy "$NEW_MANIFEST" "$RCLONE_REMOTE:$BUCKET_PATH/"
log "done. verify with:"
log " curl -fsSL $BASE_URL/$BUCKET_PATH/manifest.json | jq ."
log " banger update --check"