feat(mining): opt-in ed25519 signature verification for the xmrig updater (#1)

Closes the supply-chain gap the review flagged: today the archive and its SHA-256 share one
trust root (the release body), so a compromised/edited release can ship an arbitrary binary
that still "verifies". This adds authenticity via a detached ed25519 signature checked against
a public key PINNED IN THE BINARY (not fetched), using libsodium's crypto_sign_verify_detached.

Opt-in / soft rollout:
- kXmrigSignaturePublicKeyBase64 in xmrig_updater.h is EMPTY by default -> signatures are not
  checked and behavior is unchanged (TLS + SHA-256 only). Paste the base64 public key to enable.
- Once a key is pinned, an install verifies a "<archive>.sig" asset (base64/raw 64-byte ed25519
  signature over the archive bytes) when present; kXmrigRequireSignature=true additionally
  refuses installs that publish no signature.
- The check runs after the SHA-256 check, over the same already-read archive bytes; refuses on
  a missing key-but-required, unreachable .sig, or invalid signature.

- verifyXmrigSignature + selectXmrigSignatureAsset are pure (libsodium only) and unit-tested:
  valid base64 + raw-64-byte signatures verify; tampered data, wrong key, and malformed/empty
  inputs all fail closed. Cross-tool interop verified (Python stdlib base64 == sodium base64).
- scripts/sign-xmrig-release.sh: keygen / sign / pubkey helper (PyNaCl = same libsodium ed25519)
  to produce the .sig assets and the public key to pin.

No behavior change until a key is pinned. Both variants build; suite passes; live worker
re-verified (signatures off by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 20:29:49 -05:00
parent 98e0cce8ec
commit 8765fdf362
5 changed files with 225 additions and 1 deletions

72
scripts/sign-xmrig-release.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Sign DRG-XMRig release archives for the wallet's in-app updater (opt-in ed25519 signatures).
#
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
# key pinned in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64). For each archive
# <name>.zip this produces <name>.zip.sig containing the base64 ed25519 signature — upload that
# .sig next to the .zip as a release asset.
#
# Keys are 32-byte ed25519: the secret key signs (keep OFFLINE), the public key goes in the wallet.
#
# Usage:
# scripts/sign-xmrig-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-xmrig-release.sh sign <secret.key> <file>... # -> <file>.sig per file
# scripts/sign-xmrig-release.sh pubkey <secret.key> # print the base64 public key to pin
#
# Requires python3 with PyNaCl (pip install pynacl). PyNaCl uses the same libsodium primitives the
# wallet verifies with, so signatures are guaranteed compatible.
set -euo pipefail
die() { echo "error: $*" >&2; exit 1; }
command -v python3 >/dev/null || die "python3 not found"
python3 -c 'import nacl.signing' 2>/dev/null || die "PyNaCl missing — run: pip install pynacl"
cmd="${1:-}"; shift || true
case "$cmd" in
keygen)
prefix="${1:-drg-xmrig}"
python3 - "$prefix" <<'PY'
import sys, base64, nacl.signing
prefix = sys.argv[1]
sk = nacl.signing.SigningKey.generate()
open(prefix + ".ed25519.key", "wb").write(bytes(sk))
import os; os.chmod(prefix + ".ed25519.key", 0o600)
pub_b64 = base64.standard_b64encode(bytes(sk.verify_key)).decode()
open(prefix + ".ed25519.pub.b64", "w").write(pub_b64 + "\n")
print("secret key : %s.ed25519.key (KEEP OFFLINE, mode 600)" % prefix)
print("public key : %s.ed25519.pub.b64" % prefix)
print()
print("Pin this in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64):")
print(" %s" % pub_b64)
PY
;;
sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
keyfile="$1"; shift
for f in "$@"; do
[ -f "$f" ] || die "no such file: $f"
python3 - "$keyfile" "$f" <<'PY'
import sys, base64, nacl.signing
keyfile, f = sys.argv[1], sys.argv[2]
sk = nacl.signing.SigningKey(open(keyfile, "rb").read())
sig = sk.sign(open(f, "rb").read()).signature # 64-byte detached ed25519 signature
open(f + ".sig", "w").write(base64.standard_b64encode(sig).decode() + "\n")
print("signed: %s -> %s.sig" % (f, f))
PY
done
echo "Upload each .sig as a release asset next to its archive."
;;
pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
python3 - "$1" <<'PY'
import sys, base64, nacl.signing
sk = nacl.signing.SigningKey(open(sys.argv[1], "rb").read())
print(base64.standard_b64encode(bytes(sk.verify_key)).decode())
PY
;;
*)
die "usage: $0 {keygen [prefix] | sign <secret.key> <file>... | pubkey <secret.key>}"
;;
esac