feat(scripts): sign-daemon-release release command (package + sign prebuilt binaries)
Adds `sign-daemon-release.sh release <secret.key> <version>`: zips each staged
prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,
macos,win64}.zip, signs each (detached ed25519), and prints the SHA-256 checksum
table for the release body. Platforms with no staged daemon are skipped; warns
if the signing key doesn't match the pubkey pinned in daemon_updater.h. Keeps the
existing keygen/pubkey/sign subcommands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <name>.zip this produces <name>.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 "<archive>.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] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{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 -> <file>.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 <secret.key>"
|
||||
pubkey_b64 "$1"
|
||||
;;
|
||||
|
||||
sign)
|
||||
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||
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 <secret.key> <version> [--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 <secret.key> | sign <secret.key> <file>...}"
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user