1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

feat(legacy): Support native SegWit external inputs with non-ownership proof.

This commit is contained in:
Andrew Kozlik 2022-12-21 17:29:53 +01:00 committed by matejcik
parent fa2d618f7d
commit ec9756cabd
9 changed files with 261 additions and 43 deletions

View File

@ -0,0 +1 @@
Support native SegWit external inputs with non-ownership proof.

View File

@ -150,6 +150,9 @@ bool fsm_checkCoinPath(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool show_warning);
bool fsm_getOwnershipId(uint8_t *script_pubkey, size_t script_pubkey_size,
uint8_t ownership_id[32]);
void fsm_abortWorkflows(void);
#endif

View File

@ -528,27 +528,37 @@ static bool formatFeeRate(uint64_t fee, uint64_t tx_weight, char *output,
}
void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
uint64_t total_in, uint64_t total_out, uint64_t change_out,
uint64_t total_in, uint64_t external_in,
uint64_t total_out, uint64_t change_out,
uint64_t tx_weight) {
char str_out[32] = {0};
formatAmountDifference(coin, amount_unit, total_in, change_out, str_out,
sizeof(str_out));
char str_fee[32] = {0};
formatAmountDifference(coin, amount_unit, total_in, total_out, str_fee,
sizeof(str_fee));
if (external_in == 0) {
char str_fee[32] = {0};
formatAmountDifference(coin, amount_unit, total_in, total_out, str_fee,
sizeof(str_fee));
char str_fee_rate[32] = {0};
bool show_fee_rate = total_in >= total_out;
char str_fee_rate[32] = {0};
bool show_fee_rate = total_in >= total_out;
if (show_fee_rate) {
formatFeeRate(total_in - total_out, tx_weight, str_fee_rate,
sizeof(str_fee_rate), coin->has_segwit);
if (show_fee_rate) {
formatFeeRate(total_in - total_out, tx_weight, str_fee_rate,
sizeof(str_fee_rate), coin->has_segwit);
}
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("Confirm sending:"), str_out, _("including fee:"),
str_fee, show_fee_rate ? str_fee_rate : NULL, NULL);
} else {
char str_spend[32] = {0};
formatAmountDifference(coin, amount_unit, total_in - external_in,
change_out, str_spend, sizeof(str_spend));
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("You are contributing:"), str_spend,
_("to the total amount:"), str_out, NULL, NULL);
}
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("Confirm sending:"), str_out, _("including fee:"),
str_fee, show_fee_rate ? str_fee_rate : NULL, NULL);
}
void layoutConfirmReplacement(const char *description, uint8_t txid[32]) {

View File

@ -57,7 +57,8 @@ void layoutConfirmOutput(const CoinInfo *coin, AmountUnit amount_unit,
void layoutConfirmOmni(const uint8_t *data, uint32_t size);
void layoutConfirmOpReturn(const uint8_t *data, uint32_t size);
void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
uint64_t total_in, uint64_t total_out, uint64_t change_out,
uint64_t total_in, uint64_t external_in,
uint64_t total_out, uint64_t change_out,
uint64_t tx_weight);
void layoutConfirmReplacement(const char *description, uint8_t txid[32]);
void layoutConfirmModifyOutput(const CoinInfo *coin, AmountUnit amount_unit,

View File

@ -72,8 +72,9 @@ enum {
} signing_stage;
static bool foreign_address_confirmed; // indicates that user approved warning
static bool taproot_only; // indicates whether all internal inputs are Taproot
static uint32_t idx1; // The index of the input or output in the current tx
// which is being processed, signed or serialized.
static bool has_unverified_external_input;
static uint32_t idx1; // The index of the input or output in the current tx
// which is being processed, signed or serialized.
static uint32_t idx2; // The index of the input or output in the original tx
// (Phase 1), in the previous tx (Phase 2) or in the
// current tx when computing the legacy digest (Phase 2).
@ -98,8 +99,9 @@ static uint8_t sig[64]; // Used in Phase 1 to store signature of original tx
#if !BITCOIN_ONLY
static uint8_t decred_hash_prefix[32];
#endif
static uint64_t total_in, total_out, change_out;
static uint64_t orig_total_in, orig_total_out, orig_change_out;
static uint64_t total_in, external_in, total_out, change_out;
static uint64_t orig_total_in, orig_external_in, orig_total_out,
orig_change_out;
static uint32_t progress, progress_step, progress_meta_step;
static uint32_t tx_weight;
@ -362,14 +364,6 @@ static bool is_external_input(uint32_t i) {
return external_inputs[i / 32] & (1 << (i % 32));
}
static bool has_external_input(void) {
uint32_t sum = 0;
for (size_t i = 0; i < sizeof(external_inputs) / sizeof(uint32_t); ++i) {
sum |= external_inputs[i];
}
return sum != 0;
}
void send_req_1_input(void) {
signing_stage = STAGE_REQUEST_1_INPUT;
resp.has_request_type = true;
@ -1100,13 +1094,16 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin,
foreign_address_confirmed = false;
taproot_only = true;
has_unverified_external_input = false;
signatures = 0;
idx1 = 0;
total_in = 0;
external_in = 0;
total_out = 0;
change_out = 0;
change_count = 0;
orig_total_in = 0;
orig_external_in = 0;
orig_total_out = 0;
orig_change_out = 0;
memzero(external_inputs, sizeof(external_inputs));
@ -1175,6 +1172,13 @@ static bool signing_validate_input(const TxInputType *txinput) {
signing_abort();
return false;
}
if (txinput->has_ownership_proof) {
fsm_sendFailure(FailureType_Failure_DataError,
_("Ownership proof provided but not expected."));
signing_abort();
return false;
}
} else if (txinput->script_type == InputScriptType_EXTERNAL) {
if (txinput->address_n_count != 0) {
fsm_sendFailure(FailureType_Failure_DataError,
@ -1214,6 +1218,13 @@ static bool signing_validate_input(const TxInputType *txinput) {
return false;
}
if (txinput->has_commitment_data && !txinput->has_ownership_proof) {
fsm_sendFailure(FailureType_Failure_DataError,
_("commitment_data field provided but not expected."));
signing_abort();
return false;
}
if (txinput->has_orig_hash) {
if (!txinput->has_orig_index) {
fsm_sendFailure(FailureType_Failure_DataError,
@ -1892,7 +1903,7 @@ static bool signing_add_orig_output(TxOutputType *orig_output) {
}
static bool signing_confirm_tx(void) {
if (has_external_input()) {
if (has_unverified_external_input) {
layoutConfirmUnverifiedExternalInputs();
if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
@ -1952,12 +1963,23 @@ static bool signing_confirm_tx(void) {
}
uint64_t orig_fee = orig_total_in - orig_total_out;
// Reject adding external inputs to the original transaction, so that we
// don't have to deal with the UI implications. This could be used for
// BIP-78 Payjoins when we support presigned external inputs.
if (external_in != orig_external_in) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Adding external inputs is not supported."));
signing_abort();
return false;
}
// Sanity check. Replacement transactions are only allowed to make
// amendments which do not increase the amount that we are spending on
// external outputs. Additional funds can only go towards the fee, which is
// confirmed by the user. The check may fail if the replacement transaction
// starts mixing accounts and breaks change-output identification.
if (total_out - change_out > orig_total_out - orig_change_out) {
if (total_out - change_out - external_in >
orig_total_out - orig_change_out - orig_external_in) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Invalid replacement transaction."));
signing_abort();
@ -2000,8 +2022,8 @@ static bool signing_confirm_tx(void) {
}
// last confirmation
layoutConfirmTx(coin, amount_unit, total_in, total_out, change_out,
tx_weight);
layoutConfirmTx(coin, amount_unit, total_in, external_in, total_out,
change_out, tx_weight);
if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
signing_abort();
@ -2824,11 +2846,39 @@ void signing_txack(TransactionType *tx) {
to.is_segwit = true;
#endif
} else if (tx->inputs[0].script_type == InputScriptType_EXTERNAL) {
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("External inputs not allowed."));
signing_abort();
return;
if (tx->inputs[0].has_ownership_proof) {
uint8_t ownership_id[OWNERSHIP_ID_SIZE] = {0};
if (!fsm_getOwnershipId(tx->inputs[0].script_pubkey.bytes,
tx->inputs[0].script_pubkey.size,
ownership_id)) {
signing_abort();
return;
}
if (!tx_input_verify_nonownership(coin, tx->inputs, ownership_id)) {
fsm_sendFailure(FailureType_Failure_DataError,
_("Invalid external input."));
signing_abort();
return;
}
if (!add_amount(&external_in, tx->inputs[0].amount)) {
return;
}
if (tx->inputs[0].has_orig_hash) {
if (!add_amount(&orig_external_in, tx->inputs[0].amount)) {
return;
}
}
} else {
has_unverified_external_input = true;
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Unverifiable external input."));
signing_abort();
return;
}
}
set_external_input(idx1);
} else {

View File

@ -1310,3 +1310,147 @@ bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type,
out->ownership_proof.size = r;
return true;
}
bool tx_input_verify_nonownership(
const CoinInfo *coin, const TxInputType *txinput,
const uint8_t ownership_id[OWNERSHIP_ID_SIZE]) {
size_t r = 0;
// Check versionMagic.
if (txinput->ownership_proof.size < r + sizeof(SLIP19_VERSION_MAGIC) ||
memcmp(txinput->ownership_proof.bytes + r, SLIP19_VERSION_MAGIC,
sizeof(SLIP19_VERSION_MAGIC)) != 0) {
return false;
}
r += sizeof(SLIP19_VERSION_MAGIC);
// Skip flags.
r += 1;
// Ensure that there is only one ownership ID.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 1) {
return false;
}
r += 1;
// Ensure that the ownership ID is not ours.
if (txinput->ownership_proof.size < r + OWNERSHIP_ID_SIZE ||
memcmp(txinput->ownership_proof.bytes + r, ownership_id,
OWNERSHIP_ID_SIZE) == 0) {
return false;
}
r += OWNERSHIP_ID_SIZE;
// Compute the ownership proof digest.
Hasher hasher = {0};
hasher_InitParam(&hasher, HASHER_SHA2, NULL, 0);
hasher_Update(&hasher, txinput->ownership_proof.bytes, r);
tx_script_hash(&hasher, txinput->script_pubkey.size,
txinput->script_pubkey.bytes);
tx_script_hash(&hasher, txinput->commitment_data.size,
txinput->commitment_data.bytes);
uint8_t digest[SHA256_DIGEST_LENGTH] = {0};
hasher_Final(&hasher, digest);
// Ensure that there is no scriptSig, since we only support native SegWit
// ownership proofs.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 0) {
return false;
}
r += 1;
if (txinput->script_pubkey.size == 22 &&
memcmp(txinput->script_pubkey.bytes, "\x00\x14", 2) == 0) {
// SegWit v0 (probably P2WPKH)
const uint8_t *pubkey_hash = txinput->script_pubkey.bytes + 2;
// Ensure that there are two stack items.
if (txinput->ownership_proof.size < r + 1 ||
txinput->ownership_proof.bytes[r] != 2) {
return false;
}
r += 1;
// Read the signature.
if (txinput->ownership_proof.size < r + 1) {
return false;
}
size_t signature_size = txinput->ownership_proof.bytes[r];
r += 1;
uint8_t signature[64] = {0};
if (txinput->ownership_proof.size < r + signature_size ||
ecdsa_sig_from_der(txinput->ownership_proof.bytes + r,
signature_size - 1, signature) != 0) {
return false;
}
r += signature_size;
// Read the public key.
if (txinput->ownership_proof.size < r + 34 ||
txinput->ownership_proof.bytes[r] != 33) {
return false;
}
const uint8_t *public_key = txinput->ownership_proof.bytes + r + 1;
r += 34;
// Check the public key matches the scriptPubKey.
uint8_t expected_pubkey_hash[20] = {0};
ecdsa_get_pubkeyhash(public_key, coin->curve->hasher_pubkey,
expected_pubkey_hash);
if (memcmp(pubkey_hash, expected_pubkey_hash,
sizeof(expected_pubkey_hash)) != 0) {
return false;
}
// Ensure that we have read the entire ownership proof.
if (r != txinput->ownership_proof.size) {
return false;
}
#ifdef USE_SECP256K1_ZKP_ECDSA
if (coin->curve->params == &secp256k1) {
if (zkp_ecdsa_verify_digest(coin->curve->params, public_key, signature,
digest) != 0) {
return false;
}
} else
#endif
{
if (ecdsa_verify_digest(coin->curve->params, public_key, signature,
digest) != 0) {
return false;
}
}
} else if (txinput->script_pubkey.size == 34 &&
memcmp(txinput->script_pubkey.bytes, "\x51\x20", 2) == 0) {
// SegWit v1 (P2TR)
const uint8_t *output_public_key = txinput->script_pubkey.bytes + 2;
// Ensure that there is one stack item consisting of 64 bytes.
if (txinput->ownership_proof.size < r + 2 ||
memcmp(txinput->ownership_proof.bytes + r, "\x01\x40", 2) != 0) {
return false;
}
r += 2;
// Read the signature.
const uint8_t *signature = txinput->ownership_proof.bytes + r;
r += 64;
// Ensure that we have read the entire ownership proof.
if (r != txinput->ownership_proof.size) {
return false;
}
if (zkp_bip340_verify_digest(output_public_key, signature, digest) != 0) {
return false;
}
} else {
// Unsupported script type.
return false;
}
return true;
}

View File

@ -152,5 +152,8 @@ bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type,
size_t script_pubkey_size,
const uint8_t *commitment_data,
size_t commitment_data_size, OwnershipProof *out);
bool tx_input_verify_nonownership(
const CoinInfo *coin, const TxInputType *txinput,
const uint8_t ownership_id[OWNERSHIP_ID_SIZE]);
#endif

View File

@ -577,7 +577,6 @@ def test_p2wpkh_in_p2sh_with_proof(client: Client):
pass
@pytest.mark.skip_t1
def test_p2wpkh_with_proof(client: Client):
inp1 = messages.TxInputType(
# seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
@ -611,16 +610,18 @@ def test_p2wpkh_with_proof(client: Client):
)
with client:
t1 = client.features.model == "1"
tt = client.features.model == "T"
client.set_expected_responses(
[
request_input(0),
request_input(1),
request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput),
(tt, messages.ButtonRequest(code=B.ConfirmOutput)),
request_output(1),
messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput),
(tt, messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
request_input(0),
request_meta(TXHASH_e5b7e2),
@ -636,6 +637,7 @@ def test_p2wpkh_with_proof(client: Client):
request_input(1),
request_output(0),
request_output(1),
(t1, request_input(0)),
request_input(1),
request_finished(),
]
@ -656,7 +658,7 @@ def test_p2wpkh_with_proof(client: Client):
# Test corrupted ownership proof.
inp1.ownership_proof[10] ^= 1
with pytest.raises(TrezorFailure, match="Invalid signature"):
with pytest.raises(TrezorFailure, match="Invalid signature|Invalid external input"):
btc.sign_tx(
client,
"Testnet",
@ -666,7 +668,6 @@ def test_p2wpkh_with_proof(client: Client):
)
@pytest.mark.skip_t1
@pytest.mark.setup_client(
mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
)
@ -703,17 +704,20 @@ def test_p2tr_with_proof(client: Client):
)
with client:
t1 = client.features.model == "1"
tt = client.features.model == "T"
client.set_expected_responses(
[
request_input(0),
request_input(1),
request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput),
(tt, messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
request_input(0),
request_input(1),
request_output(0),
(t1, request_input(0)),
request_input(1),
request_finished(),
]
@ -732,11 +736,10 @@ def test_p2tr_with_proof(client: Client):
# Test corrupted ownership proof.
inp1.ownership_proof[10] ^= 1
with pytest.raises(TrezorFailure, match="Invalid signature"):
with pytest.raises(TrezorFailure, match="Invalid signature|Invalid external input"):
btc.sign_tx(client, "Testnet", [inp1, inp2], [out1], prev_txes=TX_CACHE_TESTNET)
@pytest.mark.skip_t1
def test_p2wpkh_with_false_proof(client: Client):
inp1 = messages.TxInputType(
# tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9

View File

@ -269,7 +269,10 @@
"T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.SATOSHI]": "177fd9048c7661977db228678f9be74b1e769deb4cec87aa146ef51d20c604a1",
"T1_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[None]": "f2be7c23251127b50596f1a772a9eb933e0b1cef4c30afbc912930d1413f8694",
"T1_bitcoin-test_signtx_external.py::test_p2tr_external_unverified": "19e56e826e17f0b5cb5ab26d684dd4d1ef73da2711edc8bef2a086962c1382b0",
"T1_bitcoin-test_signtx_external.py::test_p2tr_with_proof": "5c2f96acb6e23f1e5698276e697c7afa299454fa12f1b2f6eae8b35490d69f6c",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_external_unverified": "6695ba71c171c82c4335db3f70dbb5f861c44141ea0eeea98e295ab6b51da3bb",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_with_false_proof": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1_bitcoin-test_signtx_external.py::test_p2wpkh_with_proof": "8f55fbd32b618e6373ed0c68dee2b3e275e0fce6ff7502bbb34ab4702238dae7",
"T1_bitcoin-test_signtx_invalid_path.py::test_attack_path_segwit": "ee152e7534c4dd60f939b7403a8169eb7d1703e3bdab819a575bc80f261212a9",
"T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail_asap": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",