When decoding or signing a PSBT, check blinded values.

When we decode or sign a PSBT given to us via RPC, first check that, if
it contains blinded output values, they verifiably match the unblinded
output values contained in the original transaction proposal. Otherwise fail.
This commit is contained in:
Glenn Willen 2020-08-28 05:01:37 -07:00 committed by Steven Roose
parent 4ae7d7abf1
commit 6d557b9a3c
No known key found for this signature in database
GPG key ID: 2F2A88D7F8D68E87
5 changed files with 116 additions and 0 deletions

View file

@ -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<unsigned char>& vchRangeproof, CAmount& amount_out, uint256& blinding_factor_out, CAsset& asset_out, uint256& asset_blinding_factor_out)
{
if (!blinding_key.IsValid() || vchRangeproof.size() == 0) {

View file

@ -19,6 +19,14 @@ static const size_t DEFAULT_RANGEPROOF_SIZE = 4174;
// constant-size surjection proof
static const size_t SURJECTION_PROOF_SIZE = 67;
/*
* 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.

View file

@ -71,6 +71,41 @@ static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue&
}
}
void RPCCheckPSBTBlinding(const PartiallySignedTransaction& psbtx) {
// 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()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "PSBT's 'tx' field may not have pre-blinded outputs.");
}
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.
throw JSONRPCError(RPC_INVALID_PARAMETER, "PSBT has a partially-blinded output. Blinded outputs must be fully blinded.");
}
if (!VerifyConfidentialPair(output.value_commitment, output.asset_commitment, txo.nValue.GetAmount(), txo.nAsset.GetAsset(), output.value_blinding_factor, output.asset_blinding_factor)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "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.");
}
}
}
static UniValue getrawtransaction(const JSONRPCRequest& request)
{
const RPCHelpMan help{
@ -1572,6 +1607,7 @@ UniValue decodepsbt(const JSONRPCRequest& request)
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
RPCCheckPSBTBlinding(psbtx);
UniValue result(UniValue::VOBJ);

View file

@ -13,6 +13,9 @@ namespace interfaces {
class Chain;
} // namespace interfaces
/** Check that the blinder did not tamper with the values in a blinded PSBT. */
void RPCCheckPSBTBlinding(const PartiallySignedTransaction& psbtx);
/** Sign a transaction with the given keystore and previous transactions */
UniValue SignTransaction(interfaces::Chain& chain, CMutableTransaction& mtx, const UniValue& prevTxs, CBasicKeyStore *keystore, bool tempKeystore, const UniValue& hashType);

View file

@ -4464,6 +4464,7 @@ UniValue walletsignpsbt(const JSONRPCRequest& request)
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
RPCCheckPSBTBlinding(psbtx);
// Get the sighash type
int nHashType = ParseSighashString(request.params[1]);