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>
167 lines
7.6 KiB
C++
167 lines
7.6 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// XmrigUpdater — fetch + verify + install the latest DRG-XMRig miner from the DragonX Gitea.
|
|
//
|
|
// Flow (mirrors util/Bootstrap): query the Gitea releases API for the latest release, pick the
|
|
// asset matching this platform (…-linux-x64.zip / …-win-x64.zip), download it, verify its
|
|
// published SHA-256, then extract the miner binary (flattening the versioned subdir the archive
|
|
// nests it in) into the target directory and verify the extracted binary's SHA-256 before it is
|
|
// made executable. All network/disk work runs on a background thread; progress is polled
|
|
// thread-safely from the UI thread.
|
|
//
|
|
// Security: download-and-execute, so verification is mandatory — TLS is verified (libcurl
|
|
// defaults), the host is the project's own Gitea over HTTPS, and BOTH the archive and the inner
|
|
// binary are checked against the SHA-256 checksums published in the release body. The checksums
|
|
// live in the release body markdown (no SHA256SUMS asset), parsed as "<hex> <name>" lines.
|
|
|
|
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <cstddef>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
struct XmrigReleaseAsset {
|
|
std::string name;
|
|
std::string downloadUrl;
|
|
long long size = 0;
|
|
};
|
|
|
|
struct XmrigRelease {
|
|
bool ok = false;
|
|
std::string tag; // e.g. "v1.0.0"
|
|
std::string body; // release notes markdown (holds the checksum blocks)
|
|
std::vector<XmrigReleaseAsset> assets;
|
|
std::string error;
|
|
};
|
|
|
|
// ── Release signature (opt-in supply-chain hardening) ────────────────────────
|
|
//
|
|
// Pinned ed25519 public key (base64, 32 bytes) used to verify a detached signature over the
|
|
// downloaded archive. EMPTY by default = signature verification DISABLED (TLS + the release's
|
|
// published SHA-256 are the only gates, unchanged behavior). To enable: paste the base64 public
|
|
// key here and have the release publish a "<archive-name>.sig" asset containing the base64 (or
|
|
// raw 64-byte) ed25519 signature of the archive bytes (see scripts/sign-xmrig-release.sh). Once a
|
|
// key is set, an install verifies the signature when the .sig asset is present; set
|
|
// kXmrigRequireSignature=true to additionally refuse installs that publish no signature.
|
|
inline constexpr const char* kXmrigSignaturePublicKeyBase64 = "";
|
|
inline constexpr bool kXmrigRequireSignature = false;
|
|
|
|
// ── Pure helpers (no I/O; unit-tested) ───────────────────────────────────────
|
|
|
|
// Parse the Gitea GET /releases/latest JSON into an XmrigRelease (ok=false + error on failure).
|
|
XmrigRelease parseXmrigRelease(const std::string& json);
|
|
|
|
// The asset-name token for the host platform: "linux-x64", "win-x64", "macos-x64",
|
|
// "macos-arm64", or "" if unknown/unsupported.
|
|
std::string currentXmrigPlatformToken();
|
|
|
|
// Index of the asset whose name matches the platform token (e.g. ends with "-linux-x64.zip"),
|
|
// or -1 if none (e.g. no macOS build is published).
|
|
int selectXmrigAsset(const XmrigRelease& release, const std::string& platformToken);
|
|
|
|
// Parse "<sha256hex> <name> …" lines from the release body into { name -> lowercase-hex }.
|
|
// Keys are the first whitespace-delimited token after the hash, so this captures both the archive
|
|
// checksums (keyed by zip filename) and the inner-binary checksums (keyed by "xmrig"/"xmrig.exe").
|
|
std::map<std::string, std::string> parseXmrigChecksums(const std::string& body);
|
|
|
|
// Lowercase-hex SHA-256 of a buffer (libsodium). Empty string on failure.
|
|
std::string sha256Hex(const void* data, std::size_t len);
|
|
|
|
// The binary file basenames to extract for a platform: {"xmrig"} on POSIX,
|
|
// {"xmrig.exe", "WinRing0x64.sys"} on Windows. First entry is always the miner binary.
|
|
std::vector<std::string> xmrigExtractBasenames(const std::string& platformToken);
|
|
|
|
// Index of the detached-signature asset for a given archive (name "<archive>.sig" or
|
|
// "<archive>.minisig"), or -1 if none is published.
|
|
int selectXmrigSignatureAsset(const XmrigRelease& release, const std::string& archiveName);
|
|
|
|
// Verify a detached ed25519 signature over `data`. `signatureFileContent` is the .sig asset body
|
|
// (base64 of, or raw, a 64-byte ed25519 signature; whitespace tolerated). `pubKeyBase64` is the
|
|
// base64 32-byte public key. Returns true only on a cryptographically valid signature.
|
|
bool verifyXmrigSignature(const std::string& data,
|
|
const std::string& signatureFileContent,
|
|
const std::string& pubKeyBase64);
|
|
|
|
// ── Background worker ────────────────────────────────────────────────────────
|
|
|
|
class XmrigUpdater {
|
|
public:
|
|
enum class State {
|
|
Idle,
|
|
Checking,
|
|
UpToDate,
|
|
UpdateAvailable,
|
|
Unavailable, // no miner build is published for this platform (terminal, not an error)
|
|
Downloading,
|
|
Verifying,
|
|
Extracting,
|
|
Done,
|
|
Failed
|
|
};
|
|
|
|
struct Progress {
|
|
State state = State::Idle;
|
|
double downloaded_bytes = 0;
|
|
double total_bytes = 0;
|
|
float percent = 0.0f;
|
|
std::string status_text;
|
|
std::string error; // non-empty on Failed
|
|
std::string latest_tag; // tag reported by the API (once checked)
|
|
std::string installed_tag; // caller-supplied current install (for update detection)
|
|
bool update_available = false;
|
|
};
|
|
|
|
// Gitea releases API for the DRG-XMRig fork.
|
|
static constexpr const char* kApiUrl =
|
|
"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest";
|
|
|
|
XmrigUpdater() = default;
|
|
~XmrigUpdater();
|
|
XmrigUpdater(const XmrigUpdater&) = delete;
|
|
XmrigUpdater& operator=(const XmrigUpdater&) = delete;
|
|
|
|
// Query the latest release on a background thread. `installedTag` (may be empty/unknown) is
|
|
// compared to the API tag to set Progress.update_available. End state: UpToDate / UpdateAvailable
|
|
// / Failed.
|
|
void startCheck(const std::string& installedTag);
|
|
|
|
// Download → verify archive → extract (flatten) → verify binary → install into `targetDir` on a
|
|
// background thread. Re-fetches the release so it is self-contained. End state: Done / Failed.
|
|
// On Done, getProgress().latest_tag is the version that should be persisted as the installed tag.
|
|
void startInstall(const std::string& targetDir);
|
|
|
|
void cancel();
|
|
Progress getProgress() const;
|
|
bool isDone() const; // true once the worker reached a terminal state (Done/Failed/Unavailable)
|
|
|
|
// Internal: called by the libcurl progress callback. Publishes live download bytes and returns
|
|
// false to ask curl to abort (when cancel() was requested). Public only so the C callback in the
|
|
// .cpp can reach it without leaking curl types into this header.
|
|
bool onDownloadProgress(double downloadedBytes, double totalBytes);
|
|
|
|
private:
|
|
void runCheck(std::string installedTag);
|
|
void runInstall(std::string targetDir);
|
|
void setProgress(State state, const std::string& text, double done = 0, double total = 0);
|
|
bool downloadToFile(const std::string& url, const std::string& destPath);
|
|
std::string httpGet(const std::string& url);
|
|
|
|
mutable std::mutex mutex_;
|
|
Progress progress_;
|
|
std::atomic<bool> cancel_requested_{false};
|
|
std::atomic<bool> worker_running_{false};
|
|
std::thread worker_;
|
|
};
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|