From f6c5b813075cae4228d241a3567e70b210504749 Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:18:01 -0300 Subject: Add release verification tooling (#108) Co-authored-by: Gilcimar Duarte --- scripts/verify-release.sh | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100755 scripts/verify-release.sh (limited to 'scripts/verify-release.sh') 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" -- cgit v1.2.3 From c3f6b68f907b03865d2c417ee39fe9526787196f Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 6 Aug 2026 10:46:26 -0300 Subject: release: sign SHA256SUMS with a pinned key, verify against it verify-release.sh embeds the Bitflash release signing key and now requires that the checksum signature validate against that exact key, not merely against some key in the caller's keyring (#138). It also fetches releases from releases.bitflash.network instead of the retired GitHub host, resolving 'latest' from latest.txt. Release signing key fingerprint: 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8 --- docs/release-verification.md | 78 ++++++++++++++++++++----------- scripts/verify-release.sh | 108 +++++++++++++++++++++++-------------------- 2 files changed, 108 insertions(+), 78 deletions(-) (limited to 'scripts/verify-release.sh') diff --git a/docs/release-verification.md b/docs/release-verification.md index ecacd5f..32de74f 100644 --- a/docs/release-verification.md +++ b/docs/release-verification.md @@ -3,60 +3,82 @@ 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. +signed by the Bitflash 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. +1. `gpg --verify SHA256SUMS.asc SHA256SUMS` — who signed the checksums. +2. `sha256sum -c SHA256SUMS` — the binaries against those checksums. -## User Check +But the first check is only worth something if you require a **specific** key. +A good signature by *some* key proves nothing; anyone can make one. -From a shell with `curl` or `wget` and `sha256sum`: +## The release signing key -```bash -scripts/verify-release.sh latest +```text +fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8 +uid Bitflash Releases (bitflash.network release signing) + ``` -For a specific release: +The public key is published at +`https://releases.bitflash.network/bitflash-release-key.asc` and embedded in +`scripts/verify-release.sh`, so the tool's trust anchor lives in this +repository rather than on the server it verifies. + +## User check + +From a shell with `curl` or `wget`, `sha256sum` and `gpg`: ```bash -scripts/verify-release.sh v1.2.13 +scripts/verify-release.sh latest ``` -For release audits, require the signature: +For a specific release: ```bash -scripts/verify-release.sh v1.2.13 --require-signature +scripts/verify-release.sh v1.2.15 ``` -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 +The script downloads the release, imports the pinned key into a throwaway +keyring, and **refuses anything that is not a valid signature by the key above** +before checking the hashes. Releases from before signing was introduced ship +only `SHA256SUMS`; without `--require-signature` those are checked for integrity +and reported as unsigned. -After building release assets in the repository root: +By hand, if you would rather not run the script: ```bash -scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID +curl -O https://releases.bitflash.network/bitflash-release-key.asc +gpg --import bitflash-release-key.asc +gpg --verify SHA256SUMS.asc SHA256SUMS # must name the fingerprint above +sha256sum -c SHA256SUMS ``` -Upload all built assets plus: +## Maintainer flow -```text -SHA256SUMS -SHA256SUMS.asc +The signing key lives on a dedicated location, not in the build tree. After +building the release assets in the repository root, write and sign the +checksums: + +```bash +sha256sum Bitflash-*.zip Bitflash-*.AppImage bitflash-node-* > SHA256SUMS +gpg --armor --detach-sign --output SHA256SUMS.asc SHA256SUMS ``` -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. +Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update +`latest.txt` on the releases host to the new tag. Keep using the same key; if it +is ever rotated, announce the new fingerprint clearly and update the embedded +key in `verify-release.sh`. -## Why This Matters +## 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. +replacing both a binary and the checksum file. Signing `SHA256SUMS`, and +requiring a pinned key, means an attacker must also hold the release signing key +to make the replacement verify. Serving from bitflash.network makes this the +guarantee that matters: without a pinned key, "trust the site" is the only +thing standing behind a download. This is not reproducible builds yet. It is the smaller, immediate step that -makes every release asset auditable by users before they run it. +makes every release asset auditable before anyone runs it. diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 23e71d1..4fd4558 100755 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -1,23 +1,50 @@ #!/usr/bin/env bash set -euo pipefail -repo="Bitflash-sh/bitflash" -tag="${1:-latest}" +# Verifies a Bitflash release: the checksum file's signature against a pinned +# key, then the assets against the checksums. +# +# The signing key is embedded below, not fetched. That is the whole point of a +# pinned key: the trust anchor ships with this script (in git), so it does not +# depend on the same server that serves the release. A server that is +# compromised can swap the binaries and the checksums, but it cannot forge a +# signature by this key. + +host="https://releases.bitflash.network" require_signature=0 +# The Bitflash release signing key. Verification requires this exact key -- +# gpg's own "good signature" is not enough, because a good signature by *some* +# key proves nothing. See issue #138. +RELEASE_KEY_FP="910A2B4CCA879E81FB4B2AEA1D4A53D3B78AA4B8" +read -r -d '' RELEASE_KEY <<'KEY' || true +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEanSOmRYJKwYBBAHaRw8BAQdAXj24i2BYUcAmOpoXwjE4ZTynHsifCZ+niwQK +HqO0nMa0UEJpdGZsYXNoIFJlbGVhc2VzIChiaXRmbGFzaC5uZXR3b3JrIHJlbGVh +c2Ugc2lnbmluZykgPHJlbGVhc2VzQGJpdGZsYXNoLm5ldHdvcms+iJAEExYKADgW +IQSRCitMyoeegftLKuodSlPTt4qkuAUCanSOmQIbAwULCQgHAgYVCgkICwIEFgID +AQIeAQIXgAAKCRAdSlPTt4qkuIi9AP96X3OcstiCTTgKOr16Vhds3LyuZ19q3Apq +4KAcNuOUMAD+NqgJ1CqkLGPXfZLCtYBCRHXNYWxMMEk+ViLcRtdLMg4= +=mxic +-----END PGP PUBLIC KEY BLOCK----- +KEY + 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. + 1. SHA256SUMS.asc against SHA256SUMS, requiring the pinned Bitflash release + key (fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8). 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. +Releases from before signing was introduced ship only SHA256SUMS; without +--require-signature those are checked for integrity and reported as unsigned. EOF } +tag="${1:-latest}" for arg in "${@:2}"; do case "$arg" in --require-signature) require_signature=1 ;; @@ -25,19 +52,9 @@ for arg in "${@:2}"; do *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; esac done +if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then usage; exit 0; fi -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() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }; } need sed need awk @@ -46,39 +63,25 @@ if command -v sha256sum >/dev/null 2>&1; then 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 + 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 command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then wget -q "$1" -O "$2" + 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 + tag="$(download "$host/latest.txt" - 2>/dev/null | tr -d '[:space:]' || true)" + [ -n "$tag" ] || { echo "could not resolve the latest release tag from $host/latest.txt" >&2; exit 1; } 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" - +base_url="$host/$tag" echo "Verifying Bitflash $tag" echo "Working directory: $workdir" cd "$workdir" @@ -86,27 +89,32 @@ 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 + need gpg + # Verify in a throwaway keyring holding ONLY the pinned key, and require that + # the signature validates against it with the pinned fingerprint. This is the + # difference between "signed" and "signed by Bitflash". + keyring="$workdir/keyring" + mkdir -p "$keyring"; chmod 700 "$keyring" + printf '%s\n' "$RELEASE_KEY" | GNUPGHOME="$keyring" gpg --batch --quiet --import 2>/dev/null + status="$(GNUPGHOME="$keyring" gpg --batch --status-fd=1 --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null || true)" + if printf '%s' "$status" | grep -q "VALIDSIG.*$RELEASE_KEY_FP"; then + echo "Signature OK -- signed by the pinned Bitflash release key" + else + echo "SIGNATURE REJECTED: not a valid signature by the pinned release key" >&2 + echo " expected key: $RELEASE_KEY_FP" >&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 + echo "release does not publish SHA256SUMS.asc" >&2; exit 1 fi - echo "WARNING: no SHA256SUMS.asc found; checking hashes only" + echo "WARNING: no SHA256SUMS.asc found; checking hashes only (unsigned release)" fi awk '{print $2}' SHA256SUMS | while IFS= read -r file; do - file="${file#\*}" - file="${file#./}" + file="${file#\*}"; file="${file#./}" [ -z "$file" ] && continue - case "$file" in - */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; - esac + case "$file" in */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; esac echo "Downloading $file" download "$base_url/$file" "$file" done -- cgit v1.2.3