diff --git a/scripts/sign-daemon-release.sh b/scripts/sign-daemon-release.sh index a574530..047eaef 100755 --- a/scripts/sign-daemon-release.sh +++ b/scripts/sign-daemon-release.sh @@ -1,25 +1,31 @@ #!/usr/bin/env bash -# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519). +# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them +# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes). # -# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public -# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is -# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature -# is published. For each archive .zip this produces .zip.sig holding the base64 of the -# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset. +# The wallet verifies a detached ed25519 signature over the archive bytes against a public key +# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY +# (kDaemonRequireSignature = true): an in-app update is refused unless a valid ".sig" is +# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown +# checksum table in the release body, so `release` prints that table for you to paste in. # -# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032), -# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible -# (the same flow the wallet's unit tests verify for the miner updater). +# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the +# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible. # # Usage: -# scripts/sign-daemon-release.sh keygen [out-prefix] # -> .ed25519.{key,pub.b64} -# scripts/sign-daemon-release.sh pubkey # print the base64 public key to pin -# scripts/sign-daemon-release.sh sign ...# -> .sig per file +# scripts/sign-daemon-release.sh keygen [out-prefix] # -> .ed25519.{key,pub.b64} +# scripts/sign-daemon-release.sh pubkey # print the base64 public key to pin +# scripts/sign-daemon-release.sh sign ... # sign existing files -> .sig +# scripts/sign-daemon-release.sh release [--src DIR] [--out DIR] +# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx--{linux-amd64,macos, +# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd +# # binary staged are skipped. # -# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into +# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into # kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h. set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" die() { echo "error: $*" >&2; exit 1; } command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)" @@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25 # ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key. pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; } +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'; + else shasum -a 256 "$1" | awk '{print $1}'; fi +} + +# Detached ed25519 signature over the raw file bytes -> .sig (base64 of the 64-byte sig). +sign_file() { + local key="$1" f="$2" raw + raw="$(mktemp)" + openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw" + openssl base64 -A -in "$raw" > "$f.sig" + printf '\n' >> "$f.sig" + rm -f "$raw" +} + +# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name) +plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; } +plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; } +plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; } + cmd="${1:-}"; shift || true case "$cmd" in keygen) @@ -42,26 +68,90 @@ case "$cmd" in echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):" echo " $pub" ;; + pubkey) [ $# -ge 1 ] || die "usage: pubkey " pubkey_b64 "$1" ;; + sign) [ $# -ge 2 ] || die "usage: sign ..." key="$1"; shift [ -f "$key" ] || die "no such key: $key" for f in "$@"; do [ -f "$f" ] || die "no such file: $f" - raw="$(mktemp)" - openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw" - openssl base64 -A -in "$raw" > "$f.sig" - printf '\n' >> "$f.sig" - rm -f "$raw" + sign_file "$key" "$f" echo "signed: $f -> $f.sig" done echo "Upload each .sig as a release asset next to its archive." ;; + + release) + [ $# -ge 2 ] || die "usage: release [--src DIR] [--out DIR]" + key="$1"; version="$2"; shift 2 + src="$PROJECT_ROOT/prebuilt-binaries" + out="$PROJECT_ROOT/release/daemon" + while [ $# -gt 0 ]; do + case "$1" in + --src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;; + --out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;; + *) die "unknown option: $1" ;; + esac + done + [ -f "$key" ] || die "no such key: $key" + [ -d "$src" ] || die "no such source dir: $src" + command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')" + mkdir -p "$out" + + # Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would + # then reject every signature made with it — only expected when deliberately rotating the key). + pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')" + mine="$(pubkey_b64 "$key")" + if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then + echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2 + echo " signing key -> $mine" >&2 + echo " pinned key -> $pinned" >&2 + echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2 + echo >&2 + fi + + made=0 + table="" + for plat in linux mac win; do + d="$src/$(plat_dir "$plat")" + daemon="$d/$(plat_daemon "$plat")" + if [ ! -f "$daemon" ]; then + echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2 + continue + fi + archive="dragonx-$version-$(plat_token "$plat").zip" + apath="$out/$archive" + rm -f "$apath" + # Zip the staged files at the archive root (binaries + sapling params + asmap), excluding + # the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine. + files=() + while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep') + [ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; } + ( cd "$d" && zip -q -X "$apath" "${files[@]}" ) + sign_file "$key" "$apath" + sum="$(sha256_of "$apath")" + table+="| $archive | \`$sum\` |"$'\n' + echo "packaged + signed: $apath (+ .sig) sha256=$sum" + made=$((made + 1)) + done + [ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/" + + echo + echo "Checksum table (paste into the release body so the wallet can verify SHA-256):" + echo "| Archive | SHA-256 |" + echo "|---|---|" + printf '%s' "$table" + echo + echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature" + echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above." + ;; + *) - die "usage: $0 {keygen [prefix] | pubkey | sign ...}" + die "usage: $0 {keygen [prefix] | pubkey | sign ... | release [--src DIR] [--out DIR]}" ;; esac