diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b1eafc..023e046 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(POLICY CMP0167) endif() project(Triangles - VERSION 6.0.0 + VERSION 6.1.7 DESCRIPTION "Cryptographic Triangles Wallet" LANGUAGES C CXX ) @@ -84,10 +84,10 @@ option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON) option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON) option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON) option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON) -option(USE_UPNP "Enable UPnP support via miniupnpc" ON) +option(USE_UPNP "Enable UPnP support via miniupnpc" OFF) option(USE_IPV6 "Enable IPv6 support" ON) option(USE_QRCODE "Enable QR code generation via libqrencode" OFF) -option(USE_DBUS "Enable D-Bus notifications (Linux only)" ON) +option(USE_DBUS "Enable D-Bus notifications (Linux only)" OFF) option(USE_ZMQ "Enable ZMQ publisher support" OFF) # Triangles is Tor-native. Tor is REQUIRED — disabling it at build time is # not a supported configuration. The 2026-06-23 DNS2 clearnet-fork incident @@ -104,7 +104,7 @@ if(DEFINED USE_TOR_EMBEDDED AND NOT USE_TOR_EMBEDDED) "instead.") endif() option(USE_O3 "Use -O3 optimization instead of -O2" OFF) -option(ENABLE_PIE "Build position-independent executables" OFF) +option(ENABLE_PIE "Build position-independent executables" ON) option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF) # Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4e552d7..61e7604 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -907,46 +907,48 @@ void StartShutdown() {} # "$@" preservation. We use bash explicitly (not sh) for "$@" array # semantics — paths may contain spaces, so word-splitting on IFS # would corrupt them. - file(WRITE "${FUZZ_LINK_WRAPPER}" -"#!/bin/bash + set(FUZZ_LINK_WRAPPER_CONTENT [=[#!/bin/bash # Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common + # trianglesd .o files at link time and exec's the clang++ link line. # # Usage: link.sh clang++ [link-args...] # Final exec: clang++ set -euo pipefail -PROG=\"\$1\" +PROG="$1" shift -TRIANGLES_COMMON_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/triangles_common.dir\" -TRIANGLESD_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/trianglesd_objects.dir\" +TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir" +TRIANGLESD_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/trianglesd_objects.dir" # Discover .o files into a bash array. Exclude script.cpp.o (we have our # own clang-instrumented copy in fuzz_objs/ that we want to keep separate # from the main build's copy). declare -a OBJS=() -for f in \"\$TRIANGLES_COMMON_DIR\"/*.o \"\$TRIANGLES_COMMON_DIR\"/*/*.o; do - [ -f \"\$f\" ] || continue - case \"\$f\" in +for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do + [ -f "$f" ] || continue + case "$f" in */script.cpp.o) continue ;; esac - OBJS+=(\"\$f\") + OBJS+=("$f") done -if [ -d "\$TRIANGLESD_DIR" ]; then - for f in "\$TRIANGLESD_DIR"/*.o; do - [ -f "\$f" ] || continue +if [ -d "$TRIANGLESD_DIR" ]; then + for f in "$TRIANGLESD_DIR"/*.o; do + [ -f "$f" ] || continue # init.cpp defines the daemon's main(); the fuzz harness has its own # (libFuzzer's). wallet.cpp, noui.cpp etc. are safe — they don't # define main and their external references (pwalletMain, # uiInterface, nDerivationMethodIndex) are satisfied by the stub # object file we add at the end of the link line. - case "\$f" in + case "$f" in */init.cpp.o) continue ;; esac - OBJS+=("\$f") + OBJS+=("$f") done fi # Final arg list: PROG, then all .o files, then all original link args. -exec \"\$PROG\" \"\${OBJS[@]}\" \"\$@\" -") +exec "$PROG" "${OBJS[@]}" "$@" +]=]) + string(CONFIGURE "${FUZZ_LINK_WRAPPER_CONTENT}" + FUZZ_LINK_WRAPPER_CONTENT @ONLY) + file(WRITE "${FUZZ_LINK_WRAPPER}" "${FUZZ_LINK_WRAPPER_CONTENT}") file(CHMOD "${FUZZ_LINK_WRAPPER}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE diff --git a/src/bignum.h b/src/bignum.h index d46786f..f4a05c8 100644 --- a/src/bignum.h +++ b/src/bignum.h @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -69,16 +71,10 @@ public: throw bignum_error("CBigNum::CBigNum() : BN_new() returned NULL"); } - CBigNum(const CBigNum& b) + CBigNum(const CBigNum& b) : CBigNum() { - pbn = BN_new(); - if (pbn == nullptr) - throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_new() returned NULL"); if (!BN_copy(pbn, b.pbn)) - { - BN_clear_free(pbn); throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed"); - } } CBigNum& operator=(const CBigNum& b) @@ -99,21 +95,20 @@ public: const BIGNUM* get() const { return pbn; } //CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'. - CBigNum(signed char n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } - CBigNum(short n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } - CBigNum(int n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } - CBigNum(long n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } - CBigNum(long long n) { pbn = BN_new(); setint64(n); } - CBigNum(unsigned char n) { pbn = BN_new(); setulong(n); } - CBigNum(unsigned short n) { pbn = BN_new(); setulong(n); } - CBigNum(unsigned int n) { pbn = BN_new(); setulong(n); } - CBigNum(unsigned long n) { pbn = BN_new(); setulong(n); } - CBigNum(unsigned long long n) { pbn = BN_new(); setuint64(n); } - explicit CBigNum(uint256 n) { pbn = BN_new(); setuint256(n); } + CBigNum(signed char n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); } + CBigNum(short n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); } + CBigNum(int n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); } + CBigNum(long n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); } + CBigNum(long long n) : CBigNum() { setint64(n); } + CBigNum(unsigned char n) : CBigNum() { setulong(n); } + CBigNum(unsigned short n) : CBigNum() { setulong(n); } + CBigNum(unsigned int n) : CBigNum() { setulong(n); } + CBigNum(unsigned long n) : CBigNum() { setulong(n); } + CBigNum(unsigned long long n) : CBigNum() { setuint64(n); } + explicit CBigNum(uint256 n) : CBigNum() { setuint256(n); } - explicit CBigNum(const std::vector& vch) + explicit CBigNum(const std::vector& vch) : CBigNum() { - pbn = BN_new(); setvch(vch); } @@ -216,21 +211,23 @@ public: pch[1] = (nSize >> 16) & 0xff; pch[2] = (nSize >> 8) & 0xff; pch[3] = (nSize) & 0xff; - BN_mpi2bn(pch, p - pch, pbn); + if (BN_mpi2bn(pch, static_cast(p - pch), pbn) == nullptr) + throw bignum_error("CBigNum::setint64() : BN_mpi2bn failed"); } - uint64_t getuint64() + uint64_t getuint64() const { - unsigned int nSize = BN_bn2mpi(pbn, nullptr); - if (nSize < 4) + const int nSize = BN_bn2mpi(pbn, nullptr); + if (nSize <= 4) return 0; - std::vector vch(nSize); - BN_bn2mpi(pbn, &vch[0]); + std::vector vch(static_cast(nSize)); + if (BN_bn2mpi(pbn, vch.data()) != nSize) + throw bignum_error("CBigNum::getuint64() : BN_bn2mpi failed"); if (vch.size() > 4) vch[4] &= 0x7f; uint64_t n = 0; - for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--) - ((unsigned char*)&n)[i] = vch[j]; + for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i) + n |= static_cast(vch[vch.size() - 1 - i]) << (8 * i); return n; } @@ -258,7 +255,8 @@ public: pch[1] = (nSize >> 16) & 0xff; pch[2] = (nSize >> 8) & 0xff; pch[3] = (nSize) & 0xff; - BN_mpi2bn(pch, p - pch, pbn); + if (BN_mpi2bn(pch, static_cast(p - pch), pbn) == nullptr) + throw bignum_error("CBigNum::setuint64() : BN_mpi2bn failed"); } void setuint256(uint256 n) @@ -286,29 +284,33 @@ public: pch[1] = (nSize >> 16) & 0xff; pch[2] = (nSize >> 8) & 0xff; pch[3] = (nSize >> 0) & 0xff; - BN_mpi2bn(pch, p - pch, pbn); + if (BN_mpi2bn(pch, static_cast(p - pch), pbn) == nullptr) + throw bignum_error("CBigNum::setuint256() : BN_mpi2bn failed"); } uint256 getuint256() const { - unsigned int nSize = BN_bn2mpi(pbn, nullptr); - if (nSize < 4) + const int mpiSize = BN_bn2mpi(pbn, nullptr); + if (mpiSize <= 4) return 0; - std::vector vch(nSize); - BN_bn2mpi(pbn, &vch[0]); - if (vch.size() > 4) - vch[4] &= 0x7f; + std::vector vch(static_cast(mpiSize)); + if (BN_bn2mpi(pbn, vch.data()) != mpiSize) + throw bignum_error("CBigNum::getuint256() : BN_bn2mpi failed"); + vch[4] &= 0x7f; uint256 n = 0; - for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--) - ((unsigned char*)&n)[i] = vch[j]; + for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i) + reinterpret_cast(&n)[i] = vch[vch.size() - 1 - i]; return n; } void setvch(const std::vector& vch) { + if (vch.size() > static_cast(std::numeric_limits::max() - 4)) + throw bignum_error("CBigNum::setvch() : input is too large"); + std::vector vch2(vch.size() + 4); - unsigned int nSize = vch.size(); + const uint32_t nSize = static_cast(vch.size()); // BIGNUM's byte stream format expects 4 bytes of // big endian size data info at the front vch2[0] = (nSize >> 24) & 0xff; @@ -316,20 +318,25 @@ public: vch2[2] = (nSize >> 8) & 0xff; vch2[3] = (nSize >> 0) & 0xff; // swap data to big endian - reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4); - BN_mpi2bn(&vch2[0], vch2.size(), pbn); + for (size_t i = 0; i < vch.size(); ++i) + vch2.at(i + 4) = vch.at(vch.size() - 1 - i); + if (BN_mpi2bn(vch2.data(), static_cast(vch2.size()), pbn) == nullptr) + throw bignum_error("CBigNum::setvch() : BN_mpi2bn failed"); } std::vector getvch() const { - unsigned int nSize = BN_bn2mpi(pbn, nullptr); - if (nSize <= 4) + const int mpiSize = BN_bn2mpi(pbn, nullptr); + if (mpiSize <= 4) return std::vector(); - std::vector vch(nSize); - BN_bn2mpi(pbn, &vch[0]); - vch.erase(vch.begin(), vch.begin() + 4); - reverse(vch.begin(), vch.end()); - return vch; + std::vector mpi(static_cast(mpiSize)); + if (BN_bn2mpi(pbn, mpi.data()) != mpiSize) + throw bignum_error("CBigNum::getvch() : BN_bn2mpi failed"); + + std::vector result(static_cast(mpiSize - 4)); + for (size_t i = 0; i < result.size(); ++i) + result.at(i) = mpi.at(mpi.size() - 1 - i); + return result; } CBigNum& SetCompact(unsigned int nCompact) @@ -340,16 +347,20 @@ public: if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff; if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff; if (nSize >= 3) vch[6] = (nCompact >> 0) & 0xff; - BN_mpi2bn(&vch[0], vch.size(), pbn); + if (BN_mpi2bn(vch.data(), static_cast(vch.size()), pbn) == nullptr) + throw bignum_error("CBigNum::SetCompact() : BN_mpi2bn failed"); return *this; } unsigned int GetCompact() const { - unsigned int nSize = BN_bn2mpi(pbn, nullptr); - std::vector vch(nSize); - nSize -= 4; - BN_bn2mpi(pbn, &vch[0]); + const int mpiSize = BN_bn2mpi(pbn, nullptr); + if (mpiSize <= 4) + return 0; + std::vector vch(static_cast(mpiSize)); + if (BN_bn2mpi(pbn, vch.data()) != mpiSize) + throw bignum_error("CBigNum::GetCompact() : BN_bn2mpi failed"); + const unsigned int nSize = static_cast(mpiSize - 4); unsigned int nCompact = nSize << 24; if (nSize >= 1) nCompact |= (vch[4] << 16); if (nSize >= 2) nCompact |= (vch[5] << 8); @@ -361,7 +372,7 @@ public: { // skip 0x const char* psz = str.c_str(); - while (isspace(*psz)) + while (isspace(static_cast(*psz))) psz++; bool fNegative = false; if (*psz == '-') @@ -369,15 +380,15 @@ public: fNegative = true; psz++; } - if (psz[0] == '0' && tolower(psz[1]) == 'x') + if (psz[0] == '0' && tolower(static_cast(psz[1])) == 'x') psz += 2; - while (isspace(*psz)) + while (isspace(static_cast(*psz))) psz++; // hex string to bignum static constexpr signed char phexdigit[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0 }; *this = 0; - while (isxdigit(*psz)) + while (isxdigit(static_cast(*psz))) { *this <<= 4; int n = phexdigit[(unsigned char)*psz++]; @@ -389,6 +400,8 @@ public: std::string ToString(int nBase=10) const { + if (nBase < 2 || nBase > 16) + throw bignum_error("CBigNum::ToString() : base must be in [2, 16]"); CAutoBN_CTX pctx; CBigNum bnBase = nBase; CBigNum bn0 = 0; diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 48cf7af..96d058b 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -4,6 +4,7 @@ #include "bootstrap.h" #include "utxosnapshot.h" #include "txdb.h" +#include "checkpoints.h" #include #include @@ -22,6 +23,7 @@ #include "key.h" #include "base58.h" #include "util.h" +#include "json/nlohmann_json.hpp" extern const std::string strMessageMagic; @@ -30,12 +32,14 @@ extern const std::string strMessageMagic; #include #include #include +#include #ifdef WIN32 #include #include #else #include +#include #include #include #endif @@ -88,6 +92,20 @@ static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& s if (hSocket == INVALID_SOCKET) continue; + // A bootstrap endpoint must not be able to wedge daemon startup by + // accepting a connection and then never sending a response. +#ifdef WIN32 + DWORD timeoutMs = 30000; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeoutMs), sizeof(timeoutMs)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeoutMs), sizeof(timeoutMs)); +#else + struct timeval timeout = {30, 0}; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); +#endif + if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) break; // success @@ -154,8 +172,11 @@ struct HttpConn { strError = "Failed to create SSL context"; return false; } - // Skip cert verification — we verify data integrity via checkpoint hashes - SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + strError = "Failed to load the system TLS trust store"; + return false; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); ssl = SSL_new(ctx); if (!ssl) { @@ -163,7 +184,11 @@ struct HttpConn { return false; } SSL_set_fd(ssl, (int)sock); - SSL_set_tlsext_host_name(ssl, hostname.c_str()); // SNI + if (SSL_set_tlsext_host_name(ssl, hostname.c_str()) != 1 || + SSL_set1_host(ssl, hostname.c_str()) != 1) { + strError = "Failed to configure TLS hostname verification for " + hostname; + return false; + } if (SSL_connect(ssl) != 1) { unsigned long err = ERR_get_error(); @@ -172,6 +197,10 @@ struct HttpConn { strError = "TLS handshake failed with " + hostname + ": " + errBuf; return false; } + if (SSL_get_verify_result(ssl) != X509_V_OK) { + strError = "TLS certificate verification failed for " + hostname; + return false; + } return true; } }; @@ -229,13 +258,18 @@ bool DownloadFile(const std::string& host, const std::string& urlPath, ProgressCallback progressFn, std::string& strError, bool noProxy, - int portOverride) + int portOverride, + int64_t maxDownloadBytes) { try { + if (maxDownloadBytes <= 0) { + strError = "Download size limit must be positive"; + return false; + } std::string currentHost = host; std::string currentPath = urlPath; int currentPort = (portOverride > 0) ? portOverride : PORT; - bool useSSL = false; + bool useSSL = (currentPort == 443); std::string headerData; int redirectCount = 0; const int MAX_REDIRECTS = 5; @@ -324,11 +358,23 @@ bool DownloadFile(const std::string& host, const std::string& urlPath, // Parse redirect URL — supports http://, https://, and relative paths if (location.compare(0, 7, "http://") == 0 || location.compare(0, 8, "https://") == 0) { - if (!ParseAbsoluteUrl(location, useSSL, currentHost, - currentPort, currentPath)) { + bool redirectUsesSSL = false; + std::string redirectHost; + std::string redirectPath; + int redirectPort = 0; + if (!ParseAbsoluteUrl(location, redirectUsesSSL, redirectHost, + redirectPort, redirectPath)) { strError = "Unsupported redirect location: " + location; return false; } + if (useSSL && !redirectUsesSSL) { + strError = "Refusing HTTPS downgrade redirect to " + location; + return false; + } + useSSL = redirectUsesSSL; + currentHost = redirectHost; + currentPath = redirectPath; + currentPort = redirectPort; } else if (!location.empty() && location[0] == '/') { currentPath = location; } else { @@ -362,6 +408,10 @@ bool DownloadFile(const std::string& host, const std::string& urlPath, if (lineEnd != std::string::npos) content_length = std::stoll(headerData.substr(valStart, lineEnd - valStart)); } + if (content_length < 0 || content_length > maxDownloadBytes) { + strError = "Download response exceeds the configured size limit"; + return false; + } // Open output file FILE* file = fopen(destPath.string().c_str(), "wb"); @@ -385,7 +435,18 @@ bool DownloadFile(const std::string& host, const std::string& urlPath, } if (n == 0) break; // EOF - fwrite(chunk, 1, n, file); + if (bytes_written > maxDownloadBytes - n) { + fclose(file); + fs::remove(destPath); + strError = "Download response exceeded the configured size limit"; + return false; + } + if (fwrite(chunk, 1, n, file) != static_cast(n)) { + fclose(file); + fs::remove(destPath); + strError = "Failed writing bootstrap data to disk"; + return false; + } bytes_written += n; if (progressFn && (bytes_written - last_progress >= 262144)) { @@ -422,7 +483,8 @@ bool FetchFileList(const std::string& host, fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt"; std::string urlPath = std::string(BASE_PATH) + "filelist.txt"; - if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy)) + if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy, + -1, 1024 * 1024)) return false; // Read lines @@ -452,23 +514,6 @@ bool FetchFileList(const std::string& host, // --- tar.gz bootstrap support --- -namespace { - -// Parse a tar octal field (ASCII octal, null/space terminated) -static int64_t ParseTarOctal(const char* field, size_t len) -{ - int64_t result = 0; - for (size_t i = 0; i < len && field[i] != '\0' && field[i] != ' '; i++) { - if (field[i] < '0' || field[i] > '7') continue; - result = (result << 3) | (field[i] - '0'); - } - return result; -} - -// Extract a tar.gz file to a destination directory - -} // anonymous namespace - bool ParseManifest(const fs::path& manifestPath, SnapshotManifest& manifest, std::string& strError) @@ -575,12 +620,9 @@ bool VerifyManifest(const SnapshotManifest& manifest, } // ─── Signature verification (#11) ───────────────────────────────────── - // If the manifest includes a signature, verify it against the - // compiled-in snapshot signing key. This prevents MITM attacks - // where an attacker replaces the snapshot file on the bootstrap server. - // - // If no signature is present, print a warning but continue (backward - // compatibility with older snapshots that pre-date signing). + // Legacy pre-built indexes are never accepted without authentication. + // This format is disabled below, but keep its verifier fail-closed so a + // future caller cannot silently revive the old trust behavior. if (!manifest.signature.empty()) { // Build the message that was signed: "height||hash" (ASCII) std::string message = std::to_string(manifest.height) + "||" + manifest.hash; @@ -656,12 +698,12 @@ bool VerifyManifest(const SnapshotManifest& manifest, strError = "Snapshot manifest signature INVALID — possible tampering detected"; return false; } else { - // rc < 0 means error (e.g., placeholder zero pubkey not yet deployed) - printf("WARNING: Snapshot manifest signature verification error (rc=%d). " - "Signing key may not be deployed yet. Proceeding without verification.\n", rc); + strError = "Snapshot manifest signature verification error"; + return false; } } else { - printf("WARNING: Snapshot manifest has no signature — loading WITHOUT signature verification\n"); + strError = "Snapshot manifest has no signature"; + return false; } return true; @@ -672,6 +714,13 @@ bool DownloadBootstrap(const std::string& host, ProgressCallback progressFn, std::string& strError) { + (void)host; + (void)dataDir; + (void)progressFn; + strError = "Legacy file-list bootstrap is disabled; use a compiled-hash UTXO snapshot or sync from genesis"; + return false; + +#if 0 bool gotBlockFile = false; // FastImport removed (commit bdb7253). v2 UTXO snapshot is the ONLY @@ -758,8 +807,10 @@ bool DownloadBootstrap(const std::string& host, fs::remove(manifestPath); return true; +#endif } +#if 0 namespace { // Try to find the canonical UTXO snapshot entry in the bootstrap server's @@ -892,6 +943,7 @@ bool UnsetTrustedSnapshotPublisher(std::string& strError) fs::remove(filePath); return true; } +#endif // Re-enter anonymous namespace for the remaining file-private helpers. // (IsTrustedSnapshotSigner / VerifySignedMessage / ExtractJsonString are @@ -899,6 +951,7 @@ bool UnsetTrustedSnapshotPublisher(std::string& strError) namespace { +#if 0 bool IsTrustedSnapshotSigner(const std::string& addr) { // 1. Runtime override (set via RPC). @@ -1072,6 +1125,7 @@ bool FindCanonicalSnapshotInManifest(const std::string& manifestText, return true; } +#endif // Read an entire file into a string. Empty string on error. std::string ReadFileToString(const fs::path& path) @@ -1114,6 +1168,80 @@ std::string Sha256OfFile(const fs::path& path) } // anonymous namespace +namespace { + +bool IsHexString(const std::string& value, size_t expectedLength) +{ + if (value.size() != expectedLength) + return false; + for (unsigned char c : value) { + if (!std::isxdigit(c)) + return false; + } + return true; +} + +} // anonymous namespace + +bool ParseRemoteSnapshotManifest(const std::string& manifestText, + RemoteSnapshot& snapshot, + std::string& strError) +{ + snapshot = RemoteSnapshot{}; + + try { + const nlohmann::json root = nlohmann::json::parse(manifestText); + if (!root.is_object() || !root.contains("canonical") || + !root.contains("files") || !root.contains("chain_tip")) { + strError = "manifest.json is missing canonical, files, or chain_tip"; + return false; + } + + snapshot.filename = root.at("canonical").at("snapshot").get(); + if (snapshot.filename.empty() || snapshot.filename == "." || + snapshot.filename == ".." || + snapshot.filename.find('/') != std::string::npos || + snapshot.filename.find('\\') != std::string::npos) { + strError = "manifest snapshot filename must be a plain filename"; + return false; + } + + const nlohmann::json& files = root.at("files"); + if (!files.is_object() || !files.contains(snapshot.filename)) { + strError = "canonical snapshot is absent from the files object"; + return false; + } + + const nlohmann::json& file = files.at(snapshot.filename); + const std::string type = file.at("type").get(); + if (type.rfind("utxo_snapshot", 0) != 0) { + strError = "canonical file is not a UTXO snapshot"; + return false; + } + + snapshot.sha256 = file.at("sha256").get(); + std::transform(snapshot.sha256.begin(), snapshot.sha256.end(), + snapshot.sha256.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + snapshot.height = root.at("chain_tip").at("height").get(); + snapshot.blockHash = root.at("chain_tip").at("blockhash").get(); + std::transform(snapshot.blockHash.begin(), snapshot.blockHash.end(), + snapshot.blockHash.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + if (snapshot.height <= 0 || !IsHexString(snapshot.sha256, 64) || + !IsHexString(snapshot.blockHash, 64)) { + strError = "manifest snapshot height or hash fields are invalid"; + return false; + } + } catch (const std::exception& e) { + strError = std::string("invalid manifest.json: ") + e.what(); + return false; + } + + return true; +} + bool DownloadUtxoSnapshot(const std::string& host, const fs::path& dataDir, ProgressCallback progressFn, @@ -1121,79 +1249,51 @@ bool DownloadUtxoSnapshot(const std::string& host, { const bool noProxy = true; - // Step 1: discover the canonical snapshot filename + expected SHA256 + - // per-snapshot manifest filename from the big manifest.json. Falls back - // to legacy URL if manifest unavailable. - std::string snapshotFilename = "utxo-snapshot.bin"; - std::string expectedSha256; - std::string snapshotManifestFilename; - bool haveManifest = false; - + // manifest.json is discovery metadata, not a trust root. The only accepted + // snapshot hash is the one compiled into this release for the same height. fs::path tmpManifest = dataDir / "manifest.json.tmp"; - if (DownloadFile(host, "manifest.json", tmpManifest, nullptr, strError, noProxy)) { - std::string text = ReadFileToString(tmpManifest); + if (!DownloadFile(host, std::string(BASE_PATH) + "manifest.json", + tmpManifest, nullptr, strError, noProxy, + -1, 4 * 1024 * 1024)) { fs::remove(tmpManifest); - - std::string mFile, mSha, mManifest; - std::string mErr; - if (FindCanonicalSnapshotInManifest(text, mFile, mSha, mManifest, mErr)) { - snapshotFilename = mFile; - expectedSha256 = mSha; - snapshotManifestFilename = mManifest; - haveManifest = true; - printf("Bootstrap: manifest declares canonical snapshot %s (sha256=%s)\n", - snapshotFilename.c_str(), expectedSha256.substr(0, 16).c_str()); - } else { - printf("Bootstrap: manifest parse failed (%s) — falling back to legacy URL\n", - mErr.c_str()); - } - } else { - printf("Bootstrap: no manifest.json available — falling back to legacy URL\n"); - strError.clear(); + return false; } - // Step 2: verify the per-snapshot manifest's signature. This is the - // AUTHENTICATION gate — the signature attests that the listed snapshot - // file came from a trusted operator. No checkpoint required; signature - // alone proves authenticity. - if (!snapshotManifestFilename.empty()) { - fs::path tmpSnapManifest = dataDir / "snapshot-manifest.tmp"; - if (!DownloadFile(host, snapshotManifestFilename, tmpSnapManifest, nullptr, strError, noProxy)) { - fs::remove(tmpSnapManifest); - return false; - } - std::string snapManifestText = ReadFileToString(tmpSnapManifest); - fs::remove(tmpSnapManifest); - - std::string signerAddr = ExtractJsonString(snapManifestText, "signing_address"); - std::string message = ExtractJsonString(snapManifestText, "message"); - std::string signature = ExtractJsonString(snapManifestText, "signature"); - std::string declaredSha = ExtractJsonString(snapManifestText, "snapshot_sha256"); - - if (signerAddr.empty() || message.empty() || signature.empty()) { - strError = "per-snapshot manifest missing required fields (signing_address/message/signature)"; - return false; - } - if (!IsTrustedSnapshotSigner(signerAddr)) { - strError = "snapshot manifest signer " + signerAddr + " is not in trusted signers list"; - return false; - } - std::string vErr; - if (!VerifySignedMessage(signerAddr, signature, message, vErr)) { - strError = "snapshot signature verification failed: " + vErr; - return false; - } - if (!declaredSha.empty()) - expectedSha256 = declaredSha; - printf("Bootstrap: snapshot signature verified (signer=%s)\n", signerAddr.c_str()); - } else { - printf("Bootstrap: WARNING — no per-snapshot manifest available; " - "loading snapshot WITHOUT signature verification\n"); + const std::string manifestText = ReadFileToString(tmpManifest); + fs::remove(tmpManifest); + if (manifestText.empty()) { + strError = "Cannot read downloaded manifest.json"; + return false; } - // Step 3: download the canonical snapshot file. + RemoteSnapshot snapshot; + if (!ParseRemoteSnapshotManifest(manifestText, snapshot, strError)) + return false; + + const uint256 manifestBlockHash(snapshot.blockHash); + if (!Checkpoints::IsKnownCheckpoint(snapshot.height, manifestBlockHash)) { + strError = "Server snapshot tip is not a hardened checkpoint in this release"; + return false; + } + + uint256 compiledFileHash; + if (!Checkpoints::GetSnapshotHash(snapshot.height, compiledFileHash)) { + strError = "Snapshot height " + std::to_string(snapshot.height) + + " has no file hash compiled into this release"; + return false; + } + const std::string compiledSha256 = compiledFileHash.ToString(); + if (snapshot.sha256 != compiledSha256) { + strError = "Server snapshot hash does not match the hash compiled into this release"; + return false; + } + + printf("Bootstrap: manifest selects compiled snapshot %s at height %d (sha256=%s)\n", + snapshot.filename.c_str(), snapshot.height, + compiledSha256.substr(0, 16).c_str()); + fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp"; - std::string urlPath = std::string(BASE_PATH) + snapshotFilename; + std::string urlPath = std::string(BASE_PATH) + snapshot.filename; printf("Bootstrap: downloading UTXO snapshot from %s%s...\n", host.c_str(), urlPath.c_str()); @@ -1202,30 +1302,25 @@ bool DownloadUtxoSnapshot(const std::string& host, return false; } - // Step 4: verify the downloaded file's SHA256 against the manifest. - if (!expectedSha256.empty()) { - std::string actualSha = Sha256OfFile(tmpPath); - if (actualSha.empty()) { - strError = "Cannot read downloaded snapshot for SHA256 verification"; - fs::remove(tmpPath); - return false; - } - if (actualSha != expectedSha256) { - strError = "Snapshot SHA256 mismatch: expected " + expectedSha256 - + ", got " + actualSha - + " (manifest/snapshot tampering or server misconfiguration)"; - fs::remove(tmpPath); - return false; - } - printf("Bootstrap: snapshot SHA256 verified (%s)\n", actualSha.substr(0, 16).c_str()); + const std::string actualSha256 = Sha256OfFile(tmpPath); + if (actualSha256.empty()) { + strError = "Cannot read downloaded snapshot for SHA256 verification"; + fs::remove(tmpPath); + return false; } + if (actualSha256 != compiledSha256) { + strError = "Snapshot SHA256 does not match the hash compiled into this release"; + fs::remove(tmpPath); + return false; + } + printf("Bootstrap: compiled snapshot SHA256 verified (%s)\n", + actualSha256.substr(0, 16).c_str()); printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n"); - // Step 5: load the snapshot. requireCheckpoint is FALSE — signature is - // the authentication gate; checkpoints would force snapshots only at - // specific heights. Signature alone is sufficient. - if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/false)) { + // File hash and tip checkpoint are independent gates. The hash commits to + // the complete serialized UTXO set; the checkpoint commits to chain identity. + if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/true)) { fs::remove(tmpPath); return false; } diff --git a/src/bootstrap.h b/src/bootstrap.h index 07ee873..80baa5b 100644 --- a/src/bootstrap.h +++ b/src/bootstrap.h @@ -8,13 +8,14 @@ #include #include #include +#include namespace Bootstrap { // Bootstrap server configuration - static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org"; - static const char* BASE_PATH = "/"; - static const int PORT = 80; + inline constexpr const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org"; + inline constexpr const char* BASE_PATH = "/"; + inline constexpr int PORT = 443; // Progress callback: (bytesDownloaded, totalBytes) typedef std::function ProgressCallback; @@ -31,7 +32,8 @@ namespace Bootstrap { ProgressCallback progressFn, std::string& strError, bool noProxy = false, - int portOverride = -1); + int portOverride = -1, + int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024); // Fetch the file manifest (list of relative paths to download) bool FetchFileList(const std::string& host, @@ -46,6 +48,23 @@ namespace Bootstrap { ProgressCallback progressFn, std::string& strError); + // Advertised identity of a snapshot listed by manifest.json. + // The advertised SHA256 is accepted only when it matches the hash compiled + // into checkpoints.cpp for the same height. + struct RemoteSnapshot { + std::string filename; + std::string sha256; + int height; + std::string blockHash; + }; + + // Parse and validate the small, untrusted bootstrap manifest. This routine + // performs no network I/O and is exposed so malformed-input behavior can be + // covered by unit tests. + bool ParseRemoteSnapshotManifest(const std::string& manifestText, + RemoteSnapshot& snapshot, + std::string& strError); + // Snapshot manifest (parsed from snapshot.manifest in bootstrap archive) struct SnapshotManifest { int format; // format version, must be 1 @@ -73,27 +92,6 @@ namespace Bootstrap { ProgressCallback progressFn, std::string& strError); - // =================================================================== - // Trusted snapshot publisher — RPC-driven single-slot rotation - // =================================================================== - // Returns the currently active trusted publisher, or empty string if - // only the built-in fallback is in effect. - std::string GetActiveTrustedSnapshotPublisher(); - - // Atomically replaces the active publisher. The previous one is dropped - // immediately (Design A: single-slot, no grace period). Persists to - // /snapshot-publisher.json so the choice survives restarts. - bool SetTrustedSnapshotPublisher(const std::string& addr, - std::string& strError); - - // Clears the runtime override and reverts to the built-in fallback - // list. Also removes snapshot-publisher.json from disk. - bool UnsetTrustedSnapshotPublisher(std::string& strError); - - // Called once at daemon startup (from init.cpp) to load any persisted - // runtime override. - void LoadTrustedSnapshotPublisher(); - } // namespace Bootstrap #endif // TRIANGLES_BOOTSTRAP_H diff --git a/src/checkpointpublisher.cpp b/src/checkpointpublisher.cpp index 8a0ca8b..910f242 100644 --- a/src/checkpointpublisher.cpp +++ b/src/checkpointpublisher.cpp @@ -425,7 +425,8 @@ bool LoadSignedCheckpoints( if (Bootstrap::DownloadFile(host, "signed-checkpoints.json", std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp", nullptr, strError, - /*noProxy=*/true)) { + /*noProxy=*/true, /*portOverride=*/-1, + /*maxDownloadBytes=*/10 * 1024 * 1024)) { std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp"; FILE* f = fopen(tmp.string().c_str(), "rb"); if (f) { diff --git a/src/checkpoints.cpp b/src/checkpoints.cpp index 765d156..8c644ba 100644 --- a/src/checkpoints.cpp +++ b/src/checkpoints.cpp @@ -353,53 +353,14 @@ namespace Checkpoints bool SetCheckpointPrivKey(std::string strPrivKey) { - // Test signing a sync-checkpoint with genesis block - CSyncCheckpoint checkpoint; - checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet; - CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); - sMsg << (CUnsignedSyncCheckpoint)checkpoint; - checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); - - std::vector vchPrivKey = ParseHex(strPrivKey); - CKey key; - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) - return false; - - // Test signing successful, proceed - CSyncCheckpoint::strMasterPrivKey = strPrivKey; - return true; + (void)strPrivKey; + return error("SetCheckpointPrivKey: synchronized checkpoints are disabled"); } bool SendSyncCheckpoint(uint256 hashCheckpoint) { - CSyncCheckpoint checkpoint; - checkpoint.hashCheckpoint = hashCheckpoint; - CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); - sMsg << (CUnsignedSyncCheckpoint)checkpoint; - checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); - - if (CSyncCheckpoint::strMasterPrivKey.empty()) - return error("SendSyncCheckpoint: Checkpoint master key unavailable."); - std::vector vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey); - CKey key; - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) - return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?"); - - if(!checkpoint.ProcessSyncCheckpoint(nullptr)) - { - printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n"); - return false; - } - - // Relay checkpoint - { - LOCK(cs_vNodes); - for (CNode* pnode : vNodes) - checkpoint.RelayTo(pnode); - } - return true; + (void)hashCheckpoint; + return error("SendSyncCheckpoint: synchronized checkpoints are disabled"); } // Is the sync-checkpoint outside maturity window? @@ -420,13 +381,11 @@ const std::string CSyncCheckpoint::strMasterPubKey = ""; std::string CSyncCheckpoint::strMasterPrivKey = ""; // triangles: verify signature of sync-checkpoint message -// Master key system disabled - checkpoint signatures are no longer required +// The master-key system is disabled. Reject these legacy messages instead of +// treating unsigned data as authenticated if a dispatcher is added later. bool CSyncCheckpoint::CheckSignature() { - // Deserialize the checkpoint data without signature verification - CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION); - sMsg >> *(CUnsignedSyncCheckpoint*)this; - return true; + return error("CSyncCheckpoint::CheckSignature: synchronized checkpoints are disabled"); } // triangles: process synchronized checkpoint diff --git a/src/init.cpp b/src/init.cpp index 20f03ed..9d93284 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -30,8 +30,11 @@ #include "addressindex.h" #include "chaindb_migrate.h" #include +#include +#include #include #include +#include // Forward declaration: InitError / InitWarning are defined further down // in this file but referenced by AppInit (line ~423) before the definition. @@ -66,6 +69,8 @@ using namespace std; namespace fs = std::filesystem; namespace { +std::atomic g_shutdownExitCode{EXIT_SUCCESS}; + // Acquire an exclusive, non-blocking advisory lock on the datadir .lock file // and hold it for the lifetime of the process. Replaces // boost::interprocess::file_lock. The descriptor/handle is intentionally never @@ -96,8 +101,32 @@ bool LockDataDirectory(const std::filesystem::path& pathLockFile) return true; // fd held until process exit #endif } + +#ifndef WIN32 +bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error) +{ + struct stat fileStat; + if (::lstat(path.string().c_str(), &fileStat) != 0) + return errno == ENOENT; + if (!S_ISREG(fileStat.st_mode) || fileStat.st_uid != geteuid()) { + error = path.string() + " must be a regular file owned by the daemon user"; + return false; + } + if ((fileStat.st_mode & (S_IRWXG | S_IRWXO)) != 0 && + ::chmod(path.string().c_str(), S_IRUSR | S_IWUSR) != 0) { + error = "could not restrict permissions on " + path.string(); + return false; + } + return true; +} +#endif } // namespace +void MarkShutdownFailure() +{ + g_shutdownExitCode.store(EXIT_FAILURE, std::memory_order_relaxed); +} + std::unique_ptr pwalletMain; CClientUIInterface uiInterface; std::string strWalletFileName; @@ -144,7 +173,8 @@ void ExitTimeout(void* parg) { #ifdef WIN32 MilliSleep(5000); - ExitProcess(0); + ExitProcess(static_cast( + g_shutdownExitCode.load(std::memory_order_relaxed))); #endif } @@ -421,7 +451,11 @@ void Shutdown(void* parg) // MakeChainDB()->Close(); bitdb.Flush(false); bitdb.Flush(true); - fs::remove(GetPidFile()); + std::error_code pidFileError; + fs::remove(GetPidFile(), pidFileError); + if (pidFileError) + printf("Warning: could not remove PID file: %s\n", + pidFileError.message().c_str()); UnregisterWallet(pwalletMain.get()); pwalletMain.reset(); // DB is flushed and wallet saved - safe to force-exit if something hangs @@ -431,7 +465,7 @@ void Shutdown(void* parg) fExit = true; #ifndef QT_GUI // ensure non-UI client gets exited here, but let Triangles-Qt reach 'return 0;' in triangles.cpp - exit(0); + exit(g_shutdownExitCode.load(std::memory_order_relaxed)); #endif } else @@ -528,8 +562,10 @@ bool AppInit(int argc, char* argv[]) } catch (...) { PrintException(nullptr, "AppInit()"); } - if (!fRet) + if (!fRet) { + MarkShutdownFailure(); Shutdown(nullptr); + } return fRet; } @@ -610,8 +646,8 @@ std::string HelpMessage() //" -onlynet= " + _("Only connect to nodes in network (IPv4, IPv6 or Tor)") + "\n" + //" -discover " + _("Discover own IP address (default: 1 when listening and no -externalip)") + "\n" + //" -irc " + _("Find peers using internet relay chat (default: 0)") + "\n" + - //" -listen " + _("Accept connections from outside (default: 1 if no -proxy or -connect)") + "\n" + - //" -bind= " + _("Bind to given address. Use [host]:port notation for IPv6") + "\n" + + " -listen " + _("Accept inbound peer connections (default: 1 unless -proxy or -connect is set)") + "\n" + + " -bind= " + _("Bind inbound peers to this address. Use [host]:port notation for IPv6") + "\n" + // -dnsseed " + _("Find peers using DNS lookup (default: 1)") + "\n" + " -staking " + _("Stake your coins to support network and gain reward (default: 1)") + "\n" + " -synctime " + _("Sync time with other nodes. Disable if time on your system is precise e.g. syncing with NTP (default: 1)") + "\n" + @@ -652,8 +688,11 @@ std::string HelpMessage() #endif " -rpcuser= " + _("Username for JSON-RPC connections") + "\n" + " -rpcpassword= " + _("Password for JSON-RPC connections") + "\n" + - " -rpcport= " + _("Listen for JSON-RPC connections on (default: 19111 or testnet: 19112)") + "\n" + - " -rpcallowip= " + _("Allow JSON-RPC connections from specified IP address") + "\n" + + " -rpcport= " + _("Listen for JSON-RPC connections on (default: 19112 or testnet: 19111)") + "\n" + + " -rpcbind= " + _("Bind JSON-RPC to this address (default: loopback only; use * explicitly for all interfaces)") + "\n" + + " -rpcallowip= " + _("Allow JSON-RPC clients matching this address pattern; does not change the bind address") + "\n" + + " -rpcallowmethod= " + _("Allow only this JSON-RPC method (repeat for each method; default: all)") + "\n" + + " -rpcservertimeout= " + _("RPC socket read/write timeout in seconds (default: 30, range: 1-600)") + "\n" + " -rpcconnect= " + _("Send commands to node running on (default: 127.0.0.1)") + "\n" + " -blocknotify= " + _("Execute command when the best block changes (%s in cmd is replaced by block hash)") + "\n" + " -walletnotify= " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n" + @@ -1120,24 +1159,19 @@ bool AppInit2() fUseUPnP = GetBoolArg("-upnp", USE_UPNP); #endif bool fBound = false; - if (true) { - if (true) { - do { - // W1: Bind to all interfaces so external peers can connect. - // - // The previous code went through Lookup("0.0.0.0", ...) which - // hands the literal string to getaddrinfo(). On Windows that - // resolver can fail to map "0.0.0.0" to INADDR_ANY and the - // daemon would abort at startup with "Cannot resolve binding - // address". Construct the CService directly from INADDR_ANY - // instead — this is the canonical "any-address" binding and - // works on every platform without consulting the resolver. + if (!fNoListen) { + if (mapArgs.count("-bind")) { + for (const std::string& bindAddress : mapMultiArgs["-bind"]) { CService addrBind; - struct in_addr any; - any.s_addr = htonl(INADDR_ANY); - addrBind = CService(any, GetListenPort()); + if (!Lookup(bindAddress.c_str(), addrBind, GetListenPort(), false)) + return InitError(strprintf(_("Cannot resolve -bind address: '%s'"), + bindAddress.c_str())); fBound |= Bind(addrBind); - } while (false); + } + } else { + struct in_addr any; + any.s_addr = htonl(INADDR_ANY); + fBound = Bind(CService(any, GetListenPort())); } if (!fBound) return InitError(_("Failed to listen on any port.")); @@ -1167,10 +1201,9 @@ bool AppInit2() } } - if (mapArgs.count("-checkpointkey")) // triangles: checkpoint master priv key + if (mapArgs.count("-checkpointkey")) { - if (!Checkpoints::SetCheckpointPrivKey(GetArg(std::string_view{"-checkpointkey"}, std::string_view{""}))) - InitError(_("Unable to sign checkpoint, wrong checkpointkey?\n")); + return InitError(_("Synchronized checkpoint signing is disabled.")); } for (string strDest : mapMultiArgs["-seednode"]) @@ -1178,8 +1211,8 @@ bool AppInit2() StartupPerfLog("network_init", GetTimeMillis() - nStart, strprintf("listen=%d seednodes=%" PRIszu, !fNoListen, mapMultiArgs["-seednode"].size())); // ********************************************************* Step 6b: bootstrap download (daemon) - // Automatic: if data dir has no blockchain, bootstrap without asking. - // Can also be forced with -bootstrap flag, or disabled with -nobootstrap. + // Remote HTTP bootstrap is opt-in via -bootstrap. Fresh nodes otherwise + // use the compiled-hash P2P snapshot path or sync from genesis. // // v5.9.5: P2P UTXO snapshot fetch is the default for fresh installs (Step 11.6). // The legacy clearnet HTTP bootstrap only runs when the user explicitly requests @@ -1187,20 +1220,13 @@ bool AppInit2() // Bootstrap auto-download works for both GUI and daemon. // GUI users get the same automatic bootstrap on fresh installs. { - bool wantsBootstrap = GetBoolArg("-bootstrap", false); bool noBootstrap = GetBoolArg("-nobootstrap", false); - bool snapshotMode = GetBoolArg("-snapshot", true); + bool wantsBootstrap = GetBoolArg("-bootstrap", false) && !noBootstrap; fs::path dataPath = GetDataDir(); - // Load any runtime trusted snapshot publisher override that was - // persisted by a previous settrustedv2snapshotpublisher call. - Bootstrap::LoadTrustedSnapshotPublisher(); bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath); - if (needsBootstrap && !noBootstrap) { - printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n"); - printf("Bootstrap: (use -nobootstrap to skip)\n"); - uiInterface.InitMessage(_("Downloading UTXO snapshot...")); - wantsBootstrap = true; + if (needsBootstrap && !wantsBootstrap) { + printf("Bootstrap: no blockchain data found; remote bootstrap is disabled unless -bootstrap is set.\n"); } if (wantsBootstrap) @@ -1208,7 +1234,6 @@ bool AppInit2() int64_t nBootstrapStart = GetTimeMillis(); fs::path dataPath = GetDataDir(); std::string host = Bootstrap::DEFAULT_HOST; - std::string strError; int64_t lastGuiUpdate = 0; auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) { @@ -1250,19 +1275,10 @@ bool AppInit2() triedUtxoSnapshot = true; } - // Fall back to full bootstrap.tar.gz if UTXO snapshot failed + // Never consume a server-directed file list. If the authenticated + // snapshot is unavailable, normal peer-to-peer sync is the safe fallback. if (!success) { - uiInterface.InitMessage(_("Downloading blockchain snapshot...")); - printf("Bootstrap: contacting %s...\n", host.c_str()); - - success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError); - - if (!success) { - printf("\nBootstrap: failed: %s\n", strError.c_str()); - printf("Bootstrap: skipping, will sync from network.\n"); - } else { - printf("\nBootstrap: done.\n"); - } + printf("Bootstrap: no trusted compiled-hash snapshot available; syncing from peers.\n"); } StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart, @@ -1282,13 +1298,23 @@ bool AppInit2() printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n"); uiInterface.InitMessage(_("Loading UTXO snapshot...")); - // Local file load: skip the checkpoint gate. The operator has - // filesystem access, so the trust model is already equivalent - // to direct chain state modification — a malicious local file - // is no worse than a malicious chain DB. P2P-delivered - // snapshots (SnapshotNet) keep the checkpoint gate on. std::string strError; - if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) { + const int snapshotHeight = Checkpoints::GetBestSnapshotHeight(); + uint256 compiledHash; + uint256 actualHash; + const bool hasCompiledHash = snapshotHeight > 0 && + Checkpoints::GetSnapshotHash(snapshotHeight, compiledHash); + const bool hashVerified = hasCompiledHash && + SnapshotNet::ComputeSnapshotFileHash(snapshotFile, actualHash, strError) && + actualHash == compiledHash; + + if (!hashVerified) { + if (strError.empty()) + strError = "snapshot SHA256 is not compiled into this release"; + printf("UTXO snapshot rejected before import: %s\n", strError.c_str()); + printf("Will proceed with normal sync.\n"); + } else if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, + /*requireCheckpoint=*/true)) { printf("UTXO snapshot loaded successfully.\n"); } else { printf("UTXO snapshot load failed: %s\n", strError.c_str()); @@ -1501,6 +1527,11 @@ bool AppInit2() { fs::path walletPath = GetDataDir() / strWalletFileName; if (fs::exists(walletPath)) { +#ifndef WIN32 + std::string permissionError; + if (!EnsureOwnerOnlyFile(walletPath, permissionError)) + return InitError(permissionError); +#endif uintmax_t wsize = fs::file_size(walletPath); printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize); if (wsize < 1024) { @@ -1933,10 +1964,10 @@ bool AppInit2() printf("mapAddressBook.size() = %" PRIszu "\n", pwalletMain->mapAddressBook.size()); if (!NewThread(StartNode, nullptr)) - InitError(_("Error: could not start node")); + return InitError(_("Error: could not start node")); - if (fServer) - NewThread(ThreadRPCServer, nullptr); + if (fServer && !NewThread(ThreadRPCServer, nullptr)) + return InitError(_("Error: could not start the RPC server")); // ********************************************************* Step 11.6: P2P UTXO snapshot fetch // If the chain is empty and snapshot mode is enabled (default), spawn a diff --git a/src/init.h b/src/init.h index 4f91850..d4181b3 100644 --- a/src/init.h +++ b/src/init.h @@ -12,6 +12,7 @@ extern std::unique_ptr pwalletMain; extern std::string strWalletFileName; void StartShutdown(); +void MarkShutdownFailure(); bool ShutdownRequested(); void Shutdown(void* parg); bool AppInit2(); @@ -19,4 +20,3 @@ std::string HelpMessage(); #endif - diff --git a/src/keystore.cpp b/src/keystore.cpp index dc3223a..4d759d1 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -190,28 +190,49 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co return false; } -bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn) +bool CCryptoKeyStore::PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn, + CryptedKeyMap& cryptedKeysOut) const { - { - LOCK(cs_KeyStore); - if (!mapCryptedKeys.empty() || IsCrypted()) - return false; + LOCK(cs_KeyStore); + if (!mapCryptedKeys.empty() || IsCrypted()) + return false; - fUseCrypto = true; - for (KeyMap::value_type& mKey : mapKeys) - { - CKey key; - if (!key.SetSecret(mKey.second.first, mKey.second.second)) - return false; - const CPubKey vchPubKey = key.GetPubKey(); - std::vector vchCryptedSecret; - bool fCompressed; - if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed), vchPubKey.GetHash(), vchCryptedSecret)) - return false; - if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) - return false; - } - mapKeys.clear(); + cryptedKeysOut.clear(); + for (const KeyMap::value_type& mKey : mapKeys) + { + CKey key; + if (!key.SetSecret(mKey.second.first, mKey.second.second)) + return false; + const CPubKey vchPubKey = key.GetPubKey(); + std::vector vchCryptedSecret; + bool fCompressed; + if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed), + vchPubKey.GetHash(), vchCryptedSecret)) + return false; + if (!cryptedKeysOut.emplace(vchPubKey.GetID(), + std::make_pair(vchPubKey, + std::move(vchCryptedSecret))).second) + return false; } + return cryptedKeysOut.size() == mapKeys.size(); +} + +bool CCryptoKeyStore::CommitKeyEncryption(CryptedKeyMap&& cryptedKeys) +{ + LOCK(cs_KeyStore); + if (!mapCryptedKeys.empty() || IsCrypted() || cryptedKeys.size() != mapKeys.size()) + return false; + + mapCryptedKeys = std::move(cryptedKeys); + mapKeys.clear(); + fUseCrypto = true; return true; } + +bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn) +{ + CryptedKeyMap cryptedKeys; + if (!PrepareKeyEncryption(vMasterKeyIn, cryptedKeys)) + return false; + return CommitKeyEncryption(std::move(cryptedKeys)); +} diff --git a/src/keystore.h b/src/keystore.h index 4fb2523..fbfb869 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -9,6 +9,8 @@ #include "util_signal.h" #include "sync.h" +#include + class CScript; /** A virtual base class for key stores */ @@ -112,7 +114,13 @@ protected: bool SetCrypted(); - // will encrypt previously unencrypted keys + // Stage and commit wallet-key encryption separately so callers can make + // the on-disk update atomic before discarding plaintext keys in memory. + bool PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn, + CryptedKeyMap& cryptedKeysOut) const; + bool CommitKeyEncryption(CryptedKeyMap&& cryptedKeys); + + // Encrypt previously unencrypted keys in memory. bool EncryptKeys(CKeyingMaterial& vMasterKeyIn); bool Unlock(const CKeyingMaterial& vMasterKeyIn); diff --git a/src/main.cpp b/src/main.cpp index 734e2f1..fc6a46d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1747,6 +1747,11 @@ bool IsConsensusAssumeValidHeight(int nHeight) || (nHeight <= nAssumeValidThreshold); } +bool IsBlockSignatureRequiredAtHeight(int nHeight) +{ + return nHeight > Checkpoints::GetTotalBlocksEstimate(); +} + void static InvalidChainFound(CBlockIndex* pindexNew) { if (pindexNew->nChainTrust > nBestInvalidTrust) @@ -3401,6 +3406,12 @@ bool CBlock::AcceptBlock() uint256 hashProofOfStake = 0, targetProofOfStake = 0; if (IsProofOfStake()) { + // The rolling validation optimization is not a signature trust root. + // Every PoS block above the compiled checkpoint must authorize its + // exact block contents, including while the local tip is stale. + if (IsBlockSignatureRequiredAtHeight(nHeight) && !CheckBlockSignature()) + return DoS(100, error("AcceptBlock() : bad proof-of-stake block signature at height %d", nHeight)); + if (IsConsensusAssumeValidHeight(nHeight)) { // Historical fast path: blocks at/below hardcoded checkpoint or @@ -3539,10 +3550,16 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock) if (pblock->IsProofOfStake() && !GetBoolArg("-ignoredupstake", false) && setStakeSeen.count(pblock->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash)) return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for block %s", pblock->GetProofOfStake().first.ToString().c_str(), pblock->GetProofOfStake().second, hash.ToString().c_str()); - // Preliminary checks - // Skip block signature verification during initial block download (below checkpoint). - // The hardcoded checkpoint guarantees historical chain integrity. - if (!pblock->CheckBlock(true, true, !IsInitialBlockDownload())) + // Operational IBD state is never permission to skip a live proof-of-stake + // block signature. Only a candidate height committed by the latest + // hardened checkpoint uses the historical fast path. + bool checkBlockSignature = true; + const auto prevIt = mapBlockIndex.find(pblock->hashPrevBlock); + if (prevIt != mapBlockIndex.end()) { + const int candidateHeight = prevIt->second->nHeight + 1; + checkBlockSignature = IsBlockSignatureRequiredAtHeight(candidateHeight); + } + if (!pblock->CheckBlock(true, true, checkBlockSignature)) { printf("IBD-DIAG: CheckBlock FAILED for %s (PoS=%d, IBD=%d)\n", hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload()); @@ -3786,6 +3803,7 @@ bool CheckDiskSpace(uint64_t nAdditionalBytes) strMiscWarning = strMessage; printf("*** %s\n", strMessage.c_str()); uiInterface.ThreadSafeMessageBox(strMessage, "Triangles", CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL); + MarkShutdownFailure(); StartShutdown(); return false; } @@ -6039,5 +6057,3 @@ bool SendMessages(CNode* pto, bool fSendTrickle) } return true; } - - diff --git a/src/main.h b/src/main.h index ec06932..55fc5fd 100644 --- a/src/main.h +++ b/src/main.h @@ -176,6 +176,7 @@ bool IsStakingSafe(const CWallet* pwallet, const std::vector& vNodesSnap // assume-valid validation. This intentionally excludes operational IBD states // such as a stale tip; stale-tip IBD must not disable live PoS checks. [[nodiscard]] bool IsConsensusAssumeValidHeight(int nHeight); +[[nodiscard]] bool IsBlockSignatureRequiredAtHeight(int nHeight); std::string GetWarnings(std::string strFor); bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock); uint256 WantedByOrphan(const CBlock* pblockOrphan); diff --git a/src/net.cpp b/src/net.cpp index 58389fb..843febf 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -605,7 +605,8 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) } if (fDebug) { - printf("ConnectNode(): pszDest: %s\n", pszDest); + printf("ConnectNode(): destination: %s\n", + pszDest ? pszDest : addrConnect.ToString().c_str()); } /// debug print @@ -1738,7 +1739,7 @@ void ThreadOnionSeed(void* parg) // Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff. // This is the primary discovery mechanism — seeds.cryptographic-triangles.org - { + if (!GetBoolArg("-noseedurl", false)) { bool ok = false; int delays[] = {0, 30, 60, 120}; for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) { @@ -1806,7 +1807,8 @@ void ThreadOnionSeed(void* parg) else printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound); - ThreadHTTPSeedFetch2(nullptr); + if (!GetBoolArg("-noseedurl", false)) + ThreadHTTPSeedFetch2(nullptr); // Re-queue hardcoded seeds for direct connection for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) { @@ -1891,6 +1893,12 @@ bool ThreadHTTPSeedFetch2(void* parg) seedPath = seedHost.substr(slashPos); seedHost = seedHost.substr(0, slashPos); } + if (seedHost.empty() || seedHost.find_first_of("\r\n") != std::string::npos || + seedPath.empty() || seedPath[0] != '/' || + seedPath.find_first_of("\r\n") != std::string::npos) { + printf("HTTPS seed fetch: invalid -seedurl value\n"); + return false; + } printf("Fetching seed list from https://%s%s (via Tor)...\n", seedHost.c_str(), seedPath.c_str()); @@ -1929,7 +1937,14 @@ bool ThreadHTTPSeedFetch2(void* parg) } // Set SNI hostname (required for Caddy/Let's Encrypt) - SSL_set_tlsext_host_name(ssl, seedHost.c_str()); + if (SSL_set_tlsext_host_name(ssl, seedHost.c_str()) != 1 || + SSL_set1_host(ssl, seedHost.c_str()) != 1) { + printf("HTTPS seed fetch: failed to configure TLS hostname verification\n"); + SSL_free(ssl); + SSL_CTX_free(ctx); + closesocket(hSocket); + return false; + } SSL_set_fd(ssl, (int)hSocket); int ret = SSL_connect(ssl); @@ -1944,6 +1959,15 @@ bool ThreadHTTPSeedFetch2(void* parg) closesocket(hSocket); return false; } + if (SSL_get_verify_result(ssl) != X509_V_OK) { + printf("HTTPS seed fetch: certificate verification failed for %s\n", + seedHost.c_str()); + SSL_shutdown(ssl); + SSL_free(ssl); + SSL_CTX_free(ctx); + closesocket(hSocket); + return false; + } printf("HTTPS seed fetch: TLS connection established to %s\n", seedHost.c_str()); @@ -1973,10 +1997,19 @@ bool ThreadHTTPSeedFetch2(void* parg) // Read response over TLS std::string response; char buf[4096]; + static constexpr size_t MAX_SEED_RESPONSE_SIZE = 1024 * 1024; while (true) { int nBytes = SSL_read(ssl, buf, sizeof(buf)); if (nBytes <= 0) break; + if (response.size() + static_cast(nBytes) > MAX_SEED_RESPONSE_SIZE) { + printf("HTTPS seed fetch: response exceeds 1 MiB limit\n"); + SSL_shutdown(ssl); + SSL_free(ssl); + SSL_CTX_free(ctx); + closesocket(hSocket); + return false; + } response.append(buf, nBytes); } @@ -2002,7 +2035,8 @@ bool ThreadHTTPSeedFetch2(void* parg) // Check status code std::string statusLine = response.substr(0, response.find("\r\n")); - if (statusLine.find("200") == std::string::npos) { + if (statusLine.size() < 12 || statusLine.compare(0, 7, "HTTP/1.") != 0 || + statusLine.compare(9, 3, "200") != 0) { printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str()); return false; } diff --git a/src/rpc_httpsocket.h b/src/rpc_httpsocket.h index 20b9e67..ed38609 100644 --- a/src/rpc_httpsocket.h +++ b/src/rpc_httpsocket.h @@ -141,12 +141,27 @@ inline SOCKET ConnectRPCSocket(const std::string& host, int port) return hSocket; } -// Create listening sockets for the RPC server. When loopbackOnly is true the -// server binds the loopback interface(s) only; otherwise it binds the wildcard -// address(es). IPv4 and IPv6 are bound on separate sockets (IPV6_V6ONLY) so the -// two never conflict. Returns the bound, listening sockets; empty + strError on -// total failure (partial success — e.g. only IPv4 — is returned as success). -inline std::vector BindRPCSockets(int port, bool loopbackOnly, std::string& strError) +inline bool SetRPCSocketTimeouts(SOCKET socket, int timeoutSeconds) +{ +#ifdef WIN32 + DWORD timeout = static_cast(timeoutSeconds * 1000); +#else + struct timeval timeout; + timeout.tv_sec = timeoutSeconds; + timeout.tv_usec = 0; +#endif + const char* value = reinterpret_cast(&timeout); + const socklen_t valueSize = sizeof(timeout); + return ::setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, value, valueSize) == 0 && + ::setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, value, valueSize) == 0; +} + +// Create listening sockets for the RPC server. An empty bindAddress binds only +// localhost. A non-empty value binds exactly that address; "*" explicitly +// requests wildcard addresses. IPv4 and IPv6 use separate sockets when the +// selected name resolves to both families. +inline std::vector BindRPCSockets(int port, const std::string& bindAddress, + std::string& strError) { std::vector vListen; @@ -154,13 +169,15 @@ inline std::vector BindRPCSockets(int port, bool loopbackOnly, std::stri std::memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; // wildcard when node == nullptr + + const bool wildcard = bindAddress == "*"; + if (wildcard) + hints.ai_flags = AI_PASSIVE; struct addrinfo* res = nullptr; const std::string portStr = std::to_string(port); - // "localhost" resolves to the loopback addresses (127.0.0.1 and ::1); - // nullptr + AI_PASSIVE yields the wildcard addresses. - const char* node = loopbackOnly ? "localhost" : nullptr; + const char* node = wildcard ? nullptr : + (bindAddress.empty() ? "localhost" : bindAddress.c_str()); int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res); if (gai != 0) { strError = std::string("RPC bind: getaddrinfo failed: ") + gai_strerror(gai); diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index fc07be9..5a5c942 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -1323,91 +1323,3 @@ Value dumputxoset(const Array& params, bool fHelp) return result; } - -// ============================================================================ -// Trusted snapshot publisher RPCs (Design A: single-slot rotation) -// ============================================================================ -// -// settrustedv2snapshotpublisher
-// - Atomically replaces the active trusted snapshot publisher. -// - The previous publisher is dropped immediately (no grace period). -// - The new publisher is persisted to /snapshot-publisher.json -// so the choice survives daemon restarts. -// -// gettrustedv2snapshotpublisher -// - Returns the currently active runtime override. -// - Empty string means no runtime override; built-in fallback list is -// the source of truth (which contains "TG8f76ykt..."). -// -// unsettrustedv2snapshotpublisher -// - Clears the runtime override. -// - The built-in fallback list (read-only, compiled in) becomes the -// source of truth again. -// - Removes /snapshot-publisher.json. -Value settrustedv2snapshotpublisher(const Array& params, bool fHelp) -{ - if (fHelp || params.size() != 1) - throw runtime_error( - "settrustedv2snapshotpublisher
\n" - "Atomically replace the trusted snapshot publisher.\n" - "The previous publisher is dropped immediately (no grace period).\n" - "The new publisher is persisted to /snapshot-publisher.json.\n" - "\nArguments:\n" - "1. address (string, required) Triangles T-address (34 chars, starts with 'T')\n" - "\nResult:\n" - "{ previous: 'T...', current: 'T...' } (previous is empty if first set)\n" - "\nExample:\n" - " triangles-cli settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH"); - - std::string addr = params[0].get_str(); - std::string previous = Bootstrap::GetActiveTrustedSnapshotPublisher(); - std::string err; - if (!Bootstrap::SetTrustedSnapshotPublisher(addr, err)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, err); - } - Object result; - result.push_back(Pair("previous", previous)); - result.push_back(Pair("current", addr)); - if (!err.empty()) - result.push_back(Pair("warning", err)); - return result; -} - -Value gettrustedv2snapshotpublisher(const Array& params, bool fHelp) -{ - if (fHelp || params.size() != 0) - throw runtime_error( - "gettrustedv2snapshotpublisher\n" - "Returns the currently active trusted snapshot publisher.\n" - "Empty string means no runtime override is set; the built-in\n" - "fallback list (compiled in) is the source of truth.\n" - "\nResult:\n" - "{ active: 'T...', has_runtime_override: true|false }"); - - std::string active = Bootstrap::GetActiveTrustedSnapshotPublisher(); - Object result; - result.push_back(Pair("active", active)); - result.push_back(Pair("has_runtime_override", !active.empty())); - return result; -} - -Value unsettrustedv2snapshotpublisher(const Array& params, bool fHelp) -{ - if (fHelp || params.size() != 0) - throw runtime_error( - "unsettrustedv2snapshotpublisher\n" - "Clear the runtime trusted snapshot publisher override.\n" - "The built-in fallback list (compiled in) becomes the source of truth again.\n" - "Removes /snapshot-publisher.json.\n" - "\nResult:\n" - "{ unset: true, fallback_in_effect: true }"); - - std::string err; - if (!Bootstrap::UnsetTrustedSnapshotPublisher(err)) { - throw JSONRPCError(RPC_INTERNAL_ERROR, err); - } - Object result; - result.push_back(Pair("unset", true)); - result.push_back(Pair("fallback_in_effect", true)); - return result; -} diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index 34e9ab0..126efed 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -13,6 +13,8 @@ #include "tor/onion_v3.h" #include "tor/tor_embedded.h" +#include + using namespace json_spirit; using namespace std; @@ -1519,7 +1521,7 @@ Value walletpassphrase(const Array& params, bool fHelp) if (pwalletMain->IsCrypted() && (fHelp || params.size() < 2 || params.size() > 3)) throw runtime_error( "walletpassphrase [stakingonly]\n" - "Stores the wallet decryption key in memory for seconds.\n" + "Stores the wallet decryption key in memory for seconds (1-604800).\n" "if [stakingonly] is true sending functions are disabled."); if (fHelp) return true; @@ -1530,6 +1532,14 @@ Value walletpassphrase(const Array& params, bool fHelp) if (!pwalletMain->IsLocked()) throw JSONRPCError(RPC_WALLET_ALREADY_UNLOCKED, "Error: Wallet is already unlocked, use walletlock first if need to change unlock settings."); + + const int64_t timeoutSeconds = params[1].get_int64(); + if (timeoutSeconds < 1 || timeoutSeconds > 7 * 24 * 60 * 60) + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Wallet unlock timeout must be between 1 and 604800 seconds."); + const bool stakingOnly = params.size() > 2 ? params[2].get_bool() : false; + std::unique_ptr sleepTime(new int64_t(timeoutSeconds)); + // Note that the walletpassphrase is stored in params[0] which is not mlock()ed SecureString strWalletPass; strWalletPass.reserve(100); @@ -1545,15 +1555,18 @@ Value walletpassphrase(const Array& params, bool fHelp) "walletpassphrase \n" "Stores the wallet decryption key in memory for seconds."); - NewThread(ThreadTopUpKeyPool, nullptr); - int64_t* pnSleepTime = new int64_t(params[1].get_int64()); - NewThread(ThreadCleanWalletPassphrase, pnSleepTime); - // triangles: if user OS account compromised prevent trivial sendmoney commands - if (params.size() > 2) - fWalletUnlockStakingOnly = params[2].get_bool(); - else + fWalletUnlockStakingOnly = stakingOnly; + if (!NewThread(ThreadCleanWalletPassphrase, sleepTime.get())) { + pwalletMain->Lock(); fWalletUnlockStakingOnly = false; + throw JSONRPCError(RPC_WALLET_ERROR, + "Could not start the wallet relock timer; wallet was locked again."); + } + sleepTime.release(); + + if (!NewThread(ThreadTopUpKeyPool, nullptr)) + printf("walletpassphrase: could not start background keypool refill\n"); return Value::null; } diff --git a/src/scrypt-arm.S b/src/scrypt-arm.S index 143eb3a..3e4af7f 100644 --- a/src/scrypt-arm.S +++ b/src/scrypt-arm.S @@ -553,3 +553,8 @@ scrypt_core_loop2: #endif #endif + + +#if defined(__ELF__) + .section .note.GNU-stack,"",%progbits +#endif diff --git a/src/scrypt-x86.S b/src/scrypt-x86.S index c1d2cef..9c8c25c 100644 --- a/src/scrypt-x86.S +++ b/src/scrypt-x86.S @@ -910,3 +910,7 @@ xmm_scrypt_core_loop2: ret #endif + +#if defined(__ELF__) + .section .note.GNU-stack,"",@progbits +#endif diff --git a/src/scrypt-x86_64.S b/src/scrypt-x86_64.S index 7f22e40..e53654e 100644 --- a/src/scrypt-x86_64.S +++ b/src/scrypt-x86_64.S @@ -856,4 +856,8 @@ xmm_scrypt_core_loop2: popq %rbx ret -#endif \ No newline at end of file +#endif + +#if defined(__ELF__) + .section .note.GNU-stack,"",@progbits +#endif diff --git a/src/smessage.cpp b/src/smessage.cpp index 9aa5a77..d5acdea 100644 --- a/src/smessage.cpp +++ b/src/smessage.cpp @@ -3555,6 +3555,8 @@ int SecureMsgValidate(unsigned char *pHeader, unsigned char *pPayload, uint32_t memcpy(civ+i, &nonse, 4); HMAC_CTX *ctx = HMAC_CTX_new(); + if (ctx == nullptr) + return 1; unsigned int nBytes; if (!HMAC_Init_ex(ctx, &civ[0], 32, EVP_sha256(), nullptr) @@ -3571,7 +3573,7 @@ int SecureMsgValidate(unsigned char *pHeader, unsigned char *pPayload, uint32_t { if (sha256Hash[31] == 0 && sha256Hash[30] == 0 - && (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) )) + && (sha256Hash[29] & 1U) == 0) { if (fDebugSmsg) printf("Hash Valid.\n"); @@ -3614,6 +3616,8 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n bool found = false; HMAC_CTX *ctx = HMAC_CTX_new(); + if (ctx == nullptr) + return 1; uint32_t nonse = 0; @@ -3655,7 +3659,7 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n if (sha256Hash[31] == 0 && sha256Hash[30] == 0 - && (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) )) + && (sha256Hash[29] & 1U) == 0) // && sha256Hash[29] == 0) { found = true; @@ -3794,7 +3798,10 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string& // -- Generate 16 random bytes as IV. RandAddSeedPerfmon(); - RAND_bytes(&smsg.iv[0], 16); + if (RAND_bytes(&smsg.iv[0], 16) != 1) { + printf("Could not generate a secure message IV.\n"); + return 1; + } // -- Generate a new random EC key pair with private key called r and public key called R. @@ -3959,14 +3966,16 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string& unsigned int nBytes = 32; HMAC_CTX *ctx = HMAC_CTX_new(); - if (!HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr) + if (ctx == nullptr + || !HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr) || !HMAC_Update(ctx, (unsigned char*) &smsg.timestamp, sizeof(smsg.timestamp)) || !HMAC_Update(ctx, &vchCiphertext[0], vchCiphertext.size()) || !HMAC_Final(ctx, smsg.mac, &nBytes) || nBytes != 32) fHmacOk = false; - HMAC_CTX_free(ctx); + if (ctx != nullptr) + HMAC_CTX_free(ctx); if (!fHmacOk) { @@ -4269,14 +4278,16 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, unsigned char *pHeade unsigned int nBytes = 32; HMAC_CTX *ctx = HMAC_CTX_new(); - if (!HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr) + if (ctx == nullptr + || !HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr) || !HMAC_Update(ctx, (unsigned char*) &psmsg->timestamp, sizeof(psmsg->timestamp)) || !HMAC_Update(ctx, pPayload, nPayload) || !HMAC_Final(ctx, MAC, &nBytes) || nBytes != 32) fHmacOk = false; - HMAC_CTX_free(ctx); + if (ctx != nullptr) + HMAC_CTX_free(ctx); if (!fHmacOk) { @@ -4430,4 +4441,3 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, SecureMessage& smsg, { return SecureMsgDecrypt(fTestOnly, address, &smsg.hash[0], smsg.pPayload, smsg.nPayload, msg); }; - diff --git a/src/snapshotnet.cpp b/src/snapshotnet.cpp index 369da2f..9b08976 100644 --- a/src/snapshotnet.cpp +++ b/src/snapshotnet.cpp @@ -120,26 +120,18 @@ static bool VerifyDestFileHash(std::string& strErr) return false; } fflush(g_fetch.fpDest); - fseek(g_fetch.fpDest, 0, SEEK_SET); - SHA256_CTX ctx; - SHA256_Init(&ctx); - - std::vector buf(64 * 1024); - int64_t total = 0; - while (true) { - size_t n = fread(buf.data(), 1, buf.size(), g_fetch.fpDest); - if (n == 0) break; - SHA256_Update(&ctx, buf.data(), n); - total += (int64_t)n; - } - if (total != g_fetch.totalSize) { - strErr = strprintf("size mismatch: have %" PRId64 " want %" PRId64, total, g_fetch.totalSize); + std::error_code ec; + const int64_t total = static_cast(fs::file_size(g_fetch.destPath, ec)); + if (ec || total != g_fetch.totalSize) { + strErr = strprintf("size mismatch: have %" PRId64 " want %" PRId64, + ec ? -1 : total, g_fetch.totalSize); return false; } uint256 actual; - SHA256_Final((unsigned char*)&actual, &ctx); + if (!ComputeSnapshotFileHash(g_fetch.destPath, actual, strErr)) + return false; if (actual != g_fetch.expectedFileHash) { strErr = "snapshot file hash mismatch"; return false; @@ -243,6 +235,46 @@ static void ReissueStalledChunks(int64_t timeoutMicros) } // namespace +bool ComputeSnapshotFileHash(const fs::path& path, + uint256& fileHash, + std::string& strError) +{ + FILE* file = fopen(path.string().c_str(), "rb"); + if (!file) { + strError = "cannot open snapshot for hashing: " + path.string(); + return false; + } + + SHA256_CTX ctx; + SHA256_Init(&ctx); + std::vector buffer(64 * 1024); + while (true) { + const size_t count = fread(buffer.data(), 1, buffer.size(), file); + if (count > 0) + SHA256_Update(&ctx, buffer.data(), count); + if (count < buffer.size()) { + if (ferror(file)) { + fclose(file); + strError = "failed reading snapshot while hashing"; + return false; + } + break; + } + } + fclose(file); + + unsigned char digest[SHA256_DIGEST_LENGTH]; + SHA256_Final(digest, &ctx); + static const char hex[] = "0123456789abcdef"; + std::string digestHex(SHA256_DIGEST_LENGTH * 2, '0'); + for (size_t i = 0; i < SHA256_DIGEST_LENGTH; ++i) { + digestHex[2 * i] = hex[(digest[i] >> 4) & 0x0f]; + digestHex[2 * i + 1] = hex[digest[i] & 0x0f]; + } + fileHash.SetHex(digestHex); + return true; +} + // --------------------------------------------------------------------------- // Public: TryFetchSnapshot // --------------------------------------------------------------------------- @@ -424,24 +456,12 @@ static bool ScanLocalSnapshot() int64_t sz = (int64_t)fs::file_size(g_localPath, ec); if (ec) return false; - // Hash the file once on first scan to confirm it matches the compiled-in - // snapshot hash. A node won't advertise NODE_SNAPSHOT if the local file is - // corrupt or for a different height. - FILE* f = fopen(g_localPath.string().c_str(), "rb"); - if (!f) return false; - - SHA256_CTX ctx; - SHA256_Init(&ctx); - std::vector buf(64 * 1024); - while (true) { - size_t n = fread(buf.data(), 1, buf.size(), f); - if (n == 0) break; - SHA256_Update(&ctx, buf.data(), n); - } - fclose(f); - uint256 actual; - SHA256_Final((unsigned char*)&actual, &ctx); + std::string hashError; + if (!ComputeSnapshotFileHash(g_localPath, actual, hashError)) { + printf("SnapshotNet: cannot hash local snapshot: %s\n", hashError.c_str()); + return false; + } if (actual != expectedHash) { printf("SnapshotNet: local utxo-snapshot.bin hash mismatch — not advertising\n"); return false; diff --git a/src/snapshotnet.h b/src/snapshotnet.h index ba9ebe3..7668dd5 100644 --- a/src/snapshotnet.h +++ b/src/snapshotnet.h @@ -51,6 +51,12 @@ bool TryFetchSnapshot(const std::filesystem::path& dataDir, int timeoutSec, std::string& strError); +// Return SHA256 in conventional display byte order, matching sha256sum and +// the hexadecimal values compiled into checkpoints.cpp. +bool ComputeSnapshotFileHash(const std::filesystem::path& path, + uint256& fileHash, + std::string& strError); + // Server-side message dispatch. Called from main.cpp ProcessMessage. // Returns true if strCommand was a snapshot-protocol message (handled or // rejected for malformed input). diff --git a/src/sync.h b/src/sync.h index 7a960c1..3de6062 100644 --- a/src/sync.h +++ b/src/sync.h @@ -65,8 +65,7 @@ public: if (!lock.owns_lock()) { EnterCritical(pszName, pszFile, nLine, (void*)(lock.mutex()), true); - lock.try_lock(); - if (!lock.owns_lock()) + if (!lock.try_lock()) LeaveCritical(); } return lock.owns_lock(); @@ -204,4 +203,3 @@ public: } }; #endif - diff --git a/src/test/bignum_tests.cpp b/src/test/bignum_tests.cpp index b161f2c..2064853 100644 --- a/src/test/bignum_tests.cpp +++ b/src/test/bignum_tests.cpp @@ -1,125 +1,74 @@ #include #include +#include #include "bignum.h" #include "util.h" BOOST_AUTO_TEST_SUITE(bignum_tests) -// Unfortunately there's no standard way of preventing a function from being -// inlined, so we define a macro for it. -// -// You should use it like this: -// NOINLINE void function() {...} #if defined(__GNUC__) -// This also works and will be defined for any compiler implementing GCC -// extensions, such as Clang and ICC. #define NOINLINE __attribute__((noinline)) #elif defined(_MSC_VER) #define NOINLINE __declspec(noinline) #else -// We give out a warning because it impacts the correctness of one bignum test. -#warning You should define NOINLINE for your compiler. #define NOINLINE #endif -// For the following test case, it is useful to use additional tools. -// -// The simplest one to use is the compiler flag -ftrapv, which detects integer -// overflows and similar errors. However, due to optimizations and compilers -// taking advantage of undefined behavior sometimes it may not actually detect -// anything. -// -// You can also use compiler-based stack protection to possibly detect possible -// stack buffer overruns. -// -// For more accurate diagnostics, you can use an undefined arithmetic operation -// detector such as the clang-based tool: -// -// "IOC: An Integer Overflow Checker for C/C++" -// -// Available at: http://embed.cs.utah.edu/ioc/ -// -// It might also be useful to use Google's AddressSanitizer to detect -// stack buffer overruns, which valgrind can't currently detect. - -// Let's force this code not to be inlined, in order to actually -// test a generic version of the function. This increases the chance -// that -ftrapv will detect overflows. NOINLINE void mysetint64(CBigNum& num, int64_t n) { num.setint64(n); } -// For each number, we do 2 tests: one with inline code, then we reset the -// value to 0, then the second one with a non-inlined function. BOOST_AUTO_TEST_CASE(bignum_setint64) { - int64_t n; + const int64_t values[] = { + 0, + 1, + -1, + 5, + -5, + std::numeric_limits::min(), + std::numeric_limits::max(), + }; - { - n = 0; - CBigNum num(n); - BOOST_CHECK(num.ToString() == "0"); + for (int64_t value : values) { + CBigNum num(value); + BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value)); num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "0"); - } - { - n = 1; - CBigNum num(n); - BOOST_CHECK(num.ToString() == "1"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "1"); - } - { - n = -1; - CBigNum num(n); - BOOST_CHECK(num.ToString() == "-1"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "-1"); - } - { - n = 5; - CBigNum num(n); - BOOST_CHECK(num.ToString() == "5"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "5"); - } - { - n = -5; - CBigNum num(n); - BOOST_CHECK(num.ToString() == "-5"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "-5"); - } - { - n = std::numeric_limits::min(); - CBigNum num(n); - BOOST_CHECK(num.ToString() == "-9223372036854775808"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "-9223372036854775808"); - } - { - n = std::numeric_limits::max(); - CBigNum num(n); - BOOST_CHECK(num.ToString() == "9223372036854775807"); - num.setulong(0); - BOOST_CHECK(num.ToString() == "0"); - mysetint64(num, n); - BOOST_CHECK(num.ToString() == "9223372036854775807"); + BOOST_CHECK_EQUAL(num.ToString(), "0"); + mysetint64(num, value); + BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value)); } } +BOOST_AUTO_TEST_CASE(bignum_uint64_roundtrip_boundaries) +{ + const uint64_t values[] = { + 0, + 1, + 0x7f, + 0x80, + uint64_t{1} << 32, + uint64_t{1} << 63, + std::numeric_limits::max(), + }; + + for (uint64_t value : values) { + CBigNum num(value); + BOOST_CHECK_EQUAL(num.getuint64(), value); + } + + CBigNum negative(-1); + BOOST_CHECK_EQUAL(negative.getuint64(), uint64_t{1}); +} + +BOOST_AUTO_TEST_CASE(bignum_rejects_invalid_output_base) +{ + CBigNum value(42); + BOOST_CHECK_THROW(value.ToString(0), bignum_error); + BOOST_CHECK_THROW(value.ToString(1), bignum_error); + BOOST_CHECK_THROW(value.ToString(17), bignum_error); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/bootstrap_security_tests.cpp b/src/test/bootstrap_security_tests.cpp new file mode 100644 index 0000000..7e27d12 --- /dev/null +++ b/src/test/bootstrap_security_tests.cpp @@ -0,0 +1,79 @@ +// Copyright (c) 2026 Triangles developers +// Distributed under the MIT/X11 software license + +#include + +#include "../bootstrap.h" + +namespace { + +std::string ManifestWithFilename(const std::string& filename) +{ + return std::string(R"json({ + "version": "1.6", + "chain_tip": { + "height": 2206004, + "blockhash": "b34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46" + }, + "files": { + ")json") + filename + R"json(": { + "sha256": "1419282DAE817315EE1B955543F6248233FE5800F5E8488734A0ECE5BD6781EA", + "type": "utxo_snapshot_v3" + } + }, + "canonical": { "snapshot": ")json" + filename + R"json(" } + })json"; +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(bootstrap_security_tests) + +BOOST_AUTO_TEST_CASE(remote_manifest_parses_canonical_snapshot) +{ + Bootstrap::RemoteSnapshot snapshot; + std::string error; + BOOST_REQUIRE(Bootstrap::ParseRemoteSnapshotManifest( + ManifestWithFilename("utxo-snapshot.bin"), snapshot, error)); + BOOST_CHECK_EQUAL(snapshot.filename, "utxo-snapshot.bin"); + BOOST_CHECK_EQUAL(snapshot.height, 2206004); + BOOST_CHECK_EQUAL( + snapshot.sha256, + "1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea"); +} + +BOOST_AUTO_TEST_CASE(remote_manifest_rejects_path_traversal) +{ + Bootstrap::RemoteSnapshot snapshot; + std::string error; + BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest( + ManifestWithFilename("../../wallet.dat"), snapshot, error)); + BOOST_CHECK_NE(error.find("plain filename"), std::string::npos); +} + +BOOST_AUTO_TEST_CASE(remote_manifest_rejects_malformed_hashes) +{ + std::string manifest = ManifestWithFilename("utxo-snapshot.bin"); + const std::string validHash = + "1419282DAE817315EE1B955543F6248233FE5800F5E8488734A0ECE5BD6781EA"; + manifest.replace(manifest.find(validHash), validHash.size(), "not-a-sha256"); + + Bootstrap::RemoteSnapshot snapshot; + std::string error; + BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest(manifest, snapshot, error)); + BOOST_CHECK_NE(error.find("invalid"), std::string::npos); +} + +BOOST_AUTO_TEST_CASE(remote_manifest_rejects_non_snapshot_canonical_file) +{ + std::string manifest = ManifestWithFilename("wallet.dat"); + const std::string snapshotType = "utxo_snapshot_v3"; + manifest.replace(manifest.find(snapshotType), snapshotType.size(), "wallet_backup"); + + Bootstrap::RemoteSnapshot snapshot; + std::string error; + BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest(manifest, snapshot, error)); + BOOST_CHECK_NE(error.find("not a UTXO snapshot"), std::string::npos); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/chaindb_equivalence_tests_main.cpp b/src/test/chaindb_equivalence_tests_main.cpp index 4e1384f..1468731 100644 --- a/src/test/chaindb_equivalence_tests_main.cpp +++ b/src/test/chaindb_equivalence_tests_main.cpp @@ -34,6 +34,7 @@ bool fUseFastIndex = false; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT; void StartShutdown() { /* no-op for tests */ } +void MarkShutdownFailure() { /* no-op for tests */ } namespace { diff --git a/src/test/chaindb_runtime_tests.cpp b/src/test/chaindb_runtime_tests.cpp index d4f45aa..53a0655 100644 --- a/src/test/chaindb_runtime_tests.cpp +++ b/src/test/chaindb_runtime_tests.cpp @@ -125,6 +125,7 @@ bool fUseFastIndex = false; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT; void StartShutdown() { /* no-op */ } +void MarkShutdownFailure() { /* no-op for tests */ } namespace { diff --git a/src/test/consensus_safety_tests.cpp b/src/test/consensus_safety_tests.cpp index d531194..06497ea 100644 --- a/src/test/consensus_safety_tests.cpp +++ b/src/test/consensus_safety_tests.cpp @@ -358,6 +358,23 @@ BOOST_AUTO_TEST_CASE(pos_validation_skip_is_only_historical_fast_path) nAssumeValidThreshold = oldAssumeValid; } +BOOST_AUTO_TEST_CASE(pos_block_signature_is_required_above_hardened_checkpoint) +{ + const int oldAssumeValid = nAssumeValidThreshold; + const int checkpointHeight = Checkpoints::GetTotalBlocksEstimate(); + + BOOST_CHECK(!IsBlockSignatureRequiredAtHeight(checkpointHeight)); + BOOST_CHECK(IsBlockSignatureRequiredAtHeight(checkpointHeight + 1)); + + // A rolling performance threshold must never authorize unsigned live + // blocks, including when stale-tip state makes the node report IBD. + nAssumeValidThreshold = checkpointHeight + 100; + BOOST_CHECK(IsConsensusAssumeValidHeight(checkpointHeight + 50)); + BOOST_CHECK(IsBlockSignatureRequiredAtHeight(checkpointHeight + 50)); + + nAssumeValidThreshold = oldAssumeValid; +} + // ─── Orphan block cap (P1 — DoS) ────────────────────────────────────────── // The cap on stored orphan blocks prevents an attacker from filling // memory with garbage. If too low, legitimate orphans are dropped. If diff --git a/src/test/snapshotnet_tests.cpp b/src/test/snapshotnet_tests.cpp index b89bd18..d5cfad7 100644 --- a/src/test/snapshotnet_tests.cpp +++ b/src/test/snapshotnet_tests.cpp @@ -70,6 +70,7 @@ bool fUseFastIndex = false; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT; void StartShutdown() { /* no-op for tests */ } +void MarkShutdownFailure() { /* no-op for tests */ } namespace { @@ -100,19 +101,9 @@ struct TmpDataDir // Compute SHA-256 of a file's bytes. uint256 Sha256OfFile(const fs::path& p) { - FILE* f = fopen(p.string().c_str(), "rb"); - BOOST_REQUIRE_MESSAGE(f != nullptr, "open failed: " << p.string()); - SHA256_CTX ctx; - SHA256_Init(&ctx); - std::vector buf(64 * 1024); - while (true) { - size_t n = fread(buf.data(), 1, buf.size(), f); - if (n == 0) break; - SHA256_Update(&ctx, buf.data(), n); - } - fclose(f); uint256 out; - SHA256_Final(reinterpret_cast(&out), &ctx); + std::string error; + BOOST_REQUIRE_MESSAGE(SnapshotNet::ComputeSnapshotFileHash(p, out, error), error); return out; } @@ -121,8 +112,16 @@ uint256 Sha256OfBytes(const std::vector& bytes) SHA256_CTX ctx; SHA256_Init(&ctx); SHA256_Update(&ctx, bytes.data(), bytes.size()); + unsigned char digest[SHA256_DIGEST_LENGTH]; + SHA256_Final(digest, &ctx); + static const char hex[] = "0123456789abcdef"; + std::string digestHex(SHA256_DIGEST_LENGTH * 2, '0'); + for (size_t i = 0; i < SHA256_DIGEST_LENGTH; ++i) { + digestHex[2 * i] = hex[(digest[i] >> 4) & 0x0f]; + digestHex[2 * i + 1] = hex[digest[i] & 0x0f]; + } uint256 out; - SHA256_Final(reinterpret_cast(&out), &ctx); + out.SetHex(digestHex); return out; } @@ -177,6 +176,17 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(snapshotnet_hash) +BOOST_AUTO_TEST_CASE(file_hash_uses_standard_sha256_display_order) +{ + TmpDataDir td; + fs::path p = td.path / "abc.bin"; + WriteFile(p, {'a', 'b', 'c'}); + + BOOST_CHECK_EQUAL( + Sha256OfFile(p).ToString(), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); +} + BOOST_AUTO_TEST_CASE(file_hash_matches_inline_sha256) { // Synthesize a payload, hash it via stdlib openssl directly, then hash diff --git a/src/test/test_triangles.cpp b/src/test/test_triangles.cpp index 3090d93..17f0fb8 100644 --- a/src/test/test_triangles.cpp +++ b/src/test/test_triangles.cpp @@ -69,3 +69,4 @@ void StartShutdown() exit(0); } +void MarkShutdownFailure() { /* no-op for tests */ } diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index a8db143..757a0dc 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -168,6 +168,15 @@ BOOST_AUTO_TEST_CASE(util_WildcardMatch) BOOST_CHECK(WildcardMatch("abcdef", "a*f")); BOOST_CHECK(!WildcardMatch("abcdef", "a*x")); BOOST_CHECK(WildcardMatch("", "*")); + + const std::string address = "192.0.2.44"; + const std::string allow = "192.0.2.*"; + BOOST_CHECK(WildcardMatch(std::string_view(address), std::string_view(allow))); + + // A long non-match must not recurse once per wildcard/input combination. + const std::string longInput(4096, 'a'); + const std::string longMask = "*a*a*a*a*a*a*a*a*a*a*b"; + BOOST_CHECK(!WildcardMatch(std::string_view(longInput), std::string_view(longMask))); } BOOST_AUTO_TEST_CASE(util_FormatMoney) diff --git a/src/test/wallet_tests.cpp b/src/test/wallet_tests.cpp index bb477b8..25e63e6 100644 --- a/src/test/wallet_tests.cpp +++ b/src/test/wallet_tests.cpp @@ -294,6 +294,57 @@ BOOST_AUTO_TEST_CASE(coin_selection_tests) BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(wallet_security_tests) + +BOOST_AUTO_TEST_CASE(hd_key_generation_fails_when_seed_is_unavailable) +{ + CWallet wallet; + wallet.fHDEnabled = true; + + BOOST_CHECK_THROW(wallet.GenerateNewKey(), std::runtime_error); + BOOST_CHECK_EQUAL(wallet.nHDChainIndex, 0); +} + +BOOST_AUTO_TEST_CASE(memory_wallet_encryption_roundtrip_preserves_keys) +{ + CWallet wallet; + CKey original; + original.MakeNewKey(true); + BOOST_REQUIRE(wallet.AddKey(original)); + + SecureString passphrase; + passphrase.reserve(100); + passphrase = "correct horse battery staple"; + + const auto keypoolIt = mapArgs.find("-keypool"); + const bool hadKeypoolArg = keypoolIt != mapArgs.end(); + const std::string oldKeypoolArg = hadKeypoolArg ? keypoolIt->second : std::string(); + mapArgs["-keypool"] = "0"; + const bool encrypted = wallet.EncryptWallet(passphrase); + if (hadKeypoolArg) + mapArgs["-keypool"] = oldKeypoolArg; + else + mapArgs.erase("-keypool"); + + BOOST_REQUIRE(encrypted); + BOOST_CHECK(wallet.IsCrypted()); + BOOST_CHECK(wallet.IsLocked()); + + SecureString wrongPassphrase; + wrongPassphrase.reserve(100); + wrongPassphrase = "wrong passphrase"; + BOOST_CHECK(!wallet.Unlock(wrongPassphrase)); + BOOST_CHECK(wallet.IsLocked()); + + BOOST_REQUIRE(wallet.Unlock(passphrase)); + CKey recovered; + BOOST_REQUIRE(wallet.GetKey(original.GetPubKey().GetID(), recovered)); + BOOST_CHECK(recovered.GetPubKey() == original.GetPubKey()); + BOOST_CHECK(wallet.Lock()); +} + +BOOST_AUTO_TEST_SUITE_END() + // ---------------------------------------------------------------------------- // AbandonTransaction tests // diff --git a/src/triangles-cli.cpp b/src/triangles-cli.cpp index 1c6fbc4..37b7c5c 100644 --- a/src/triangles-cli.cpp +++ b/src/triangles-cli.cpp @@ -19,7 +19,7 @@ // Connection parameters (highest precedence first): // 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport // 2. triangles.conf in the data directory (or -conf=) -// 3. Defaults: 127.0.0.1:19111 mainnet, 19112 testnet; no auth (must be set in conf) +// 3. Defaults: 127.0.0.1:19112 mainnet, 19111 testnet; no auth (must be set in conf) // // Usage: // triangles-cli help List commands (delegates to daemon) @@ -70,6 +70,7 @@ #define TRI_CLI_CLOSE_SOCKET(s) closesocket(s) #else #include + #include #include #include #include @@ -108,10 +109,20 @@ static bool GetBoolArg(const string& key, bool def) return (v != "0" && v != "false" && v != "no"); } -static void ReadConfigFile(const string& path) +static bool ReadConfigFile(const string& path, string& error) { +#ifndef _WIN32 + struct stat configStat; + if (::lstat(path.c_str(), &configStat) == 0 && + (!S_ISREG(configStat.st_mode) || configStat.st_uid != geteuid() || + (configStat.st_mode & (S_IRWXG | S_IRWXO)) != 0)) { + error = "refusing insecure configuration file " + path + + "; require a regular file owned by the current user with no group or other access"; + return false; + } +#endif ifstream f(path); - if (!f.good()) return; + if (!f.good()) return true; string line; while (getline(f, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); @@ -141,6 +152,7 @@ static void ReadConfigFile(const string& path) mapMultiArgs[dashKey].push_back(value); } } + return true; } static fs::path GetDefaultDataDir() @@ -231,7 +243,7 @@ static void ParseCommandLine(int argc, char* const argv[]) struct RPCConn { string host = "127.0.0.1"; - string port = "19111"; + string port = "19112"; string user; string pass; }; @@ -244,11 +256,17 @@ static int AppInitRPCConn(RPCConn& conn) { std::ifstream f(confPath); confExisted = f.good(); - if (confExisted) ReadConfigFile(confPath.string()); + if (confExisted) { + string configError; + if (!ReadConfigFile(confPath.string(), configError)) { + cerr << "triangles-cli: " << configError << "\n"; + return 1; + } + } } bool fTestNet = GetBoolArg("-testnet", false); - conn.port = GetArg("-rpcport", fTestNet ? "19112" : "19111"); + conn.port = GetArg("-rpcport", fTestNet ? "19111" : "19112"); conn.host = GetArg("-rpcconnect", "127.0.0.1"); conn.user = GetArg("-rpcuser", ""); conn.pass = GetArg("-rpcpassword", ""); @@ -595,9 +613,9 @@ static int CommandLineHelp(ostream& out) << "Options:\n" << " -conf= Specify configuration file (default: triangles.conf)\n" << " -datadir= Specify data directory\n" - << " -testnet Use testnet (RPC port 19112)\n" + << " -testnet Use testnet (RPC port 19111)\n" << " -rpcconnect= Send commands to node running on (default: 127.0.0.1)\n" - << " -rpcport= Connect to JSON-RPC on (default: 19111 or testnet: 19112)\n" + << " -rpcport= Connect to JSON-RPC on (default: 19112 or testnet: 19111)\n" << " -rpcuser= Username for JSON-RPC connections\n" << " -rpcpassword= Password for JSON-RPC connections\n" << " -stdin Read extra params from standard input, one per line\n" diff --git a/src/trianglesrpc.cpp b/src/trianglesrpc.cpp index a6d313f..ad4957e 100644 --- a/src/trianglesrpc.cpp +++ b/src/trianglesrpc.cpp @@ -17,10 +17,12 @@ #undef printf #include "rpc_httpsocket.h" // raw-socket HTTP transport (replaces Boost.Asio) -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #ifndef WIN32 #include @@ -34,13 +36,26 @@ namespace fs = std::filesystem; void ThreadRPCServer2(void* parg); -static std::string strRPCUserColonPass; +static std::string strRPCUserColonPass; const Object emptyobj; CNotificationQueue* pNotificationQueue = nullptr; -void ThreadRPCServer3(void* parg); +void ThreadRPCServer3(void* parg); + +static bool RPCMethodAllowed(const std::string& method) +{ + const auto it = mapMultiArgs.find("-rpcallowmethod"); + if (it == mapMultiArgs.end() || it->second.empty()) + return true; + + for (const std::string& allowed : it->second) { + if (allowed == method) + return true; + } + return false; +} static inline unsigned short GetDefaultRPCPort() { @@ -316,11 +331,8 @@ static const CRPCCommand vRPCCommands[] = { "sendrawtransaction", &sendrawtransaction, false, false }, { "getcheckpoint", &getcheckpoint, true, false }, { "gencheckpoints", &gencheckpoints, true, false }, - { "publishcheckpoint", &publishcheckpoint, true, false }, - { "settrustedv2snapshotpublisher", &settrustedv2snapshotpublisher, false, false }, - { "gettrustedv2snapshotpublisher", &gettrustedv2snapshotpublisher, false, false }, - { "unsettrustedv2snapshotpublisher", &unsettrustedv2snapshotpublisher, false, false }, - { "getchaintips", &getchaintips, true, false }, + { "publishcheckpoint", &publishcheckpoint, true, false }, + { "getchaintips", &getchaintips, true, false }, { "invalidateblock", &invalidateblock, false, false }, { "reconsiderblock", &reconsiderblock, false, false }, { "recalculatesupply", &recalculatesupply, false, false }, @@ -455,11 +467,27 @@ static string HTTPReply(int nStatus, const string& strMsg, bool keepalive) strMsg.c_str()); } -int ReadHTTPStatus(std::basic_istream& stream, int &proto, - string& strMethodHTTP, string& strURI) -{ - string str; - getline(stream, str); +static bool ReadHTTPLine(std::basic_istream& stream, std::string& line, + size_t maxLength) +{ + line.clear(); + char c = 0; + while (stream.get(c)) { + if (c == '\n') + return true; + if (line.size() >= maxLength) + return false; + line.push_back(c); + } + return false; +} + +int ReadHTTPStatus(std::basic_istream& stream, int &proto, + string& strMethodHTTP, string& strURI) +{ + string str; + if (!ReadHTTPLine(stream, str, 8192)) + return HTTP_BAD_REQUEST; // Trim trailing \r if (!str.empty() && str[str.size()-1] == '\r') str.resize(str.size()-1); @@ -482,27 +510,45 @@ int ReadHTTPStatus(std::basic_istream& stream, int &proto, return atoi(vWords[1].c_str()); } -int ReadHTTPHeader(std::basic_istream& stream, map& mapHeadersRet) -{ - int nLen = 0; - while (true) - { - string str; - std::getline(stream, str); - if (str.empty() || str == "\r") - break; +int ReadHTTPHeader(std::basic_istream& stream, map& mapHeadersRet) +{ + int nLen = 0; + size_t totalHeaderBytes = 0; + while (true) + { + string str; + if (!ReadHTTPLine(stream, str, 8192)) + return -1; + totalHeaderBytes += str.size() + 1; + if (totalHeaderBytes > 64 * 1024) + return -1; + if (str.empty() || str == "\r") + break; string::size_type nColon = str.find(":"); if (nColon != string::npos) { - string strHeader = str.substr(0, nColon); - strHeader = TrimString(strHeader); - strHeader = ToLower(strHeader); - string strValue = str.substr(nColon+1); - strValue = TrimString(strValue); - mapHeadersRet[strHeader] = strValue; - if (strHeader == "content-length") - nLen = atoi(strValue.c_str()); - } + string strHeader = str.substr(0, nColon); + strHeader = TrimString(strHeader); + strHeader = ToLower(strHeader); + string strValue = str.substr(nColon+1); + strValue = TrimString(strValue); + if (strHeader == "transfer-encoding" || + (strHeader == "content-length" && mapHeadersRet.count(strHeader) != 0)) { + return -1; + } + mapHeadersRet[strHeader] = strValue; + if (strHeader == "content-length") { + errno = 0; + char* end = nullptr; + const unsigned long long parsed = std::strtoull(strValue.c_str(), &end, 10); + if (errno != 0 || end == strValue.c_str() || *end != '\0' || + parsed > static_cast(MAX_SIZE) || + parsed > static_cast(std::numeric_limits::max())) { + return -1; + } + nLen = static_cast(parsed); + } + } } return nLen; } @@ -702,8 +748,16 @@ void ThreadRPCServer2(void* parg) if ((mapArgs["-rpcpassword"] == "") || (mapArgs["-rpcuser"] == mapArgs["-rpcpassword"])) { - unsigned char rand_pwd[32]; - RAND_bytes(rand_pwd, 32); + unsigned char rand_pwd[32]; + if (RAND_bytes(rand_pwd, sizeof(rand_pwd)) != 1) { + uiInterface.ThreadSafeMessageBox( + _("Unable to generate a secure suggested RPC password. " + "The RPC server will not start."), + _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); + MarkShutdownFailure(); + StartShutdown(); + return; + } string strWhatAmI = "To use trianglesd"; if (mapArgs.count("-server")) strWhatAmI = strprintf(_("To use the %s option"), "\"-server\""); @@ -720,8 +774,9 @@ void ThreadRPCServer2(void* parg) strWhatAmI.c_str(), GetConfigFile().string().c_str(), EncodeBase58(&rand_pwd[0],&rand_pwd[0]+32).c_str()), - _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); - StartShutdown(); + _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); + MarkShutdownFailure(); + StartShutdown(); return; } @@ -732,23 +787,23 @@ void ThreadRPCServer2(void* parg) "or Tor.\n"); } - // Bind the loopback interface(s) unless the operator explicitly opened the - // RPC port to other hosts with -rpcallowip. - const bool loopbackOnly = !mapArgs.count("-rpcallowip"); - const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort()); - - std::string strBindError; - std::vector vListen = BindRPCSockets(nPort, loopbackOnly, strBindError); + const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort()); + const std::string rpcBind = GetArg( + std::string_view{"-rpcbind"}, std::string_view{""}); + + std::string strBindError; + std::vector vListen = BindRPCSockets(nPort, rpcBind, strBindError); if (vListen.empty()) { uiInterface.ThreadSafeMessageBox( - strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"), - nPort, strBindError.c_str()), - _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); - StartShutdown(); + strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"), + nPort, strBindError.c_str()), + _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); + MarkShutdownFailure(); + StartShutdown(); return; } - printf("RPC server listening on port %d (%s)\n", nPort, - loopbackOnly ? "loopback only" : "all interfaces"); + printf("RPC server listening on port %d (%s)\n", nPort, + rpcBind.empty() ? "loopback only" : rpcBind.c_str()); // Accept loop. select() with a short timeout keeps the listener responsive // to fShutdown. Each accepted connection is handed to its own handler thread @@ -780,10 +835,19 @@ void ThreadRPCServer2(void* parg) struct sockaddr_storage ss; socklen_t len = sizeof(ss); SOCKET hConn = accept(s, (struct sockaddr*)&ss, &len); - if (hConn == INVALID_SOCKET) { - printf("RPC accept() failed\n"); - continue; - } + if (hConn == INVALID_SOCKET) { + printf("RPC accept() failed\n"); + continue; + } + + int64_t timeoutSeconds = GetArg("-rpcservertimeout", 30); + if (timeoutSeconds < 1) + timeoutSeconds = 1; + if (timeoutSeconds > 600) + timeoutSeconds = 600; + if (!SetRPCSocketTimeouts(hConn, static_cast(timeoutSeconds))) { + printf("RPC warning: failed to set connection timeouts\n"); + } const std::string strPeer = SockaddrToString((struct sockaddr*)&ss, len); @@ -979,7 +1043,13 @@ void ThreadRPCServer3(void* parg) map mapHeaders; string strRequest; - ReadHTTP(conn->stream(), mapHeaders, strRequest); + const int requestStatus = ReadHTTP(conn->stream(), mapHeaders, strRequest); + if (requestStatus != 0) { + conn->stream() << HTTPReply(HTTP_BAD_REQUEST, + "{\"error\":\"Malformed HTTP request\"}", + false) << std::flush; + break; + } // Handle REST API requests string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST"; @@ -1097,9 +1167,12 @@ void ThreadRPCServer3(void* parg) } } -json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array ¶ms) const -{ - // Find method +json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array ¶ms) const +{ + if (!RPCMethodAllowed(strMethod)) + throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found"); + + // Find method const CRPCCommand *pcmd = tableRPC[strMethod]; if (!pcmd) throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found"); diff --git a/src/trianglesrpc.h b/src/trianglesrpc.h index 8488adf..9dcf085 100644 --- a/src/trianglesrpc.h +++ b/src/trianglesrpc.h @@ -228,9 +228,6 @@ extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, boo extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value publishcheckpoint(const json_spirit::Array& params, bool fHelp); -extern json_spirit::Value settrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp); -extern json_spirit::Value gettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp); -extern json_spirit::Value unsettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp); @@ -259,4 +256,3 @@ extern json_spirit::Value smsgbroadcast(const json_spirit::Array& params, bool f #endif - diff --git a/src/util.cpp b/src/util.cpp index 0cbed60..ee48cb1 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -33,6 +33,8 @@ #ifndef WIN32 #include +#include +#include #endif #include "util.h" @@ -174,9 +176,10 @@ uint64_t GetRand(uint64_t nMax) // to give every possible output value an equal possibility uint64_t nRange = (std::numeric_limits::max() / nMax) * nMax; uint64_t nRand = 0; - do - RAND_bytes((unsigned char*)&nRand, sizeof(nRand)); - while (nRand >= nRange); + do { + if (RAND_bytes(reinterpret_cast(&nRand), sizeof(nRand)) != 1) + throw std::runtime_error("OpenSSL CSPRNG failure in GetRand"); + } while (nRand >= nRange); return (nRand % nMax); } @@ -188,7 +191,8 @@ int GetRandInt(int nMax) uint256 GetRandHash() { uint256 hash; - RAND_bytes((unsigned char*)&hash, sizeof(hash)); + if (RAND_bytes(reinterpret_cast(&hash), sizeof(hash)) != 1) + throw std::runtime_error("OpenSSL CSPRNG failure in GetRandHash"); return hash; } @@ -616,7 +620,9 @@ bool SoftSetBoolArg(std::string_view strArg, bool fValue) bool WildcardMatch(std::string_view str, std::string_view mask) { - return WildcardMatch(std::string(str), std::string(mask)); + const std::string strOwned(str); + const std::string maskOwned(mask); + return WildcardMatch(strOwned.c_str(), maskOwned.c_str()); } @@ -954,31 +960,27 @@ string DecodeBase32(const string& str) bool WildcardMatch(const char* psz, const char* mask) { - while (true) - { - switch (*mask) - { - case '\0': - return (*psz == '\0'); - case '*': - return WildcardMatch(psz, mask+1) || (*psz && WildcardMatch(psz+1, mask)); - case '?': - if (*psz == '\0') - return false; - break; - default: - if (*psz != *mask) - return false; - break; - } - psz++; - mask++; - } -} + const char* star = nullptr; + const char* retry = nullptr; -bool WildcardMatch(const string& str, const string& mask) -{ - return WildcardMatch(str.c_str(), mask.c_str()); + while (*psz != '\0') { + if (*mask == '?' || *mask == *psz) { + ++psz; + ++mask; + } else if (*mask == '*') { + star = mask++; + retry = psz; + } else if (star != nullptr) { + mask = star + 1; + psz = ++retry; + } else { + return false; + } + } + + while (*mask == '*') + ++mask; + return *mask == '\0'; } @@ -1145,7 +1147,20 @@ void ReadConfigFile(map& mapSettingsRet, // - Section headers ([section]) // If any of those become needed, the actual conf syntax in // contrib/triangles.conf.example should be extended first. - std::ifstream streamConfig(GetConfigFile()); + const std::filesystem::path configPath = GetConfigFile(); +#ifndef WIN32 + struct stat configStat; + if (::lstat(configPath.string().c_str(), &configStat) == 0) { + if (!S_ISREG(configStat.st_mode) || configStat.st_uid != geteuid() || + (configStat.st_mode & (S_IRWXG | S_IRWXO)) != 0) { + throw std::runtime_error( + "Refusing to read insecure configuration file " + configPath.string() + + "; it must be a regular file owned by the current user with no group or other access"); + } + } +#endif + + std::ifstream streamConfig(configPath); if (!streamConfig.good()) return; // No triangles.conf file is OK diff --git a/src/wallet.cpp b/src/wallet.cpp index e9c468f..3e7becc 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -13,12 +13,15 @@ #include "kernel.h" #include "coincontrol.h" #include "addressindex.h" +#include "init.h" #include "util.h" #include #include #include +#include #include #include +#include using namespace std; extern unsigned int nStakeMaxAge; @@ -26,6 +29,29 @@ extern unsigned int nStakeMaxAge; unsigned int nStakeSplitAge = 1 * 3 * 60 * 60; int64_t nStakeCombineThreshold = 20 * COIN; +namespace { +void CleanseWalletString(std::string& value) +{ + if (!value.empty()) + OPENSSL_cleanse(value.data(), value.size()); + value.clear(); +} + +bool RewriteWalletDatabase(const std::string& walletFile, const char* skip = nullptr) +{ + if (ResolveWalletDbKind() != WalletDbKind::SQLite) + return CDB::Rewrite(walletFile, skip); + + try { + CWalletDB walletdb(walletFile); + return walletdb.RewriteDatabase(skip); + } catch (const std::exception& e) { + printf("RewriteWalletDatabase: %s\n", e.what()); + return false; + } +} +} // namespace + ////////////////////////////////////////////////////////////////////////////// // // mapWallet @@ -132,32 +158,80 @@ CPubKey CWallet::GenerateNewKey() RandAddSeedPerfmon(); CKey key; bool fUsedHD = false; - if (fHDEnabled && !hdMnemonic.empty()) { - if (DeriveHDKey(nHDChainIndex, key)) { fUsedHD = true; fCompressed = true; } + if (fHDEnabled) { + if (hdMnemonic.empty()) + throw std::runtime_error("CWallet::GenerateNewKey() : HD seed is unavailable while wallet is locked"); + if (!DeriveHDKey(nHDChainIndex, key)) + throw std::runtime_error("CWallet::GenerateNewKey() : HD key derivation failed"); + fUsedHD = true; + fCompressed = true; } if (!fUsedHD) key.MakeNewKey(fCompressed); // Compressed public keys were introduced in version 0.6.0 - if (fCompressed) - SetMinVersion(WalletFeature::ComprPubKey); + if (fCompressed && !SetMinVersion(WalletFeature::ComprPubKey)) + throw std::runtime_error("CWallet::GenerateNewKey() : wallet version update failed"); CPubKey pubkey = key.GetPubKey(); - // Create new metadata - int64_t nCreationTime = GetTime(); - mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(nCreationTime); + const int64_t nCreationTime = GetTime(); + const CKeyMetadata metadata(nCreationTime); + + if (fUsedHD && fFileBacked) { + const int64_t nextHDChainIndex = nHDChainIndex + 1; + std::vector cryptedSecret; + if (IsCrypted()) { + if (IsLocked() || vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE) + throw std::runtime_error("CWallet::GenerateNewKey() : encrypted HD wallet is locked"); + bool compressed = false; + if (!EncryptSecret(vMasterKey, key.GetSecret(compressed), + pubkey.GetHash(), cryptedSecret)) + throw std::runtime_error("CWallet::GenerateNewKey() : key encryption failed"); + } + + CWalletDB walletdb(strWalletFile); + if (!walletdb.TxnBegin()) + throw std::runtime_error("CWallet::GenerateNewKey() : database transaction failed"); + bool wrote = IsCrypted() + ? walletdb.WriteCryptedKey(pubkey, cryptedSecret, metadata) + : walletdb.WriteKey(pubkey, key.GetPrivKey(), metadata); + wrote = wrote && walletdb.WriteHDChain(nextHDChainIndex); + if (!wrote) { + walletdb.TxnAbort(); + throw std::runtime_error("CWallet::GenerateNewKey() : atomic HD key write failed"); + } + if (!walletdb.TxnCommit()) { + walletdb.TxnAbort(); + throw std::runtime_error("CWallet::GenerateNewKey() : atomic HD key commit failed"); + } + + const bool added = IsCrypted() + ? CCryptoKeyStore::AddCryptedKey(pubkey, cryptedSecret) + : CBasicKeyStore::AddKey(key); + if (!added) { + printf("GenerateNewKey: HD key persisted but could not be added in memory; shutting down\n"); + MarkShutdownFailure(); + StartShutdown(); + throw std::runtime_error("CWallet::GenerateNewKey() : in-memory HD key commit failed"); + } + mapKeyMetadata[pubkey.GetID()] = metadata; + if (!nTimeFirstKey || nCreationTime < nTimeFirstKey) + nTimeFirstKey = nCreationTime; + nHDChainIndex = nextHDChainIndex; + return pubkey; + } + + mapKeyMetadata[pubkey.GetID()] = metadata; + if (!AddKey(key)) { + mapKeyMetadata.erase(pubkey.GetID()); + throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed"); + } if (!nTimeFirstKey || nCreationTime < nTimeFirstKey) nTimeFirstKey = nCreationTime; - - if (!AddKey(key)) - throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed"); - if (fUsedHD) { - nHDChainIndex++; - if (fFileBacked) - CWalletDB(strWalletFile).WriteHDChain(nHDChainIndex); - } - return key.GetPubKey(); + if (fUsedHD) + ++nHDChainIndex; + return pubkey; } bool CWallet::AddKey(const CKey& key) @@ -215,22 +289,17 @@ bool fWalletUnlockStakingOnly = false; bool CWallet::Lock() { + if (IsCrypted()) { + CleanseWalletString(hdMnemonic); + CleanseWalletString(hdPassphrase); + } + if (IsLocked()) return true; if (fDebug) printf("Locking wallet.\n"); - if (IsCrypted()) { - hdMnemonic.clear(); // keep only the encrypted copies while locked - hdPassphrase.clear(); - } - - { - LOCK(cs_wallet); - CWalletDB wdb(strWalletFile); - - } return LockKeyStore(); }; @@ -242,38 +311,66 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase) CCrypter crypter; CKeyingMaterial vMasterKey; + bool unlocked = false; { LOCK(cs_wallet); for (const MasterKeyMap::value_type& pMasterKey : mapMasterKeys) { if(!crypter.SetKeyFromPassphrase(strWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) - return false; + continue; if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey)) - return false; + continue; if (CCryptoKeyStore::Unlock(vMasterKey)) { - if (fHDEnabled && hdMnemonic.empty() && !vchCryptedHDMnemonic.empty()) { + if (fHDEnabled) { + if (vchCryptedHDMnemonic.empty()) { + LockKeyStore(); + return false; + } CSecret sec; - if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec)) - hdMnemonic.assign(sec.begin(), sec.end()); + if (!DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec)) { + LockKeyStore(); + return false; + } + std::string mnemonic(sec.begin(), sec.end()); + if (!hd::CheckMnemonic(mnemonic)) { + OPENSSL_cleanse(mnemonic.data(), mnemonic.size()); + LockKeyStore(); + return false; + } + + std::string passphrase; + if (!vchCryptedHDPassphrase.empty()) { + CSecret psec; + if (!DecryptSecret(vMasterKey, vchCryptedHDPassphrase, + hdPassphraseIV, psec)) { + OPENSSL_cleanse(mnemonic.data(), mnemonic.size()); + LockKeyStore(); + return false; + } + passphrase.assign(psec.begin(), psec.end()); + } + + CleanseWalletString(hdMnemonic); + CleanseWalletString(hdPassphrase); + hdMnemonic = std::move(mnemonic); + hdPassphrase = std::move(passphrase); } - if (fHDEnabled && hdPassphrase.empty() && !vchCryptedHDPassphrase.empty()) { - CSecret psec; - if (DecryptSecret(vMasterKey, vchCryptedHDPassphrase, hdPassphraseIV, psec)) - hdPassphrase.assign(psec.begin(), psec.end()); - } - return true; + unlocked = true; + break; } } - - SecureMsgWalletUnlocked(); - return true; } - return false; + if (!unlocked) + return false; + + SecureMsgWalletUnlocked(); + return true; } bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase) { - bool fWasLocked = IsLocked(); + const bool fWasLocked = IsLocked(); + bool changed = false; { LOCK(cs_wallet); @@ -284,39 +381,73 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, for (MasterKeyMap::value_type& pMasterKey : mapMasterKeys) { if(!crypter.SetKeyFromPassphrase(strOldWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) - return false; + continue; if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey)) - return false; + continue; if (CCryptoKeyStore::Unlock(vMasterKey)) { + CMasterKey updatedMasterKey = pMasterKey.second; int64_t nStartTime = GetTimeMillis(); - crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod); + if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase, + updatedMasterKey.vchSalt, + updatedMasterKey.nDeriveIterations, + updatedMasterKey.nDerivationMethod)) + break; int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime); - pMasterKey.second.nDeriveIterations = pMasterKey.second.nDeriveIterations * (100 / ((double)nElapsed)); + updatedMasterKey.nDeriveIterations = + updatedMasterKey.nDeriveIterations * (100 / ((double)nElapsed)); nStartTime = GetTimeMillis(); - crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod); + if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase, + updatedMasterKey.vchSalt, + updatedMasterKey.nDeriveIterations, + updatedMasterKey.nDerivationMethod)) + break; nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime); - pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + pMasterKey.second.nDeriveIterations * 100 / ((double)nElapsed)) / 2; + updatedMasterKey.nDeriveIterations = + (updatedMasterKey.nDeriveIterations + + updatedMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2; - if (pMasterKey.second.nDeriveIterations < 25000) - pMasterKey.second.nDeriveIterations = 25000; + if (updatedMasterKey.nDeriveIterations < 25000) + updatedMasterKey.nDeriveIterations = 25000; - printf("Wallet passphrase changed to an nDeriveIterations of %i\n", pMasterKey.second.nDeriveIterations); + printf("Wallet passphrase changed to an nDeriveIterations of %i\n", + updatedMasterKey.nDeriveIterations); - if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) - return false; - if (!crypter.Encrypt(vMasterKey, pMasterKey.second.vchCryptedKey)) - return false; - CWalletDB(strWalletFile).WriteMasterKey(pMasterKey.first, pMasterKey.second); - if (fWasLocked) - Lock(); - return true; + if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase, + updatedMasterKey.vchSalt, + updatedMasterKey.nDeriveIterations, + updatedMasterKey.nDerivationMethod)) + break; + if (!crypter.Encrypt(vMasterKey, updatedMasterKey.vchCryptedKey)) + break; + + bool persisted = true; + if (fFileBacked) { + try { + persisted = CWalletDB(strWalletFile).WriteMasterKey( + pMasterKey.first, updatedMasterKey); + } catch (const std::exception& e) { + printf("ChangeWalletPassphrase: %s\n", e.what()); + persisted = false; + } + } + if (!persisted) + break; + + pMasterKey.second = std::move(updatedMasterKey); + changed = true; + break; } } + LockKeyStore(); } - return false; + if (!changed) + return false; + if (!fWasLocked && !Unlock(strNewWalletPassphrase)) + return false; + return true; } void CWallet::SetBestChain(const CBlockLocator& loc) @@ -346,27 +477,44 @@ bool CWallet::SetMinVersion(WalletFeature nVersion, CWalletDB* pwalletdbIn, bool if (fExplicit && static_cast(nVersion) > nWalletMaxVersion) nVersion = WalletFeature::Latest; - nWalletVersion = static_cast(nVersion); - - if (static_cast(nVersion) > nWalletMaxVersion) - nWalletMaxVersion = static_cast(nVersion); + const int newWalletVersion = static_cast(nVersion); if (fFileBacked) { - CWalletDB* pwalletdb = pwalletdbIn ? pwalletdbIn : new CWalletDB(strWalletFile); - if (nWalletVersion >= 40000) + std::unique_ptr ownedWalletDB; + CWalletDB* pwalletdb = pwalletdbIn; + if (!pwalletdb) { + ownedWalletDB = std::make_unique(strWalletFile); + pwalletdb = ownedWalletDB.get(); + if (!pwalletdb->TxnBegin()) + return false; + } + + bool wrote = true; + if (newWalletVersion >= 40000) { // Versions prior to 0.4.0 did not support the "minversion" record. // Use a CCorruptAddress to make them crash instead. CCorruptAddress corruptAddress; - pwalletdb->WriteSetting("addrIncoming", corruptAddress); + wrote = pwalletdb->WriteSetting("addrIncoming", corruptAddress); + } + if (wrote && newWalletVersion > 40000) + wrote = pwalletdb->WriteMinVersion(newWalletVersion); + + if (!wrote) { + if (ownedWalletDB) + pwalletdb->TxnAbort(); + return false; + } + if (ownedWalletDB && !pwalletdb->TxnCommit()) { + pwalletdb->TxnAbort(); + return false; } - if (nWalletVersion > 40000) - pwalletdb->WriteMinVersion(nWalletVersion); - if (!pwalletdbIn) - delete pwalletdb; } + nWalletVersion = newWalletVersion; + if (newWalletVersion > nWalletMaxVersion) + nWalletMaxVersion = newWalletVersion; return true; } @@ -385,27 +533,38 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) { if (IsCrypted()) return false; + if (fHDEnabled && hdMnemonic.empty()) + return false; + if (nMasterKeyMaxID == std::numeric_limits::max()) + return false; CKeyingMaterial vMasterKey; RandAddSeedPerfmon(); vMasterKey.resize(WALLET_CRYPTO_KEY_SIZE); - RAND_bytes(&vMasterKey[0], WALLET_CRYPTO_KEY_SIZE); + if (RAND_bytes(&vMasterKey[0], WALLET_CRYPTO_KEY_SIZE) != 1) + return false; CMasterKey kMasterKey(nDerivationMethodIndex); RandAddSeedPerfmon(); kMasterKey.vchSalt.resize(WALLET_CRYPTO_SALT_SIZE); - RAND_bytes(&kMasterKey.vchSalt[0], WALLET_CRYPTO_SALT_SIZE); + if (RAND_bytes(&kMasterKey.vchSalt[0], WALLET_CRYPTO_SALT_SIZE) != 1) + return false; CCrypter crypter; int64_t nStartTime = GetTimeMillis(); - crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, 25000, kMasterKey.nDerivationMethod); + if (!crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, + 25000, kMasterKey.nDerivationMethod)) + return false; int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime); kMasterKey.nDeriveIterations = 2500000 / ((double)nElapsed); nStartTime = GetTimeMillis(); - crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, kMasterKey.nDeriveIterations, kMasterKey.nDerivationMethod); + if (!crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + kMasterKey.nDerivationMethod)) + return false; nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime); kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2; @@ -421,51 +580,109 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) { LOCK(cs_wallet); - mapMasterKeys[++nMasterKeyMaxID] = kMasterKey; + CryptedKeyMap cryptedKeys; + if (!PrepareKeyEncryption(vMasterKey, cryptedKeys)) + return false; + + uint256 newMnemonicIV; + uint256 newPassphraseIV; + std::vector newCryptedMnemonic; + std::vector newCryptedPassphrase; + if (fHDEnabled) { + CSecret mnemonicSecret(hdMnemonic.begin(), hdMnemonic.end()); + newMnemonicIV = GetRandHash(); + if (!EncryptSecret(vMasterKey, mnemonicSecret, newMnemonicIV, + newCryptedMnemonic)) + return false; + if (!hdPassphrase.empty()) { + CSecret passphraseSecret(hdPassphrase.begin(), hdPassphrase.end()); + newPassphraseIV = GetRandHash(); + if (!EncryptSecret(vMasterKey, passphraseSecret, newPassphraseIV, + newCryptedPassphrase)) + return false; + } + } + + const unsigned int newMasterKeyID = nMasterKeyMaxID + 1; + const int oldWalletVersion = nWalletVersion; + const int oldWalletMaxVersion = nWalletMaxVersion; if (fFileBacked) { - std::unique_ptr dbEnc(new CWalletDB(strWalletFile)); + std::unique_ptr dbEnc = std::make_unique(strWalletFile); if (!dbEnc->TxnBegin()) return false; - dbEnc->WriteMasterKey(nMasterKeyMaxID, kMasterKey); + bool wrote = dbEnc->WriteMasterKey(newMasterKeyID, kMasterKey); + for (const auto& item : cryptedKeys) { + if (!wrote) + break; + CKeyMetadata metadata; + auto metadataIt = mapKeyMetadata.find(item.first); + if (metadataIt != mapKeyMetadata.end()) + metadata = metadataIt->second; + wrote = dbEnc->WriteCryptedKey(item.second.first, + item.second.second, metadata); + } + if (wrote && fHDEnabled) { + wrote = dbEnc->WriteHDCryptedMnemonic(newMnemonicIV, + newCryptedMnemonic); + if (wrote) { + wrote = hdPassphrase.empty() + ? dbEnc->EraseHDPassphrase() + : dbEnc->WriteHDCryptedPassphrase(newPassphraseIV, + newCryptedPassphrase); + } + } + if (wrote) + wrote = SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true); - if (!EncryptKeys(vMasterKey)) - { + if (!wrote) { dbEnc->TxnAbort(); + nWalletVersion = oldWalletVersion; + nWalletMaxVersion = oldWalletMaxVersion; return false; } - - if (fHDEnabled && !hdMnemonic.empty()) { - CSecret sec(hdMnemonic.begin(), hdMnemonic.end()); - uint256 iv = GetRandHash(); - std::vector cipher; - if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { dbEnc->TxnAbort(); return false; } - hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; - dbEnc->WriteHDCryptedMnemonic(iv, cipher); - } - if (fHDEnabled && !hdPassphrase.empty()) { - CSecret psec(hdPassphrase.begin(), hdPassphrase.end()); - uint256 piv = GetRandHash(); - std::vector pcipher; - if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { dbEnc->TxnAbort(); return false; } - hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher; - dbEnc->WriteHDCryptedPassphrase(piv, pcipher); - } - - SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true); - - if (!dbEnc->TxnCommit()) + if (!dbEnc->TxnCommit()) { + dbEnc->TxnAbort(); + nWalletVersion = oldWalletVersion; + nWalletMaxVersion = oldWalletMaxVersion; return false; + } + } else if (!SetMinVersion(WalletFeature::WalletCrypt, nullptr, true)) { + return false; + } + + if (!CommitKeyEncryption(std::move(cryptedKeys))) { + printf("EncryptWallet: disk was updated but in-memory key encryption could not be committed; shutting down\n"); + MarkShutdownFailure(); + StartShutdown(); + return false; + } + nMasterKeyMaxID = newMasterKeyID; + mapMasterKeys[newMasterKeyID] = kMasterKey; + if (fHDEnabled) { + hdMnemonicIV = newMnemonicIV; + hdPassphraseIV = newPassphraseIV; + vchCryptedHDMnemonic = std::move(newCryptedMnemonic); + vchCryptedHDPassphrase = std::move(newCryptedPassphrase); } Lock(); - Unlock(strWalletPassphrase); - NewKeyPool(); + if (!Unlock(strWalletPassphrase)) { + printf("EncryptWallet: encrypted wallet could not be verified; shutting down\n"); + MarkShutdownFailure(); + StartShutdown(); + return false; + } + if (!NewKeyPool()) + printf("EncryptWallet: wallet encrypted, but keypool regeneration failed\n"); Lock(); - // Need to completely rewrite the wallet file; if we don't, bdb might keep - // bits of the unencrypted private key in slack space in the database file. - CDB::Rewrite(strWalletFile); + if (fFileBacked && !RewriteWalletDatabase(strWalletFile)) { + printf("EncryptWallet: secure wallet database rewrite failed; shutting down\n"); + MarkShutdownFailure(); + StartShutdown(); + return false; + } } NotifyStatusChanged(this); @@ -793,7 +1010,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx) // the inputs (vfSpent was tracked on the wtx) and resolves the conflict. bool fErased = EraseFromWallet(hashTx); - LogPrintf("CWallet::AbandonTransaction: %s abandoned (%u descendant(s) noted)\n", + LogPrintf("CWallet::AbandonTransaction: %s abandoned (%" PRIszu " descendant(s) noted)\n", hashTx.ToString().c_str(), sDescendants.size()); return fErased; } @@ -2115,10 +2332,12 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int break; // unable to find corresponding public key } - if (key.GetPubKey() != vchPubKey) - { - if (fDebug && GetBoolArg("-printcoinstake")) - printf("CreateCoinStake : invalid key for kernel type=%d\n", whichType); + if (key.GetPubKey() != vchPubKey) + { + if (fDebug && GetBoolArg("-printcoinstake")) { + printf("CreateCoinStake : invalid key for kernel type=%d\n", + static_cast(whichType)); + } break; // keys mismatch } @@ -2134,7 +2353,7 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int if (GetWeight(block.GetBlockTime(), (int64_t)txNew.nTime) < nStakeSplitAge) txNew.vout.push_back(CTxOut(0, scriptPubKeyOut)); //split stake if (fDebug && GetBoolArg("-printcoinstake")) - printf("CreateCoinStake : added kernel type=%d\n", whichType); + printf("CreateCoinStake : added kernel type=%d\n", static_cast(whichType)); fKernelFound = true; break; } @@ -2351,7 +2570,7 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet) DBErrors nLoadWalletRet = CWalletDB(strWalletFile,"cr+").LoadWallet(this); if (nLoadWalletRet == DB_NEED_REWRITE) { - if (CDB::Rewrite(strWalletFile, "\x04pool")) + if (RewriteWalletDatabase(strWalletFile, "\x04pool")) { setKeyPool.clear(); // Note: can't top-up keypool here, because wallet is locked. @@ -2478,9 +2697,13 @@ bool CWallet::NewKeyPool() { { LOCK(cs_wallet); - CWalletDB walletdb(strWalletFile); - for (int64_t nIndex : setKeyPool) - walletdb.ErasePool(nIndex); + std::unique_ptr walletdb; + if (fFileBacked) + walletdb = std::make_unique(strWalletFile); + for (int64_t nIndex : setKeyPool) { + if (walletdb && !walletdb->ErasePool(nIndex)) + return false; + } setKeyPool.clear(); if (IsLocked()) @@ -2490,7 +2713,9 @@ bool CWallet::NewKeyPool() for (int i = 0; i < nKeys; i++) { int64_t nIndex = i+1; - walletdb.WritePool(nIndex, CKeyPool(GenerateNewKey())); + CKeyPool keypool(GenerateNewKey()); + if (walletdb && !walletdb->WritePool(nIndex, keypool)) + return false; setKeyPool.insert(nIndex); } printf("CWallet::NewKeyPool wrote %"PRId64" new keys\n", nKeys); @@ -2506,7 +2731,9 @@ bool CWallet::TopUpKeyPool(unsigned int nSize) if (IsLocked()) return false; - CWalletDB walletdb(strWalletFile); + std::unique_ptr walletdb; + if (fFileBacked) + walletdb = std::make_unique(strWalletFile); // Top up key pool unsigned int nTargetSize; @@ -2520,7 +2747,8 @@ bool CWallet::TopUpKeyPool(unsigned int nSize) int64_t nEnd = 1; if (!setKeyPool.empty()) nEnd = *(--setKeyPool.end()) + 1; - if (!walletdb.WritePool(nEnd, CKeyPool(GenerateNewKey()))) + CKeyPool keypool(GenerateNewKey()); + if (walletdb && !walletdb->WritePool(nEnd, keypool)) throw runtime_error("TopUpKeyPool() : writing generated key failed"); setKeyPool.insert(nEnd); printf("keypool added key %"PRId64", size=%"PRIszu"\n", nEnd, setKeyPool.size()); @@ -2946,16 +3174,18 @@ void CWallet::GetKeyBirthTimes(std::map &mapKeyBirth) const { // ---- HD wallet (BIP39/BIP32) implementation ---- bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const { - if (hdMnemonic.empty()) + if (hdMnemonic.empty() || index < 0 || + static_cast(index) > std::numeric_limits::max()) return false; // If a BIP39 passphrase ("25th word") was set with the seed, it MUST be // part of every derivation — otherwise restored wallets derive different // addresses than the originals. Empty string = no passphrase (legacy). unsigned char priv[32]; - if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0, (uint32_t)index, priv)) + if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0, + static_cast(index), priv)) return false; CSecret secret(priv, priv + 32); - memset(priv, 0, sizeof(priv)); + OPENSSL_cleanse(priv, sizeof(priv)); keyOut.SetSecret(secret, true); // HD keys are compressed return true; } @@ -2972,6 +3202,7 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph { LOCK(cs_wallet); if (IsLocked()) { strError = "Wallet is locked; unlock it before setting an HD seed."; return false; } + if (fHDEnabled) { strError = "Wallet already has an HD seed; refusing to replace it."; return false; } std::string m = mnemonicIn; if (m.empty()) { @@ -2983,45 +3214,86 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph unsigned char priv[32]; if (!hd::DeriveTriangles(m, passphrase, 0, 0, 0, priv)) { strError = "Key derivation failed."; return false; } - memset(priv, 0, sizeof(priv)); + OPENSSL_cleanse(priv, sizeof(priv)); - hdMnemonic = m; - hdPassphrase = passphrase; - fHDEnabled = true; - nHDChainIndex = 0; + uint256 newMnemonicIV; + uint256 newPassphraseIV; + std::vector newCryptedMnemonic; + std::vector newCryptedPassphrase; + + if (IsCrypted()) { + if (vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE) { + strError = "Wallet master key is unavailable."; + return false; + } + CSecret sec(m.begin(), m.end()); + newMnemonicIV = GetRandHash(); + if (!EncryptSecret(vMasterKey, sec, newMnemonicIV, newCryptedMnemonic)) { + strError = "Failed to encrypt seed."; + return false; + } + if (!passphrase.empty()) { + CSecret psec(passphrase.begin(), passphrase.end()); + newPassphraseIV = GetRandHash(); + if (!EncryptSecret(vMasterKey, psec, newPassphraseIV, + newCryptedPassphrase)) { + strError = "Failed to encrypt passphrase."; + return false; + } + } + } if (fFileBacked) { CWalletDB wdb(strWalletFile); - if (IsCrypted()) { - CSecret sec(m.begin(), m.end()); - uint256 iv = GetRandHash(); - std::vector cipher; - if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; } - hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; - wdb.WriteHDCryptedMnemonic(iv, cipher); - if (!passphrase.empty()) { - CSecret psec(passphrase.begin(), passphrase.end()); - uint256 piv = GetRandHash(); - std::vector pcipher; - if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { strError = "Failed to encrypt passphrase."; return false; } - hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher; - wdb.WriteHDCryptedPassphrase(piv, pcipher); - } else { - vchCryptedHDPassphrase.clear(); - wdb.EraseHDPassphrase(); // re-seed without passphrase: drop any old record - } - } else { - wdb.WriteHDMnemonic(m); - if (!passphrase.empty()) - wdb.WriteHDPassphrase(passphrase); - else - wdb.EraseHDPassphrase(); + if (!wdb.TxnBegin()) { + strError = "Failed to start wallet database transaction."; + return false; + } + + bool wrote = false; + if (IsCrypted()) { + wrote = wdb.WriteHDCryptedMnemonic(newMnemonicIV, newCryptedMnemonic) && + (passphrase.empty() + ? wdb.EraseHDPassphrase() + : wdb.WriteHDCryptedPassphrase(newPassphraseIV, + newCryptedPassphrase)); + } else { + wrote = wdb.WriteHDMnemonic(m) && + (passphrase.empty() + ? wdb.EraseHDPassphrase() + : wdb.WriteHDPassphrase(passphrase)); + } + wrote = wrote && wdb.WriteHDChain(0); + + if (!wrote) { + wdb.TxnAbort(); + strError = "Failed to persist HD seed; wallet database transaction was rolled back."; + return false; + } + if (!wdb.TxnCommit()) { + wdb.TxnAbort(); + strError = "Failed to commit HD seed to the wallet database."; + return false; } - wdb.WriteHDChain(nHDChainIndex); } + + CleanseWalletString(hdMnemonic); + CleanseWalletString(hdPassphrase); + hdMnemonic = m; + hdPassphrase = passphrase; + hdMnemonicIV = newMnemonicIV; + hdPassphraseIV = newPassphraseIV; + vchCryptedHDMnemonic = std::move(newCryptedMnemonic); + vchCryptedHDPassphrase = std::move(newCryptedPassphrase); + fHDEnabled = true; + nHDChainIndex = 0; + // Replace any pre-existing (random) keypool with HD-derived keys so that // getnewaddress immediately hands out deterministic m/44'/2222'/0'/0/i keys. - NewKeyPool(); + if (!NewKeyPool()) { + strError = "HD seed was stored, but rebuilding the keypool failed."; + return false; + } mnemonicOut = m; return true; } diff --git a/src/wallet.h b/src/wallet.h index f5329dd..c654c5d 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -76,8 +76,6 @@ private: bool SelectCoinsSimple(int64_t nTargetValue, unsigned int nSpendTime, int nMinConf, std::set >& setCoinsRet, int64_t& nValueRet) const; bool SelectCoins(int64_t nTargetValue, unsigned int nSpendTime, std::set >& setCoinsRet, int64_t& nValueRet, const CCoinControl *coinControl=nullptr) const; - CWalletDB *pwalletdbEncryption; - // the current wallet version: clients below this version are not able to load the wallet int nWalletVersion; @@ -104,9 +102,9 @@ public: nWalletMaxVersion = static_cast(WalletFeature::Base); fFileBacked = false; nMasterKeyMaxID = 0; - pwalletdbEncryption = nullptr; fHDEnabled = false; nHDChainIndex = 0; + nTimeFirstKey = 0; nOrderPosNext = 0; nCachedStakeWeight = 0; nCachedStakeWeightTime = 0; diff --git a/src/walletdb-sqlite.cpp b/src/walletdb-sqlite.cpp index 4f68642..3e90257 100644 --- a/src/walletdb-sqlite.cpp +++ b/src/walletdb-sqlite.cpp @@ -71,6 +71,9 @@ bool SQLiteDatabase::Open(std::string& strError) // Durability + integrity pragmas. FULL fsync on commit — a wallet must not // lose a freshly-written key on power loss. if (!ExecOrError("PRAGMA synchronous = FULL;", strError)) return false; + if (!ExecOrError("PRAGMA journal_mode = DELETE;", strError)) return false; + if (!ExecOrError("PRAGMA secure_delete = ON;", strError)) return false; + if (!ExecOrError("PRAGMA temp_store = MEMORY;", strError)) return false; if (!ExecOrError("PRAGMA foreign_keys = ON;", strError)) return false; // Fail loudly instead of silently truncating an over-long blob. if (!ExecOrError("PRAGMA cell_size_check = ON;", strError)) return false; @@ -156,6 +159,20 @@ bool SQLiteDatabase::Backup(const std::string& strDest) const if (pDest) sqlite3_close(pDest); return false; } +#ifndef WIN32 + { + std::error_code ec; + fs::permissions(strDest, + fs::perms::owner_read | fs::perms::owner_write, + fs::perm_options::replace, ec); + if (ec) { + printf("SQLiteDatabase::Backup cannot restrict destination permissions: %s\n", + ec.message().c_str()); + sqlite3_close(pDest); + return false; + } + } +#endif sqlite3_backup* bk = sqlite3_backup_init(pDest, "main", m_db, "main"); bool ok = false; diff --git a/src/walletdb.cpp b/src/walletdb.cpp index 2a4b4cd..d7590b3 100644 --- a/src/walletdb.cpp +++ b/src/walletdb.cpp @@ -42,6 +42,20 @@ namespace fs = std::filesystem; static uint64_t nAccountingEntryNumber = 0; extern bool fWalletUnlockStakingOnly; +static bool RestrictWalletFilePermissions(const fs::path& path) +{ +#ifdef WIN32 + (void)path; + return true; +#else + std::error_code ec; + fs::permissions(path, + fs::perms::owner_read | fs::perms::owner_write, + fs::perm_options::replace, ec); + return !ec; +#endif +} + // // Auto-backup wallet before flush/rewrite operations. // Copies wallet.dat to wallet.dat.auto.bak if the backup is older than the wallet. @@ -61,10 +75,17 @@ bool AutoBackupWallet(const fs::path& walletPath) } if (fs::exists(backupPath)) { uintmax_t backupSize = fs::file_size(backupPath); - if (backupSize == walletSize) + if (backupSize == walletSize) { + if (!RestrictWalletFilePermissions(backupPath)) + return false; return true; + } } fs::copy_file(walletPath, backupPath, fs::copy_options::overwrite_existing); + if (!RestrictWalletFilePermissions(backupPath)) { + printf("AutoBackupWallet: could not restrict backup file permissions\n"); + return false; + } printf("AutoBackupWallet: backed up wallet.dat (%llu bytes) to wallet.dat.auto.bak\n", (unsigned long long)walletSize); return true; @@ -257,6 +278,10 @@ public: unsigned int nKeyMeta; bool fIsEncrypted; bool fAnyUnordered; + bool fHDPlainMnemonic; + bool fHDCryptedMnemonic; + bool fHDPlainPassphrase; + bool fHDCryptedPassphrase; int nFileVersion; std::vector vWalletUpgrade; @@ -264,6 +289,10 @@ public: nKeys = nCKeys = nKeyMeta = 0; fIsEncrypted = false; fAnyUnordered = false; + fHDPlainMnemonic = false; + fHDCryptedMnemonic = false; + fHDPlainPassphrase = false; + fHDCryptedPassphrase = false; nFileVersion = 0; } }; @@ -380,6 +409,7 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa return false; } pwallet->mapMasterKeys[nID] = kMasterKey; + wss.fIsEncrypted = true; if (pwallet->nMasterKeyMaxID < nID) pwallet->nMasterKeyMaxID = nID; } else if (strType == "ckey") { @@ -417,18 +447,22 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa } else if (strType == "hdmnemonic") { std::string m; ssValue >> m; + wss.fHDPlainMnemonic = true; pwallet->LoadHDMnemonic(m); } else if (strType == "hdcmnemonic") { std::pair> cm; ssValue >> cm; + wss.fHDCryptedMnemonic = true; pwallet->LoadCryptedHDMnemonic(cm.first, cm.second); } else if (strType == "hdpassphrase") { std::string p; ssValue >> p; + wss.fHDPlainPassphrase = true; pwallet->LoadHDPassphrase(p); } else if (strType == "hdcpassphrase") { std::pair> cp; ssValue >> cp; + wss.fHDCryptedPassphrase = true; pwallet->LoadCryptedHDPassphrase(cp.first, cp.second); } else if (strType == "hdchain") { int64_t n; @@ -513,6 +547,35 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) if (fNoncriticalErrors && result == DB_LOAD_OK) result = DB_NONCRITICAL_ERROR; + if (result != DB_LOAD_OK && result != DB_NONCRITICAL_ERROR) + return result; + + const bool conflictingMnemonicRecords = + wss.fHDPlainMnemonic && wss.fHDCryptedMnemonic; + const bool conflictingPassphraseRecords = + wss.fHDPlainPassphrase && wss.fHDCryptedPassphrase; + const bool missingMnemonic = + (wss.fHDPlainPassphrase || wss.fHDCryptedPassphrase) && + !(wss.fHDPlainMnemonic || wss.fHDCryptedMnemonic); + const bool mixedHDProtection = + (wss.fHDPlainMnemonic && wss.fHDCryptedPassphrase) || + (wss.fHDCryptedMnemonic && wss.fHDPlainPassphrase); + const bool plaintextInEncryptedWallet = + wss.fIsEncrypted && + (wss.nKeys != 0 || wss.fHDPlainMnemonic || wss.fHDPlainPassphrase); + const bool encryptedHDInPlainWallet = + !wss.fIsEncrypted && + (wss.fHDCryptedMnemonic || wss.fHDCryptedPassphrase); + const bool encryptedKeysWithoutMasterKey = + wss.nCKeys != 0 && pwallet->mapMasterKeys.empty(); + + if (conflictingMnemonicRecords || conflictingPassphraseRecords || + missingMnemonic || mixedHDProtection || plaintextInEncryptedWallet || + encryptedHDInPlainWallet || encryptedKeysWithoutMasterKey) { + printf("Error reading wallet database: inconsistent encryption or HD seed records\n"); + return DB_CORRUPT; + } + if (result != DB_LOAD_OK) return result; @@ -617,18 +680,55 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest) if (!wallet.fFileBacked) return false; - // For the SQLite backend, the database is a single file — copy directly - // (after a checkpoint flush to fold any -wal into the main file). + // SQLite's online backup API takes a consistent snapshot while the daemon + // is running; copying the live database file directly can produce a torn + // backup if a transaction commits during the copy. if (ResolveWalletDbKind() == WalletDbKind::SQLite) { fs::path pathSrc = GetDataDir() / wallet.strWalletFile; fs::path pathDest(strDest); if (fs::is_directory(pathDest)) pathDest /= wallet.strWalletFile; + std::error_code ec; - fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing, ec); + if (fs::exists(pathDest, ec) && fs::equivalent(pathSrc, pathDest, ec)) { + printf("refusing to back up wallet.dat onto itself\n"); + return false; + } + ec.clear(); + + const fs::path pathTemp = pathDest.string() + + strprintf(".tmp.%" PRId64, GetTimeMillis()); + try { + CWalletDB walletdb(wallet.strWalletFile); + if (!walletdb.BackupDatabase(pathTemp.string())) { + fs::remove(pathTemp, ec); + return false; + } + } catch (const std::exception& e) { + printf("error backing up wallet.dat to %s - %s\n", + pathDest.string().c_str(), e.what()); + fs::remove(pathTemp, ec); + return false; + } + if (!RestrictWalletFilePermissions(pathTemp)) { + printf("error restricting wallet backup permissions: %s\n", + pathDest.string().c_str()); + fs::remove(pathTemp, ec); + return false; + } + fs::rename(pathTemp, pathDest, ec); +#ifdef WIN32 if (ec) { - printf("error copying wallet.dat to %s - %s\n", + ec.clear(); + fs::remove(pathDest, ec); + ec.clear(); + fs::rename(pathTemp, pathDest, ec); + } +#endif + if (ec) { + printf("error finalizing wallet backup %s - %s\n", pathDest.string().c_str(), ec.message().c_str()); + fs::remove(pathTemp, ec); return false; } printf("copied wallet.dat to %s\n", pathDest.string().c_str()); @@ -653,6 +753,11 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest) try { fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing); + if (!RestrictWalletFilePermissions(pathDest)) { + printf("error restricting wallet backup permissions: %s\n", + pathDest.string().c_str()); + return false; + } printf("copied wallet.dat to %s\n", pathDest.string().c_str()); return true; } catch (const fs::filesystem_error& e) { @@ -665,4 +770,4 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest) MilliSleep(100); } return false; -} \ No newline at end of file +} diff --git a/src/walletdb.h b/src/walletdb.h index 07ccce4..c9be309 100644 --- a/src/walletdb.h +++ b/src/walletdb.h @@ -123,8 +123,10 @@ public: return false; if (fEraseUnencryptedKey) { - Erase(std::make_pair(std::string("key"), vchPubKey.Raw())); - Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw())); + if (!Erase(std::make_pair(std::string("key"), vchPubKey.Raw()))) + return false; + if (!Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw()))) + return false; } return true; } @@ -166,13 +168,13 @@ public: bool WriteHDMnemonic(const std::string& mnemonic) { nWalletDBUpdated++; - Erase(std::string("hdcmnemonic")); - return Write(std::string("hdmnemonic"), mnemonic); + const bool erased = Erase(std::string("hdcmnemonic")); + return erased && Write(std::string("hdmnemonic"), mnemonic); } bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector& cipher) { nWalletDBUpdated++; - Erase(std::string("hdmnemonic")); - return Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher)); + const bool erased = Erase(std::string("hdmnemonic")); + return erased && Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher)); } bool WriteHDChain(int64_t nIndex) { nWalletDBUpdated++; @@ -183,19 +185,19 @@ public: // means no passphrase (legacy wallets and the common case). bool WriteHDPassphrase(const std::string& passphrase) { nWalletDBUpdated++; - Erase(std::string("hdcpassphrase")); - return Write(std::string("hdpassphrase"), passphrase); + const bool erased = Erase(std::string("hdcpassphrase")); + return erased && Write(std::string("hdpassphrase"), passphrase); } bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector& cipher) { nWalletDBUpdated++; - Erase(std::string("hdpassphrase")); - return Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher)); + const bool erased = Erase(std::string("hdpassphrase")); + return erased && Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher)); } bool EraseHDPassphrase() { nWalletDBUpdated++; - Erase(std::string("hdpassphrase")); - Erase(std::string("hdcpassphrase")); - return true; + const bool erasedPlain = Erase(std::string("hdpassphrase")); + const bool erasedCrypted = Erase(std::string("hdcpassphrase")); + return erasedPlain && erasedCrypted; } bool ReadPool(int64_t nPool, CKeyPool& keypool) @@ -251,6 +253,16 @@ public: return Read(std::string("version"), nVersion); } + bool RewriteDatabase(const char* pszSkip = nullptr) + { + return m_database && m_database->Rewrite(pszSkip); + } + + bool BackupDatabase(const std::string& destination) const + { + return m_database && m_database->Backup(destination); + } + bool ReadAccount(const std::string& strAccount, CAccount& account); bool WriteAccount(const std::string& strAccount, const CAccount& account); private: