upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <admin@bitflash.network>2026-08-06 10:46:26 -0300
committerBitflash <admin@bitflash.network>2026-08-06 10:46:26 -0300
commitc3f6b68f907b03865d2c417ee39fe9526787196f (patch)
tree98a7c58fdf511d3cf98aae0811913afa51cd0c24
parentb3a5a858e536acf65e6c6761293626628f2227bf (diff)
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
-rw-r--r--docs/release-verification.md78
-rwxr-xr-xscripts/verify-release.sh108
2 files changed, 108 insertions, 78 deletions
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 @@
3Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file 3Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file
4proves that the file you downloaded matches the file the release page names. 4proves that the file you downloaded matches the file the release page names.
5A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was 5A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was
6signed by a trusted release key. 6signed by the Bitflash release key.
7 7
8That gives users two separate checks: 8That gives users two separate checks:
9 9
101. `gpg --verify SHA256SUMS.asc SHA256SUMS` checks who signed the checksums. 101. `gpg --verify SHA256SUMS.asc SHA256SUMS` — who signed the checksums.
112. `sha256sum -c SHA256SUMS` checks the binaries against those checksums. 112. `sha256sum -c SHA256SUMS` — the binaries against those checksums.
12 12
13## User Check 13But the first check is only worth something if you require a **specific** key.
14A good signature by *some* key proves nothing; anyone can make one.
14 15
15From a shell with `curl` or `wget` and `sha256sum`: 16## The release signing key
16 17
17```bash 18```text
18scripts/verify-release.sh latest 19fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8
20uid Bitflash Releases (bitflash.network release signing)
21 <releases@bitflash.network>
19``` 22```
20 23
21For a specific release: 24The public key is published at
25`https://releases.bitflash.network/bitflash-release-key.asc` and embedded in
26`scripts/verify-release.sh`, so the tool's trust anchor lives in this
27repository rather than on the server it verifies.
28
29## User check
30
31From a shell with `curl` or `wget`, `sha256sum` and `gpg`:
22 32
23```bash 33```bash
24scripts/verify-release.sh v1.2.13 34scripts/verify-release.sh latest
25``` 35```
26 36
27For release audits, require the signature: 37For a specific release:
28 38
29```bash 39```bash
30scripts/verify-release.sh v1.2.13 --require-signature 40scripts/verify-release.sh v1.2.15
31``` 41```
32 42
33Older releases may not have `SHA256SUMS.asc`. In that case the script warns and 43The script downloads the release, imports the pinned key into a throwaway
34still checks file integrity. New release audits should use `--require-signature`. 44keyring, and **refuses anything that is not a valid signature by the key above**
35 45before checking the hashes. Releases from before signing was introduced ship
36## Maintainer Flow 46only `SHA256SUMS`; without `--require-signature` those are checked for integrity
47and reported as unsigned.
37 48
38After building release assets in the repository root: 49By hand, if you would rather not run the script:
39 50
40```bash 51```bash
41scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID 52curl -O https://releases.bitflash.network/bitflash-release-key.asc
53gpg --import bitflash-release-key.asc
54gpg --verify SHA256SUMS.asc SHA256SUMS # must name the fingerprint above
55sha256sum -c SHA256SUMS
42``` 56```
43 57
44Upload all built assets plus: 58## Maintainer flow
45 59
46```text 60The signing key lives on a dedicated location, not in the build tree. After
47SHA256SUMS 61building the release assets in the repository root, write and sign the
48SHA256SUMS.asc 62checksums:
63
64```bash
65sha256sum Bitflash-*.zip Bitflash-*.AppImage bitflash-node-* > SHA256SUMS
66gpg --armor --detach-sign --output SHA256SUMS.asc SHA256SUMS
49``` 67```
50 68
51Keep the private signing key offline or on a dedicated release machine. Publish 69Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update
52the public key fingerprint in the release notes and keep using the same key for 70`latest.txt` on the releases host to the new tag. Keep using the same key; if it
53future releases unless there is a clearly announced rotation. 71is ever rotated, announce the new fingerprint clearly and update the embedded
72key in `verify-release.sh`.
54 73
55## Why This Matters 74## Why this matters
56 75
57`SHA256SUMS` alone protects against a broken download, but not against someone 76`SHA256SUMS` alone protects against a broken download, but not against someone
58replacing both a binary and the checksum file. Signing `SHA256SUMS` means an 77replacing both a binary and the checksum file. Signing `SHA256SUMS`, and
59attacker must also have the release signing key to make the replacement verify. 78requiring a pinned key, means an attacker must also hold the release signing key
79to make the replacement verify. Serving from bitflash.network makes this the
80guarantee that matters: without a pinned key, "trust the site" is the only
81thing standing behind a download.
60 82
61This is not reproducible builds yet. It is the smaller, immediate step that 83This is not reproducible builds yet. It is the smaller, immediate step that
62makes every release asset auditable by users before they run it. 84makes 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 @@
1#!/usr/bin/env bash 1#!/usr/bin/env bash
2set -euo pipefail 2set -euo pipefail
3 3
4repo="Bitflash-sh/bitflash" 4# Verifies a Bitflash release: the checksum file's signature against a pinned
5tag="${1:-latest}" 5# key, then the assets against the checksums.
6#
7# The signing key is embedded below, not fetched. That is the whole point of a
8# pinned key: the trust anchor ships with this script (in git), so it does not
9# depend on the same server that serves the release. A server that is
10# compromised can swap the binaries and the checksums, but it cannot forge a
11# signature by this key.
12
13host="https://releases.bitflash.network"
6require_signature=0 14require_signature=0
7 15
16# The Bitflash release signing key. Verification requires this exact key --
17# gpg's own "good signature" is not enough, because a good signature by *some*
18# key proves nothing. See issue #138.
19RELEASE_KEY_FP="910A2B4CCA879E81FB4B2AEA1D4A53D3B78AA4B8"
20read -r -d '' RELEASE_KEY <<'KEY' || true
21-----BEGIN PGP PUBLIC KEY BLOCK-----
22
23mDMEanSOmRYJKwYBBAHaRw8BAQdAXj24i2BYUcAmOpoXwjE4ZTynHsifCZ+niwQK
24HqO0nMa0UEJpdGZsYXNoIFJlbGVhc2VzIChiaXRmbGFzaC5uZXR3b3JrIHJlbGVh
25c2Ugc2lnbmluZykgPHJlbGVhc2VzQGJpdGZsYXNoLm5ldHdvcms+iJAEExYKADgW
26IQSRCitMyoeegftLKuodSlPTt4qkuAUCanSOmQIbAwULCQgHAgYVCgkICwIEFgID
27AQIeAQIXgAAKCRAdSlPTt4qkuIi9AP96X3OcstiCTTgKOr16Vhds3LyuZ19q3Apq
284KAcNuOUMAD+NqgJ1CqkLGPXfZLCtYBCRHXNYWxMMEk+ViLcRtdLMg4=
29=mxic
30-----END PGP PUBLIC KEY BLOCK-----
31KEY
32
8usage() { 33usage() {
9 cat <<'EOF' 34 cat <<'EOF'
10Usage: scripts/verify-release.sh [tag|latest] [--require-signature] 35Usage: scripts/verify-release.sh [tag|latest] [--require-signature]
11 36
12Downloads the release checksum file and every asset named in it, then verifies: 37Downloads the release checksum file and every asset named in it, then verifies:
13 1. SHA256SUMS.asc, if present, against SHA256SUMS with gpg. 38 1. SHA256SUMS.asc against SHA256SUMS, requiring the pinned Bitflash release
39 key (fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8).
14 2. SHA256SUMS against the downloaded assets. 40 2. SHA256SUMS against the downloaded assets.
15 41
16Use --require-signature for release audits. Without it, older releases that 42Releases from before signing was introduced ship only SHA256SUMS; without
17ship only SHA256SUMS are checked for integrity and reported as unsigned. 43--require-signature those are checked for integrity and reported as unsigned.
18EOF 44EOF
19} 45}
20 46
47tag="${1:-latest}"
21for arg in "${@:2}"; do 48for arg in "${@:2}"; do
22 case "$arg" in 49 case "$arg" in
23 --require-signature) require_signature=1 ;; 50 --require-signature) require_signature=1 ;;
@@ -25,19 +52,9 @@ for arg in "${@:2}"; do
25 *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; 52 *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;;
26 esac 53 esac
27done 54done
55if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then usage; exit 0; fi
28 56
29if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then 57need() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }; }
30 usage
31 exit 0
32fi
33
34need() {
35 if ! command -v "$1" >/dev/null 2>&1; then
36 echo "missing required command: $1" >&2
37 exit 1
38 fi
39}
40
41need sed 58need sed
42need awk 59need awk
43 60
@@ -46,39 +63,25 @@ if command -v sha256sum >/dev/null 2>&1; then
46elif command -v shasum >/dev/null 2>&1; then 63elif command -v shasum >/dev/null 2>&1; then
47 sha_check=(shasum -a 256 -c SHA256SUMS) 64 sha_check=(shasum -a 256 -c SHA256SUMS)
48else 65else
49 echo "missing required command: sha256sum or shasum" >&2 66 echo "missing required command: sha256sum or shasum" >&2; exit 1
50 exit 1
51fi 67fi
52 68
53download() { 69download() {
54 local url="$1" 70 if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2"
55 local out="$2" 71 elif command -v wget >/dev/null 2>&1; then wget -q "$1" -O "$2"
56 if command -v curl >/dev/null 2>&1; then 72 else echo "missing required command: curl or wget" >&2; exit 1; fi
57 curl -fsSL "$url" -o "$out"
58 elif command -v wget >/dev/null 2>&1; then
59 wget -q "$url" -O "$out"
60 else
61 echo "missing required command: curl or wget" >&2
62 exit 1
63 fi
64} 73}
65 74
66if [ "$tag" = "latest" ]; then 75if [ "$tag" = "latest" ]; then
67 tag="$(download "https://api.github.com/repos/$repo/releases/latest" - | 76 tag="$(download "$host/latest.txt" - 2>/dev/null | tr -d '[:space:]' || true)"
68 sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | 77 [ -n "$tag" ] || { echo "could not resolve the latest release tag from $host/latest.txt" >&2; exit 1; }
69 head -1)"
70 if [ -z "$tag" ]; then
71 echo "could not resolve latest release tag" >&2
72 exit 1
73 fi
74fi 78fi
75 79
76workdir="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-release-verify.XXXXXX")" 80workdir="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-release-verify.XXXXXX")"
77cleanup() { rm -rf "$workdir"; } 81cleanup() { rm -rf "$workdir"; }
78trap cleanup EXIT 82trap cleanup EXIT
79 83
80base_url="https://github.com/$repo/releases/download/$tag" 84base_url="$host/$tag"
81
82echo "Verifying Bitflash $tag" 85echo "Verifying Bitflash $tag"
83echo "Working directory: $workdir" 86echo "Working directory: $workdir"
84cd "$workdir" 87cd "$workdir"
@@ -86,27 +89,32 @@ cd "$workdir"
86download "$base_url/SHA256SUMS" SHA256SUMS 89download "$base_url/SHA256SUMS" SHA256SUMS
87 90
88if download "$base_url/SHA256SUMS.asc" SHA256SUMS.asc 2>/dev/null; then 91if download "$base_url/SHA256SUMS.asc" SHA256SUMS.asc 2>/dev/null; then
89 if ! command -v gpg >/dev/null 2>&1; then 92 need gpg
90 echo "SHA256SUMS.asc exists, but gpg is not installed" >&2 93 # Verify in a throwaway keyring holding ONLY the pinned key, and require that
94 # the signature validates against it with the pinned fingerprint. This is the
95 # difference between "signed" and "signed by Bitflash".
96 keyring="$workdir/keyring"
97 mkdir -p "$keyring"; chmod 700 "$keyring"
98 printf '%s\n' "$RELEASE_KEY" | GNUPGHOME="$keyring" gpg --batch --quiet --import 2>/dev/null
99 status="$(GNUPGHOME="$keyring" gpg --batch --status-fd=1 --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null || true)"
100 if printf '%s' "$status" | grep -q "VALIDSIG.*$RELEASE_KEY_FP"; then
101 echo "Signature OK -- signed by the pinned Bitflash release key"
102 else
103 echo "SIGNATURE REJECTED: not a valid signature by the pinned release key" >&2
104 echo " expected key: $RELEASE_KEY_FP" >&2
91 exit 1 105 exit 1
92 fi 106 fi
93 gpg --verify SHA256SUMS.asc SHA256SUMS
94 echo "Signature OK"
95else 107else
96 if [ "$require_signature" -eq 1 ]; then 108 if [ "$require_signature" -eq 1 ]; then
97 echo "release does not publish SHA256SUMS.asc" >&2 109 echo "release does not publish SHA256SUMS.asc" >&2; exit 1
98 exit 1
99 fi 110 fi
100 echo "WARNING: no SHA256SUMS.asc found; checking hashes only" 111 echo "WARNING: no SHA256SUMS.asc found; checking hashes only (unsigned release)"
101fi 112fi
102 113
103awk '{print $2}' SHA256SUMS | while IFS= read -r file; do 114awk '{print $2}' SHA256SUMS | while IFS= read -r file; do
104 file="${file#\*}" 115 file="${file#\*}"; file="${file#./}"
105 file="${file#./}"
106 [ -z "$file" ] && continue 116 [ -z "$file" ] && continue
107 case "$file" in 117 case "$file" in */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; esac
108 */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;;
109 esac
110 echo "Downloading $file" 118 echo "Downloading $file"
111 download "$base_url/$file" "$file" 119 download "$base_url/$file" "$file"
112done 120done