diff --git a/src/blind.cpp b/src/blind.cpp index e9db1a034f..e99395225d 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -36,6 +36,74 @@ public: static Blind_ECC_Init ecc_init_on_load; +bool VerifyConfidentialPair(const CConfidentialValue& conf_value, const CConfidentialAsset& conf_asset, const CAmount& claimed_value, const CAsset& claimed_asset, const uint256& value_blinding_factor, const uint256& asset_blinding_factor) { + if (conf_value.IsNull() || conf_asset.IsNull() || claimed_asset.IsNull()) { + return false; + } + + if (conf_value.IsExplicit()) { + // Match behavior of UnblindConfidentialPair + return false; + } + if (conf_asset.IsExplicit() && conf_asset.GetAsset() != claimed_asset) { + return false; + } + + // Just to be safe + if (!MoneyRange(claimed_value)) { + return false; + } + + // Valid asset commitment? + secp256k1_generator observed_gen; + if (conf_asset.IsCommitment()) { + if (secp256k1_generator_parse(secp256k1_blind_context, &observed_gen, &conf_asset.vchCommitment[0]) != 1) + return false; + } else if (conf_asset.IsExplicit()) { + if (secp256k1_generator_generate(secp256k1_blind_context, &observed_gen, conf_asset.GetAsset().begin()) != 1) + return false; + } + + // Valid value commitment? + secp256k1_pedersen_commitment value_commit; + if (secp256k1_pedersen_commitment_parse(secp256k1_blind_context, &value_commit, conf_value.vchCommitment.data()) != 1) { + return false; + } + + const unsigned char *asset_type = claimed_asset.id.begin(); + const unsigned char *asset_blinder = asset_blinding_factor.begin(); + secp256k1_generator recalculated_gen; + if (secp256k1_generator_generate_blinded(secp256k1_blind_context, &recalculated_gen, asset_type, asset_blinder) != 1) { + return false; + } + + // Serialize both generators then compare + unsigned char observed_generator[33]; + unsigned char derived_generator[33]; + secp256k1_generator_serialize(secp256k1_blind_context, observed_generator, &observed_gen); + secp256k1_generator_serialize(secp256k1_blind_context, derived_generator, &recalculated_gen); + if (memcmp(observed_generator, derived_generator, sizeof(observed_generator))) { + return false; + } + + const unsigned char *value_blinder = value_blinding_factor.begin(); + secp256k1_pedersen_commitment recalculated_commit; + if(secp256k1_pedersen_commit(secp256k1_blind_context, &recalculated_commit, value_blinder, claimed_value, &observed_gen) != 1) { + return false; + } + + // Serialize both value commitments then compare + unsigned char claimed_commitment[33]; + unsigned char derived_commitment[33]; + secp256k1_pedersen_commitment_serialize(secp256k1_blind_context, claimed_commitment, &value_commit); + secp256k1_pedersen_commitment_serialize(secp256k1_blind_context, derived_commitment, &recalculated_commit); + if (memcmp(claimed_commitment, derived_commitment, sizeof(claimed_commitment))) { + return false; + } + + return true; +} + bool UnblindConfidentialPair(const CKey& blinding_key, const CConfidentialValue& conf_value, const CConfidentialAsset& conf_asset, const CConfidentialNonce& nonce_commitment, const CScript& committedScript, const std::vector& vchRangeproof, CAmount& amount_out, uint256& blinding_factor_out, CAsset& asset_out, uint256& asset_blinding_factor_out) { if (!blinding_key.IsValid() || vchRangeproof.size() == 0) { diff --git a/src/blind.h b/src/blind.h index 1e1a436e43..f3e5ecc1c9 100644 --- a/src/blind.h +++ b/src/blind.h @@ -24,6 +24,14 @@ static const size_t DEFAULT_SURJECTIONPROOF_SIZE = 135; // 32 bytes of asset type, 32 bytes of asset blinding factor in sidechannel static const size_t SIDECHANNEL_MSG_SIZE = 64; +/* + * Verify a pair of confidential asset and value, given the blinding factors for both. + * Unlike UnblindConfidentialPair, this does _not_ require the recipient's blinding + * key, but it _does_ require the blinding factors be provided (rather than extracting + * them from the rangeproof.) +*/ +bool VerifyConfidentialPair(const CConfidentialValue& conf_value, const CConfidentialAsset& conf_asset, const CAmount& claimed_value, const CAsset& claimed_asset, const uint256& value_blinding_factor, const uint256& asset_blinding_factor); + /* * Unblind a pair of confidential asset and value. * Note that unblinded data will only be outputted if *BOTH* asset and value could be unblinded. diff --git a/src/psbt.cpp b/src/psbt.cpp index a555b0aefe..bad4d86177 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -443,3 +444,43 @@ bool DecodeRawPSBT(PartiallySignedTransaction& psbt, const std::string& tx_data, } return true; } + +bool CheckPSBTBlinding(const PartiallySignedTransaction& psbtx, std::string& error) { + // Plausibly, we may want a way to let the user continue anyway. However, we + // want to fail by default, to make it as hard as possible to do something + // really dangerous. And since this way of handling blinded PSBTs is going + // away "real soon now" in favor of a better one, no sense in trying too + // hard about it. + + for (size_t i = 0; i < psbtx.outputs.size(); ++i) { + const PSBTOutput& output = psbtx.outputs[i]; + const CTxOut& txo = psbtx.tx->vout[i]; + + if (txo.nValue.IsCommitment() || txo.nAsset.IsCommitment()) { + error = "PSBT's 'tx' field may not have pre-blinded outputs."; + return false; + } + + if (!output.value_commitment.IsCommitment() && + !output.asset_commitment.IsCommitment() && + output.value_blinding_factor.IsNull() && + output.asset_blinding_factor.IsNull()) { + // Nothing blinded, nothing to check. + continue; + } else if (!output.value_commitment.IsCommitment() || + !output.asset_commitment.IsCommitment() || + output.value_blinding_factor.IsNull() || + output.asset_blinding_factor.IsNull()) { + // Something blinded, but not everything? That's not expected. + error = "PSBT has a partially-blinded output. Blinded outputs must be fully blinded."; + return false; + } + + if (!VerifyConfidentialPair(output.value_commitment, output.asset_commitment, txo.nValue.GetAmount(), txo.nAsset.GetAsset(), output.value_blinding_factor, output.asset_blinding_factor)) { + error = "PSBT's 'tx' field output values do not match blinded output values (or are invalid in some way)! Either there is a bug, or the blinder is attacking you."; + return false; + } + } + return true; +} + diff --git a/src/psbt.h b/src/psbt.h index 2bfa85178f..daa62573bd 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -1001,4 +1001,7 @@ NODISCARD bool DecodeRawPSBT(PartiallySignedTransaction& decoded_psbt, const std std::string EncodePSBT(const PartiallySignedTransaction& psbt); +/** Check that the blinder did not tamper with the values in a blinded PSBT. */ +bool CheckPSBTBlinding(const PartiallySignedTransaction& psbtx, std::string& error); + #endif // BITCOIN_PSBT_H diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index ef65fe88df..6822e500e3 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1298,6 +1298,9 @@ static RPCHelpMan decodepsbt() if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error)); } + if (!CheckPSBTBlinding(psbtx, error)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, error); + } UniValue result(UniValue::VOBJ); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index fdee658f45..08e3f79c58 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4733,6 +4733,9 @@ static RPCHelpMan walletsignpsbt() if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error)); } + if (!CheckPSBTBlinding(psbtx, error)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, error); + } // Get the sighash type int nHashType = ParseSighashString(request.params[1]);