From b1f3a515cc2457298d10765d41fc1d8da6143919 Mon Sep 17 00:00:00 2001 From: Gilcimar Duarte Date: Sat, 1 Aug 2026 22:19:53 -0300 Subject: [PATCH] Add release verification tooling --- .gitignore | 1 + Makefile | 14 +++- README.md | 19 ++++- docs/release-verification.md | 62 ++++++++++++++++ scripts/make-release-checksums.sh | 78 ++++++++++++++++++++ scripts/verify-release.sh | 115 ++++++++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 docs/release-verification.md create mode 100755 scripts/make-release-checksums.sh create mode 100755 scripts/verify-release.sh diff --git a/.gitignore b/.gitignore index 1ec9099..111f7ba 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ Bitflash-*-windows.zip Bitflash-*.AppImage bitflash-node-*-x86_64 SHA256SUMS +SHA256SUMS.asc # Runtime data / logs debug.log diff --git a/Makefile b/Makefile index b0f22f7..23c7d98 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ # make linux build Bitflash-*-x86_64.AppImage # make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64) # make tests build and run standalone unit tests +# make checksums create SHA256SUMS for release assets +# make verify-release TAG=v1.2.13 # make clean remove build artifacts ROOT := $(shell pwd) @@ -160,5 +162,15 @@ clean: tests: $(MAKE) -C src -f Makefile tests +checksums: + ./scripts/make-release-checksums.sh + +sign-checksums: + ./scripts/make-release-checksums.sh --sign $(if $(KEY),--local-user $(KEY),) + +verify-release: + ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) + .PHONY: linux windows clean appimage \ - tests deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx + tests checksums sign-checksums verify-release \ + deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx diff --git a/README.md b/README.md index 28030f1..ed5b335 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,30 @@ configuration — it connects automatically and starts syncing. **Windows:** extract the `-windows.zip` and run `Bitflash.exe`. -Every release ships a `SHA256SUMS` covering both assets. Verifying takes a second +Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second and is worth doing: ```bash sha256sum -c SHA256SUMS ``` +For newer signed releases, verify the checksum file itself first: + +```bash +gpg --verify SHA256SUMS.asc SHA256SUMS +sha256sum -c SHA256SUMS +``` + +The helper below downloads the release assets, verifies `SHA256SUMS.asc` when it +is present, then checks the hashes: + +```bash +scripts/verify-release.sh latest +``` + +See [release verification](docs/release-verification.md) for the full release +audit flow and the maintainer signing step. + **Keep your node current.** Consensus rules have changed since the first releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 added a per-block signature-operation cap. A node on an older build will accept diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 0000000..ecacd5f --- /dev/null +++ b/docs/release-verification.md @@ -0,0 +1,62 @@ +# Release Verification + +Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file +proves that the file you downloaded matches the file the release page names. +A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was +signed by a trusted release key. + +That gives users two separate checks: + +1. `gpg --verify SHA256SUMS.asc SHA256SUMS` checks who signed the checksums. +2. `sha256sum -c SHA256SUMS` checks the binaries against those checksums. + +## User Check + +From a shell with `curl` or `wget` and `sha256sum`: + +```bash +scripts/verify-release.sh latest +``` + +For a specific release: + +```bash +scripts/verify-release.sh v1.2.13 +``` + +For release audits, require the signature: + +```bash +scripts/verify-release.sh v1.2.13 --require-signature +``` + +Older releases may not have `SHA256SUMS.asc`. In that case the script warns and +still checks file integrity. New release audits should use `--require-signature`. + +## Maintainer Flow + +After building release assets in the repository root: + +```bash +scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID +``` + +Upload all built assets plus: + +```text +SHA256SUMS +SHA256SUMS.asc +``` + +Keep the private signing key offline or on a dedicated release machine. Publish +the public key fingerprint in the release notes and keep using the same key for +future releases unless there is a clearly announced rotation. + +## Why This Matters + +`SHA256SUMS` alone protects against a broken download, but not against someone +replacing both a binary and the checksum file. Signing `SHA256SUMS` means an +attacker must also have the release signing key to make the replacement verify. + +This is not reproducible builds yet. It is the smaller, immediate step that +makes every release asset auditable by users before they run it. diff --git a/scripts/make-release-checksums.sh b/scripts/make-release-checksums.sh new file mode 100755 index 0000000..5b29e2d --- /dev/null +++ b/scripts/make-release-checksums.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +sign=0 +key="" + +usage() { + cat <<'EOF' +Usage: scripts/make-release-checksums.sh [--sign] [--local-user KEYID] + +Creates SHA256SUMS for release assets in the current directory: + Bitflash-*-windows.zip + Bitflash-*-x86_64.AppImage + bitflash-node-*-x86_64 + +With --sign, also creates SHA256SUMS.asc as a detached ASCII-armored GPG +signature. Upload both files with the release assets. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --sign) sign=1 ;; + --local-user) + [ "$#" -ge 2 ] || { echo "--local-user needs a key id" >&2; exit 2; } + key="$2" + shift + ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if command -v sha256sum >/dev/null 2>&1; then + sha_cmd=(sha256sum) +elif command -v shasum >/dev/null 2>&1; then + sha_cmd=(shasum -a 256) +else + echo "missing required command: sha256sum or shasum" >&2 + exit 1 +fi + +assets=() +for pattern in \ + "Bitflash-*-windows.zip" \ + "Bitflash-*-x86_64.AppImage" \ + "bitflash-node-*-x86_64" +do + for file in $pattern; do + [ -e "$file" ] || continue + assets+=("$file") + done +done + +if [ "${#assets[@]}" -eq 0 ]; then + echo "no release assets found in $(pwd)" >&2 + exit 1 +fi + +printf '%s\n' "${assets[@]}" | LC_ALL=C sort | while IFS= read -r file; do + "${sha_cmd[@]}" "$file" +done > SHA256SUMS + +echo "Wrote SHA256SUMS" + +if [ "$sign" -eq 1 ]; then + if ! command -v gpg >/dev/null 2>&1; then + echo "missing required command: gpg" >&2 + exit 1 + fi + gpg_args=(--armor --detach-sign --output SHA256SUMS.asc) + if [ -n "$key" ]; then + gpg_args+=(--local-user "$key") + fi + gpg "${gpg_args[@]}" SHA256SUMS + echo "Wrote SHA256SUMS.asc" +fi diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh new file mode 100755 index 0000000..23e71d1 --- /dev/null +++ b/scripts/verify-release.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="Bitflash-sh/bitflash" +tag="${1:-latest}" +require_signature=0 + +usage() { + cat <<'EOF' +Usage: scripts/verify-release.sh [tag|latest] [--require-signature] + +Downloads the release checksum file and every asset named in it, then verifies: + 1. SHA256SUMS.asc, if present, against SHA256SUMS with gpg. + 2. SHA256SUMS against the downloaded assets. + +Use --require-signature for release audits. Without it, older releases that +ship only SHA256SUMS are checked for integrity and reported as unsigned. +EOF +} + +for arg in "${@:2}"; do + case "$arg" in + --require-signature) require_signature=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; + esac +done + +if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then + usage + exit 0 +fi + +need() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command: $1" >&2 + exit 1 + fi +} + +need sed +need awk + +if command -v sha256sum >/dev/null 2>&1; then + sha_check=(sha256sum -c SHA256SUMS) +elif command -v shasum >/dev/null 2>&1; then + sha_check=(shasum -a 256 -c SHA256SUMS) +else + echo "missing required command: sha256sum or shasum" >&2 + exit 1 +fi + +download() { + local url="$1" + local out="$2" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "$out" + elif command -v wget >/dev/null 2>&1; then + wget -q "$url" -O "$out" + else + echo "missing required command: curl or wget" >&2 + exit 1 + fi +} + +if [ "$tag" = "latest" ]; then + tag="$(download "https://api.github.com/repos/$repo/releases/latest" - | + sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | + head -1)" + if [ -z "$tag" ]; then + echo "could not resolve latest release tag" >&2 + exit 1 + fi +fi + +workdir="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-release-verify.XXXXXX")" +cleanup() { rm -rf "$workdir"; } +trap cleanup EXIT + +base_url="https://github.com/$repo/releases/download/$tag" + +echo "Verifying Bitflash $tag" +echo "Working directory: $workdir" +cd "$workdir" + +download "$base_url/SHA256SUMS" SHA256SUMS + +if download "$base_url/SHA256SUMS.asc" SHA256SUMS.asc 2>/dev/null; then + if ! command -v gpg >/dev/null 2>&1; then + echo "SHA256SUMS.asc exists, but gpg is not installed" >&2 + exit 1 + fi + gpg --verify SHA256SUMS.asc SHA256SUMS + echo "Signature OK" +else + if [ "$require_signature" -eq 1 ]; then + echo "release does not publish SHA256SUMS.asc" >&2 + exit 1 + fi + echo "WARNING: no SHA256SUMS.asc found; checking hashes only" +fi + +awk '{print $2}' SHA256SUMS | while IFS= read -r file; do + file="${file#\*}" + file="${file#./}" + [ -z "$file" ] && continue + case "$file" in + */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; + esac + echo "Downloading $file" + download "$base_url/$file" "$file" +done + +"${sha_check[@]}" +echo "Release assets OK"