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

@@ -48,6 +48,7 @@
#include <unordered_set>
#include <utility>
#include <vector>
#include <sodium.h>
namespace fs = std::filesystem;
@@ -4042,6 +4043,52 @@ void testXmrigSha256AndBasenames()
!= winNames.end());
}
void testXmrigSignatureAssetSelection()
{
using namespace dragonx::util;
XmrigRelease rel; rel.ok = true; rel.tag = "v1";
rel.assets.push_back({"drg-xmrig-linux-x64.zip", "https://x/zip", 100});
EXPECT_EQ(selectXmrigSignatureAsset(rel, "drg-xmrig-linux-x64.zip"), -1); // none published
rel.assets.push_back({"drg-xmrig-linux-x64.zip.sig", "https://x/sig", 64});
EXPECT_TRUE(selectXmrigSignatureAsset(rel, "drg-xmrig-linux-x64.zip") >= 0);
EXPECT_EQ(selectXmrigSignatureAsset(rel, "other.zip"), -1);
}
void testXmrigSignatureVerify()
{
using namespace dragonx::util;
EXPECT_TRUE(sodium_init() >= 0);
unsigned char pk[crypto_sign_PUBLICKEYBYTES], sk[crypto_sign_SECRETKEYBYTES];
crypto_sign_keypair(pk, sk);
const std::string data = "drg-xmrig archive payload bytes for signing test";
unsigned char sig[crypto_sign_BYTES];
crypto_sign_detached(sig, nullptr,
reinterpret_cast<const unsigned char*>(data.data()), data.size(), sk);
auto b64 = [](const unsigned char* b, std::size_t n) {
std::vector<char> buf(sodium_base64_encoded_len(n, sodium_base64_VARIANT_ORIGINAL));
sodium_bin2base64(buf.data(), buf.size(), b, n, sodium_base64_VARIANT_ORIGINAL);
return std::string(buf.data());
};
const std::string pkB64 = b64(pk, sizeof(pk));
const std::string sigB64 = b64(sig, sizeof(sig));
// Valid base64 signature verifies; a raw 64-byte signature is also accepted.
EXPECT_TRUE(verifyXmrigSignature(data, sigB64, pkB64));
EXPECT_TRUE(verifyXmrigSignature(data, std::string(reinterpret_cast<const char*>(sig),
crypto_sign_BYTES), pkB64));
// Whitespace around the base64 signature is tolerated.
EXPECT_TRUE(verifyXmrigSignature(data, " " + sigB64 + "\n", pkB64));
// Fails closed: tampered data, wrong key, malformed/empty inputs.
EXPECT_FALSE(verifyXmrigSignature(data + "x", sigB64, pkB64));
unsigned char pk2[crypto_sign_PUBLICKEYBYTES], sk2[crypto_sign_SECRETKEYBYTES];
crypto_sign_keypair(pk2, sk2);
EXPECT_FALSE(verifyXmrigSignature(data, sigB64, b64(pk2, sizeof(pk2))));
EXPECT_FALSE(verifyXmrigSignature(data, "not valid base64 !!!", pkB64));
EXPECT_FALSE(verifyXmrigSignature(data, sigB64, ""));
EXPECT_FALSE(verifyXmrigSignature(data, "", pkB64));
}
// Live end-to-end exercise of the XmrigUpdater WORKER (real network + curl + miniz). Env-gated so
// CI / offline runs skip it; run with DRAGONX_TEST_NETWORK=1 to hit git.dragonx.is. Verifies the
// full download -> archive-checksum -> extract/flatten -> inner-binary-checksum -> install path.
@@ -4138,6 +4185,8 @@ int main()
testXmrigAssetSelection();
testXmrigChecksumParsing();
testXmrigSha256AndBasenames();
testXmrigSignatureAssetSelection();
testXmrigSignatureVerify();
testXmrigLiveInstall();
testGeneratedResourceBehavior();