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:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user