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

View File

@@ -264,7 +264,10 @@ void XmrigUpdater::runInstall(std::string targetDir)
}
if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; }
// 2. Verify the archive SHA-256 against the published checksum (refuse on missing/mismatch).
// 2. Verify the downloaded archive. Read it once, then (a) compare its SHA-256 to the published
// checksum and (b) — when a signing key is pinned — verify a detached ed25519 signature over
// the archive bytes against that key, so a checksum rewritten in a tampered release body is
// not sufficient to install.
setProgress(State::Verifying, "Verifying download…");
{
std::ifstream f(zipPath, std::ios::binary);
@@ -279,6 +282,8 @@ void XmrigUpdater::runInstall(std::string targetDir)
setProgress(State::Failed, "Could not read the downloaded archive.");
return;
}
// 2a. SHA-256 (integrity / transit corruption).
const std::string actual = sha256Hex(bytes.data(), bytes.size());
const auto it = checksums.find(asset.name);
if (it == checksums.end()) {
@@ -291,6 +296,29 @@ void XmrigUpdater::runInstall(std::string targetDir)
setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering).");
return;
}
// 2b. Detached signature (authenticity) — only when a public key is pinned (opt-in).
const std::string pubKey = kXmrigSignaturePublicKeyBase64;
if (!pubKey.empty()) {
const int sigIdx = selectXmrigSignatureAsset(rel, asset.name);
if (sigIdx < 0) {
if (kXmrigRequireSignature) {
fs::remove(zipPath, ec);
setProgress(State::Failed, "No signature published for this release — refusing to install.");
return;
}
DEBUG_LOGF("[xmrig-updater] no signature asset for %s; proceeding on checksum only\n",
asset.name.c_str());
} else {
setProgress(State::Verifying, "Verifying signature…");
const std::string sigContent = httpGet(rel.assets[sigIdx].downloadUrl);
if (sigContent.empty() || !verifyXmrigSignature(bytes, sigContent, pubKey)) {
fs::remove(zipPath, ec);
setProgress(State::Failed, "Signature verification failed — refusing to install.");
return;
}
}
}
}
// 3. Extract the wanted binaries (flatten the versioned subdir), verifying the miner binary's