diff --git a/depends/packages/libcurl.mk b/depends/packages/libcurl.mk index 6376e37653..2a3e5ddce6 100644 --- a/depends/packages/libcurl.mk +++ b/depends/packages/libcurl.mk @@ -15,6 +15,9 @@ define $(package)_set_vars $(package)_config_opts += --disable-tftp --without-brotli --without-zstd --without-libidn2 $(package)_config_opts += --without-libpsl --without-nghttp2 --disable-dependency-tracking $(package)_config_opts_linux=--with-pic + # -D_GNU_SOURCE exposes POSIX functions (fileno, fdopen) that libcurl's + # fopen.c needs but are hidden by the depends system's strict -std=c11. + $(package)_cppflags_linux=-D_GNU_SOURCE $(package)_config_env_linux=LIBS="-ldl -lpthread" $(package)_config_opts_mingw32=--with-pic $(package)_config_env_mingw32=LIBS="-lws2_32 -lcrypt32" diff --git a/depends/packages/openssl.mk b/depends/packages/openssl.mk index 1bbcb671cf..5780089a92 100644 --- a/depends/packages/openssl.mk +++ b/depends/packages/openssl.mk @@ -5,15 +5,31 @@ $(package)_file_name=$(package)-$($(package)_version).tar.gz $(package)_sha256_hash=cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8 define $(package)_set_vars - $(package)_config_env=AR="$($(package)_ar)" ARFLAGS=$($(package)_arflags) RANLIB="$($(package)_ranlib)" CC="$($(package)_cc)" + # Do NOT export ARFLAGS in config_env. The depends system never defines + # $(package)_arflags, so it expands to empty string. When CFLAGS/CPPFLAGS + # are passed as VAR=value (no positional flags), OpenSSL's Configure sets + # $anyuseradd=false and falls back to reading env vars. An empty ARFLAGS + # in the environment overrides the target default ("r"), producing a + # Makefile with ARFLAGS= (empty), which causes "ar: two different + # operation options specified" at build time. + $(package)_config_env=AR="$($(package)_ar)" RANLIB="$($(package)_ranlib)" CC="$($(package)_cc)" $(package)_config_env_android=ANDROID_NDK_ROOT=$(host_prefix)/native $(package)_config_opts=no-capieng no-dso no-dtls1 no-ec_nistp_64_gcc_128 no-gost $(package)_config_opts+=no-md2 no-rc5 no-rdrand no-rfc3779 no-sctp no-shared $(package)_config_opts+=no-ssl-trace no-ssl2 no-ssl3 no-tests no-unit-test no-weak-ssl-ciphers $(package)_config_opts+=no-zlib no-zlib-dynamic no-static-engine no-comp no-afalgeng - $(package)_config_opts+=no-engine no-hw no-asm $($(package)_cflags) $($(package)_cppflags) - $(package)_config_opts_linux=-fPIC -D_GNU_SOURCE - $(package)_config_opts_freebsd=-fPIC + $(package)_config_opts+=no-engine no-hw no-asm + # Pass compiler flags as VAR=value assignments (per OpenSSL INSTALL docs) + # rather than positional args, because multi-word flags like "-arch arm64" + # get split by the shell and OpenSSL's Configure misparses the second word + # as a target name, causing "target already defined" errors on ARM64 macOS. + # Use $$ for deferred evaluation so OS-specific flags (cflags_linux, etc.) + # appended by funcs.mk after set_vars runs are included in the expansion. + $(package)_config_opts+=CFLAGS="$$($(package)_cflags)" + $(package)_config_opts+=CPPFLAGS="$$($(package)_cppflags)" + $(package)_cflags_linux=-fPIC + $(package)_cppflags_linux=-D_GNU_SOURCE + $(package)_cflags_freebsd=-fPIC $(package)_config_opts_x86_64_linux=linux-x86_64 $(package)_config_opts_i686_linux=linux-generic32 $(package)_config_opts_arm_linux=linux-generic32 diff --git a/src/digidollar/validation.cpp b/src/digidollar/validation.cpp index 8ea1ad5962..9ef339c0b8 100644 --- a/src/digidollar/validation.cpp +++ b/src/digidollar/validation.cpp @@ -5,6 +5,7 @@ #include #include #include +#include // Phase 1 metadata tracking support using DigiDollar::ScriptMetadata; @@ -870,7 +871,18 @@ bool ValidateMintTransaction(const CTransaction& tx, LogPrintf("DigiDollar: Invalid DD amount: %d\n", ddAmount); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-dd-amount"); } - totalDD += ddAmount; + // If totalDD was already set from OP_RETURN, verify consistency + // rather than double-counting the DD amount. + if (totalDD > 0) { + if (ddAmount != totalDD) { + LogPrintf("DigiDollar: DD amount mismatch: token output=%lld, OP_RETURN=%lld\n", + (long long)ddAmount, (long long)totalDD); + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-dd-amount-mismatch", + "DD token output amount does not match OP_RETURN amount"); + } + } else { + totalDD += ddAmount; + } } // If we can't extract (cross-node validation), we'll calculate after loop } @@ -1001,8 +1013,9 @@ bool ValidateMintTransaction(const CTransaction& tx, // Verify sufficient collateral if (totalCollateral < requiredCollateral) { - LogPrintf("DigiDollar: Insufficient collateral: provided %d, required %d\n", - totalCollateral, requiredCollateral); + LogPrintf("DigiDollar: Insufficient collateral: provided %lld, required %lld (totalDD=%lld, lockPeriod=%lld)\n", + (long long)totalCollateral, (long long)requiredCollateral, + (long long)totalDD, (long long)lockPeriod); return state.Invalid(TxValidationResult::TX_CONSENSUS, "insufficient-collateral"); } diff --git a/src/oracle/bundle_manager.cpp b/src/oracle/bundle_manager.cpp index a2579b6e86..da7659be93 100644 --- a/src/oracle/bundle_manager.cpp +++ b/src/oracle/bundle_manager.cpp @@ -32,6 +32,29 @@ int32_t GetBestHeight() { return 0; } +/** + * Verify an oracle price message against the chain-configured oracle pubkey. + * + * SECURITY (DGB-SEC-002): Oracle messages arrive via P2P with an embedded pubkey + * that is attacker-controlled. We must never verify the signature against that + * embedded key — instead, look up the trusted pubkey from chainparams by oracle_id, + * rebind it onto the message, and then verify the Phase 2 Schnorr signature. + * + * @param message The oracle price message to verify + * @return true if the message's signature is valid against the chainparams pubkey + */ +static bool VerifyOracleMessageWithChainparamsKey(const COraclePriceMessage& message) +{ + const CChainParams& params = Params(); + const OracleNodeInfo* oracle_config = params.GetOracleNode(message.oracle_id); + if (!oracle_config) { + return false; + } + COraclePriceMessage bound_msg = message; + bound_msg.oracle_pubkey = XOnlyPubKey(oracle_config->pubkey); + return bound_msg.VerifyPhase2(); +} + //! Global oracle bundle manager instance std::unique_ptr g_oracle_bundle_manager; @@ -1048,32 +1071,21 @@ void OracleBundleManager::UpdateEpochBundle(int32_t epoch) bool OracleBundleManager::IsValidOracleMessage(const COraclePriceMessage& message) const { - // Phase One: Skip chainparams check when min_oracle_count == 1 (testing mode) - if (min_oracle_count == 1) { - if (!message.IsValid()) return false; - return message.VerifyPhase2(); - } - - // Phase Two (min_oracle_count > 1): Use Phase 2 signature hash - // Basic field validation without calling IsValid() which uses Phase 1 Verify() + // Common price bounds validation (applies to both phases) if (message.price_micro_usd < ORACLE_MIN_PRICE_MICRO_USD) return false; if (message.price_micro_usd > ORACLE_MAX_PRICE_MICRO_USD) return false; - // Verify oracle ID is in valid range and matches chainparams - const CChainParams& params = Params(); - const OracleNodeInfo* oracle_config = params.GetOracleNode(message.oracle_id); - if (!oracle_config) { - return false; - } + // Phase One: min_oracle_count == 1 (regtest/testing mode) + if (min_oracle_count == 1) { + // Field validation (timestamp freshness, price range) via IsValid() + if (!message.IsValid()) return false; - // SECURITY: Bind pubkey from chainparams before verification. - // The message may contain an attacker-supplied pubkey — we must verify - // against the authorized key, not whatever was deserialized from P2P. - COraclePriceMessage bound_msg = message; - bound_msg.oracle_pubkey = XOnlyPubKey(oracle_config->pubkey); + // SECURITY FIX (DGB-SEC-002): Verify against chainparams pubkey + return VerifyOracleMessageWithChainparamsKey(message); + } - // Verify Phase 2 Schnorr signature against chainparams pubkey - return bound_msg.VerifyPhase2(); + // Phase Two (min_oracle_count > 1): Verify against chainparams pubkey + return VerifyOracleMessageWithChainparamsKey(message); } std::vector OracleBundleManager::GetActiveOraclesForEpoch(int32_t epoch) const @@ -1362,15 +1374,15 @@ bool OracleDataValidator::ValidateOracleMessage(const COraclePriceMessage& messa return false; } - // Verify oracle is authorized + // Verify oracle is authorized and active const CChainParams& chainparams = Params(); const OracleNodeInfo* oracle_config = chainparams.GetOracleNode(message.oracle_id); if (!oracle_config || !oracle_config->is_active) { return false; } - // Verify signature - return message.VerifyPhase2(); + // SECURITY (DGB-SEC-002): Verify against chainparams pubkey + return VerifyOracleMessageWithChainparamsKey(message); } bool OracleDataValidator::ValidateOracleBundle(const COracleBundle& bundle, int32_t epoch, const Consensus::Params& params) @@ -1463,9 +1475,12 @@ bool OracleBundleManager::ValidatePhaseOneBundle(const COracleBundle& bundle, co } // Phase One: Schnorr signature verification (if signature is present) - if (!msg.schnorr_sig.empty() && !msg.VerifyPhase2()) { - LogPrintf("Oracle: Phase One signature verification failed\n"); - return false; + if (!msg.schnorr_sig.empty()) { + // SECURITY (DGB-SEC-002): Verify against chainparams pubkey + if (!VerifyOracleMessageWithChainparamsKey(msg)) { + LogPrintf("Oracle: Phase One signature verification failed for oracle %d\n", msg.oracle_id); + return false; + } } return true; @@ -1514,7 +1529,8 @@ bool OracleBundleManager::ValidatePhaseTwoBundle(const COracleBundle& bundle, co // Verify Schnorr signature using Phase 2 hash (oracle_id + price + timestamp only) if (!msg.schnorr_sig.empty()) { - if (!msg.VerifyPhase2()) { + // SECURITY (DGB-SEC-002): Verify against chainparams pubkey + if (!VerifyOracleMessageWithChainparamsKey(msg)) { LogPrintf("Oracle: Phase Two signature verification failed for oracle %d\n", msg.oracle_id); continue; // Skip message with invalid signature } diff --git a/src/test/digidollar_validation_tests.cpp b/src/test/digidollar_validation_tests.cpp index b497978904..b788410e73 100644 --- a/src/test/digidollar_validation_tests.cpp +++ b/src/test/digidollar_validation_tests.cpp @@ -37,8 +37,10 @@ struct DigiDollarValidationTestSetup : public TestingSetup { testPubKey = testKey.GetPubKey(); testXOnlyKey = XOnlyPubKey(testPubKey); - // Clear volatility freeze state from previous tests - DigiDollar::Volatility::VolatilityMonitor::ClearFreeze(); + // Clear ALL volatility state from previous test suites. + // ClearFreeze() alone is insufficient — UpdateState() recalculates + // from stale price history and can re-set freeze flags. + DigiDollar::Volatility::VolatilityMonitor::ClearHistory(); // Validation context is initialized in member initializer list } @@ -50,6 +52,22 @@ struct DigiDollarValidationTestSetup : public TestingSetup { int mockSystemCollateral; int mockHeight; DigiDollar::ValidationContext validationContext; + + // Helper: Build a DD mint OP_RETURN output script + // Format: OP_RETURN <"DD"> + CScript MakeDDMintOpReturn(CAmount ddAmount, int64_t lockHeight, int lockTier) { + CScript script; + script << OP_RETURN; + std::vector dd_marker = {'D', 'D'}; + script << dd_marker; + script << CScriptNum(1); // Type = MINT + script << CScriptNum::serialize(ddAmount); + script << CScriptNum::serialize(lockHeight); + script << CScriptNum(lockTier); + std::vector keyData(testXOnlyKey.begin(), testXOnlyKey.end()); + script << keyData; + return script; + } }; // ============================================================================ @@ -313,31 +331,45 @@ BOOST_FIXTURE_TEST_CASE(script_validation_non_dd_script, DigiDollarValidationTes BOOST_FIXTURE_TEST_CASE(transaction_validation_mint_tx, DigiDollarValidationTestSetup) { - // Create a mock mint transaction + // Create a valid mint transaction with all required components: + // - Collateral input + // - DD OP_RETURN with owner pubkey (required for NUMS verification per T1-04b) + // - Collateral output (P2TR with NUMS internal key) + // - DD token output CMutableTransaction mtx; mtx.nVersion = 0x01000770; // DD_TX_MINT (type=1 in bits 24-31, marker=0x0770 in bits 0-15) - // Add collateral input (simplified for test) + // Add collateral input mtx.vin.resize(1); mtx.vin[0].prevout = COutPoint(uint256S("0x1234"), 0); - // Add collateral output + // Set up mint parameters DigiDollar::MintParams params; params.ddAmount = 10000; // $100.00 - params.lockHeight = mockHeight + 30 * 24 * 60 * 4; + params.lockHeight = mockHeight + 30 * 24 * 60 * 4; // 30-day lock params.ownerKey = testXOnlyKey; - params.internalKey = testXOnlyKey; + params.internalKey = DigiDollar::GetCollateralNUMSKey(); params.oracleKeys = DigiDollar::GetOracleKeys(15); CScript collateralScript = DigiDollar::CreateCollateralP2TR(params); CAmount requiredCollateral = (static_cast(params.ddAmount) * COIN * 500 * 100) / mockOraclePrice; - mtx.vout.resize(2); - mtx.vout[0] = CTxOut(requiredCollateral, collateralScript); + // DD OP_RETURN with owner pubkey (required for NUMS verification) + CScript opReturn = CScript() << OP_RETURN + << std::vector{'D', 'D'} + << CScriptNum(1) + << CScriptNum(params.ddAmount) + << CScriptNum(params.lockHeight) + << CScriptNum(1) // lockTier 1 = 30 days + << std::vector(testXOnlyKey.begin(), testXOnlyKey.end()); + + mtx.vout.resize(3); + mtx.vout[0] = CTxOut(0, opReturn); + mtx.vout[1] = CTxOut(requiredCollateral, collateralScript); // Add DD token output CScript ddScript = DigiDollar::CreateDigiDollarP2TR(testXOnlyKey, params.ddAmount); - mtx.vout[1] = CTxOut(0, ddScript); // DD tokens have no DGB value + mtx.vout[2] = CTxOut(0, ddScript); // DD tokens have no DGB value CTransaction tx(mtx); TxValidationState state;