Files
privatae-bastion/deploy/release-assets.sh
T
Privatae 8822d5df85
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s
bastion 0.55.3
2026-10-25 23:16:45 +00:00

83 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Rebuild a published release from its tag and collect the files its GitHub
# release carries, refusing unless they are exactly what was signed.
#
# deploy/release-assets.sh v<version> <out dir>
#
# Needs git, docker, curl and python3, and a clone of this repository that has
# the tag. It needs no key or account of ours. .github/workflows/release.yml
# runs it for every release tag; anyone can run it to check a release.
#
# The tag must be annotated, with the line `built from <commit>` that names
# the commit the release was built from in the development repository. The
# tag's own deploy/build.sh rebuilds the binaries (a pinned toolchain image,
# so the bytes are reproducible), deploy/manifest.py writes the release
# manifest from them, and the signature published beside the manifest, which
# is named by the manifest's digest and never removed, must verify against
# the release keys pinned in this checkout and at the tag. A rebuild that
# differs by one byte gives another digest, no signature by that name, and
# nothing in <out dir>. When the tag is the release the public manifest names
# now, the rebuilt manifest must also equal that one byte for byte.
#
# <out dir> gets the six binaries, version.json (the signed manifest),
# version.json.<first 16 hex of its sha256>.sig (its signature, the file every
# bastion's self-update checks) and SHA256SUMS (the binaries and the manifest,
# in the format `sha256sum -c` reads; it is not signed itself).
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
TAG="${1:?usage: deploy/release-assets.sh v<version> <out dir>}"
OUT="${2:?usage: deploy/release-assets.sh v<version> <out dir>}"
BASE="${MANIFEST_BASE:-https://privatae.ai/bastion}"
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "not a release tag: $TAG" >&2; exit 2; }
VER="${TAG#v}"
[ ! -e "$OUT" ] || [ -z "$(ls -A "$OUT")" ] || { echo "$OUT is not empty" >&2; exit 2; }
git -C "$HERE" rev-parse -q --verify "refs/tags/$TAG" >/dev/null || { echo "no tag $TAG in this clone" >&2; exit 1; }
[ "$(git -C "$HERE" cat-file -t "refs/tags/$TAG")" = tag ] || { echo "$TAG is not an annotated tag" >&2; exit 1; }
FROM="$(git -C "$HERE" tag -l --format='%(contents)' "$TAG" | sed -n 's/^built from \([0-9a-f]\{7,40\}\)$/\1/p')"
[ "$(printf '%s\n' "$FROM" | grep -c .)" = 1 ] || { echo "$TAG does not carry exactly one 'built from <commit>' line" >&2; exit 1; }
SRC="$(git -C "$HERE" rev-parse "refs/tags/$TAG^{commit}")"
[ "$(git -C "$HERE" show "$SRC:deploy/VERSION" | tr -d '[:space:]')" = "$VER" ] \
|| { echo "deploy/VERSION at $TAG is not $VER" >&2; exit 1; }
echo "release: $TAG at $SRC, built from $FROM"
WORK="$(mktemp -d)"
trap 'git -C "$HERE" worktree remove --force "$WORK/src" >/dev/null 2>&1 || true; rm -rf "$WORK"' EXIT
git -C "$HERE" worktree add -q --detach "$WORK/src" "$SRC"
"$WORK/src/deploy/build.sh" "$VER"
DIST="$WORK/src/client-go/dist"
python3 "$HERE/deploy/manifest.py" "$VER" "$FROM" "$DIST" > "$WORK/version.json"
H="$(python3 -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest()[:16])' "$WORK/version.json")"
SIG="version.json.$H.sig"
curl -fsS --max-time 30 "$BASE/$SIG" -o "$WORK/$SIG" \
|| { echo "no signature is published for the rebuilt manifest ($SIG): the rebuild is not the signed release" >&2; exit 1; }
SIGNED_BY="$("$HERE/deploy/release-verify.sh" bastion "$WORK/version.json" "$WORK/$SIG" "$SRC")" \
|| { echo "the signature $SIG does not verify against the rebuilt manifest" >&2; exit 1; }
echo "signature: ok ($SIGNED_BY)"
if curl -fsS --max-time 30 "$BASE/version.json" -o "$WORK/live.json"; then
LIVE="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["version"])' "$WORK/live.json")"
if [ "$LIVE" = "$VER" ]; then
cmp -s "$WORK/live.json" "$WORK/version.json" \
|| { echo "the public manifest names $VER but differs from the rebuilt one" >&2; exit 1; }
echo "the public manifest names $VER and is byte for byte the rebuilt one"
else
echo "the public manifest names $LIVE; $VER is checked by its signature alone"
fi
fi
mkdir -p "$OUT"
cp "$DIST"/privatae-bastion-* "$WORK/version.json" "$WORK/$SIG" "$OUT/"
(cd "$OUT" && python3 - <<'PY'
import hashlib, os
names = sorted(f for f in os.listdir(".") if f.startswith("privatae-bastion-")) + ["version.json"]
with open("SHA256SUMS", "w") as fh:
for n in names:
fh.write("%s %s\n" % (hashlib.sha256(open(n, "rb").read()).hexdigest(), n))
PY
)
n="$(ls "$OUT" | grep -c '^privatae-bastion-')"
[ "$n" = 6 ] || { echo "expected 6 binaries, built $n" >&2; exit 1; }
echo "assets for $TAG in $OUT:"
ls -l "$OUT"