def test_attack_change_input(client: Client): # NOTE: fake input tx used inp1 = messages.TxInputType( address_n=parse_path("m/44h/156h/0h/0/0"), amount=1_252_382_934, prev_hash=FAKE_TXHASH_6f0398, prev_index=0, script_type=messages.InputScriptType.SPENDADDRESS, ) out1 = messages.TxOutputType( address_n=parse_path("m/44h/156h/0h/1/0"), amount=1_896_050, script_type=messages.OutputScriptType.PAYTOADDRESS, ) out2 = messages.TxOutputType( address="GfDB1tvjfm3bukeoBTtfNqrJVFohS2kCTe", amount=1_252_382_934 - 1_896_050 - 1_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) attack_count = 2 def attack_processor(msg): nonlocal attack_count if msg.tx.inputs and msg.tx.inputs[0] == inp1: if attack_count > 0: attack_count -= 1 else: msg.tx.inputs[0].address_n[2] = H_(1) return msg with client: client.set_filter(messages.TxAck, attack_processor) client.set_expected_responses([ request_input(0), request_output(0), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(FAKE_TXHASH_6f0398), request_input(0, FAKE_TXHASH_6f0398), request_output(0, FAKE_TXHASH_6f0398), request_output(1, FAKE_TXHASH_6f0398), request_input(0), messages.Failure(code=messages.FailureType.ProcessError), ]) with pytest.raises(TrezorFailure): btc.sign_tx(client, "Bgold", [inp1], [out1, out2], prev_txes=TX_API)
def test_attack_change_input(client: Client): # NOTE: fake input tx used inp1 = messages.TxInputType( address_n=parse_path("m/44h/145h/10h/0/0"), amount=1_995_344, prev_hash=FAKE_TXHASH_bd32ff, prev_index=0, script_type=messages.InputScriptType.SPENDADDRESS, ) out1 = messages.TxOutputType( address_n=parse_path("m/44h/145h/10h/1/0"), amount=1_896_050, script_type=messages.OutputScriptType.PAYTOADDRESS, ) out2 = messages.TxOutputType( address="bitcoincash:qr23ajjfd9wd73l87j642puf8cad20lfmqdgwvpat4", amount=73_452, script_type=messages.OutputScriptType.PAYTOADDRESS, ) attack_count = 2 def attack_processor(msg): nonlocal attack_count if msg.tx.inputs and msg.tx.inputs[0] == inp1: if attack_count > 0: attack_count -= 1 else: msg.tx.inputs[0].address_n[2] = H_(1) return msg with client: client.set_filter(messages.TxAck, attack_processor) client.set_expected_responses([ request_input(0), request_output(0), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(FAKE_TXHASH_bd32ff), request_input(0, FAKE_TXHASH_bd32ff), request_output(0, FAKE_TXHASH_bd32ff), request_input(0), messages.Failure(code=messages.FailureType.ProcessError), ]) with pytest.raises(TrezorFailure): btc.sign_tx(client, "Bcash", [inp1], [out1, out2], prev_txes=TX_API)
def test_invalid_prev_hash_attack(client: Client, prev_hash): # prepare input with a valid prev-hash inp1 = messages.TxInputType( address_n=tools.parse_path("m/44h/0h/0h/0/0"), amount=100_000_000, prev_hash=PREV_HASH, prev_index=0, script_type=messages.InputScriptType.SPENDP2SHWITNESS, ) out1 = messages.TxOutputType( address="1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1", amount=100_000_000 - 10_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) counter = 1 def attack_filter(msg): nonlocal counter if not msg.tx.inputs: return msg # on first attempt, send unmodified input if counter > 0: counter -= 1 return msg # on second request, send modified input msg.tx.inputs[0].prev_hash = prev_hash return msg with client, pytest.raises(TrezorFailure) as e: client.set_filter(messages.TxAck, attack_filter) btc.sign_tx(client, "Bitcoin", [inp1], [out1], prev_txes=PREV_TXES) # check that injection was performed assert counter == 0 _check_error_message(prev_hash, client.features.model, e.value.message)
def test_attack_change_input(client: Client): """ In Phases 1 and 2 the attacker replaces a non-multisig input `input_real` with a multisig input `input_fake`, which allows the attacker to provide a 1-of-2 multisig change address. When `input_real` is provided in the signing phase, an error must occur. """ address_n = parse_path( "m/48h/1h/0h/1h/0/0") # 2NErUdruXmM8o8bQySrzB3WdBRcmc5br4E8 attacker_multisig_public_key = bytes.fromhex( "03653a148b68584acb97947344a7d4fd6a6f8b8485cad12987ff8edac874268088") input_real = messages.TxInputType( address_n=address_n, prev_hash=TXHASH_509e08, prev_index=0, script_type=messages.InputScriptType.SPENDP2SHWITNESS, amount=61_093, ) multisig_fake = messages.MultisigRedeemScriptType( m=1, nodes=[ btc.get_public_node(client, address_n, coin_name="Testnet").node, messages.HDNodeType( depth=0, fingerprint=0, child_num=0, chain_code=bytes(32), public_key=attacker_multisig_public_key, ), ], address_n=[], ) input_fake = messages.TxInputType( address_n=address_n, prev_hash=input_real.prev_hash, prev_index=input_real.prev_index, script_type=input_real.script_type, multisig=multisig_fake, amount=input_real.amount, ) output_payee = messages.TxOutputType( address="n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi", amount=10_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) output_change = messages.TxOutputType( address_n=address_n, amount=input_real.amount - output_payee.amount - 1_000, script_type=messages.OutputScriptType.PAYTOP2SHWITNESS, multisig=multisig_fake, ) # Transaction can be signed without the attack processor with client: btc.sign_tx( client, "Testnet", [input_real], [output_payee, output_change], prev_txes=TX_API_TESTNET, ) attack_count = 3 def attack_processor(msg): nonlocal attack_count # replace the first input_real with input_fake if attack_count > 0 and msg.tx.inputs and msg.tx.inputs[ 0] == input_real: msg.tx.inputs[0] = input_fake attack_count -= 1 return msg with client: client.set_filter(messages.TxAck, attack_processor) with pytest.raises(TrezorFailure): btc.sign_tx( client, "Testnet", [input_real], [output_payee, output_change], prev_txes=TX_API_TESTNET, )
def test_attack_change_input_address(client: Client): # Simulates an attack where the user is coerced into unknowingly # transferring funds from one account to another one of their accounts, # potentially resulting in privacy issues. inp1 = messages.TxInputType( address_n=parse_path("m/49h/1h/0h/1/0"), # 2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX amount=123_456_789, prev_hash=TXHASH_20912f, prev_index=0, script_type=messages.InputScriptType.SPENDP2SHWITNESS, ) out1 = messages.TxOutputType( address="mhRx1CeVfaayqRwq5zgRQmD7W5aWBfD5mC", amount=12_300_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) out2 = messages.TxOutputType( address_n=parse_path("m/49h/1h/12h/1/0"), script_type=messages.OutputScriptType.PAYTOP2SHWITNESS, amount=123_456_789 - 11_000 - 12_300_000, ) # Test if the transaction can be signed normally. with client: client.set_expected_responses([ request_input(0), request_output(0), # The user is required to confirm transfer to another account. messages.ButtonRequest(code=B.ConfirmOutput), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_20912f), request_input(0, TXHASH_20912f), request_output(0, TXHASH_20912f), request_output(1, TXHASH_20912f), request_input(0), request_output(0), request_output(1), request_input(0), request_finished(), ]) _, serialized_tx = btc.sign_tx(client, "Testnet", [inp1], [out1, out2], prev_txes=TX_API_TESTNET) assert ( serialized_tx.hex() == "0100000000010137c361fb8f2d9056ba8c98c5611930fcb48cacfdd0fe2e0449d83eea982f91200000000017160014d16b8c0680c61fc6ed2e407455715055e41052f5ffffffff02e0aebb00000000001976a91414fdede0ddc3be652a0ce1afbc1b509a55b6b94888ac3df39f060000000017a9142f98413cb83ff8b3eaf1926192e68973cbd68a3a8702473044022013cbce7c575337ca05dbe03b5920a0805b510cd8dfd3180bd7c5d01cec6439cd0220050001be4bcefb585caf973caae0ffec682347f2127cc22f26efd93ee54fd852012103e7bfe10708f715e8538c92d46ca50db6f657bbc455b7494e6a0303ccdb868b7900000000" ) attack_count = 2 def attack_processor(msg): nonlocal attack_count if attack_count > 0 and msg.tx.inputs and msg.tx.inputs[0] == inp1: attack_count -= 1 msg.tx.inputs[0].address_n[2] = H_(12) return msg # Now run the attack, must trigger the exception with client: client.set_filter(messages.TxAck, attack_processor) with pytest.raises(TrezorFailure): btc.sign_tx(client, "Testnet", [inp1], [out1, out2], prev_txes=TX_API_TESTNET)
def test_attack_script_type(client: Client): # Scenario: The attacker falsely claims that the transaction is Taproot-only to # avoid prev tx streaming and gives a lower amount for one of the inputs. The # correct input types and amounts are revelaled only in step6_sign_segwit_inputs() # to get a valid signature. This results in a transaction which pays a fee much # larger than what the user confirmed. inp1 = messages.TxInputType( address_n=parse_path("m/84h/1h/0h/1/0"), amount=7_289_000, prev_hash=TXHASH_65b811, prev_index=1, script_type=messages.InputScriptType.SPENDWITNESS, ) inp2 = messages.TxInputType( address_n=parse_path("m/84h/1h/1h/0/0"), amount=12_300_000, prev_hash=TXHASH_091446, prev_index=0, script_type=messages.InputScriptType.SPENDWITNESS, ) out1 = messages.TxOutputType( address="tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu", script_type=messages.OutputScriptType.PAYTOADDRESS, amount=7_289_000 + 10_000 - 1_000, ) attack_count = 5 def attack_processor(msg): nonlocal attack_count if attack_count > 0 and msg.tx.inputs: attack_count -= 1 if msg.tx.inputs[0] == inp2: msg.tx.inputs[0].amount = 10000 msg.tx.inputs[0].address_n[0] = H_(86) msg.tx.inputs[0].script_type = messages.InputScriptType.SPENDTAPROOT return msg with client: client.set_filter(messages.TxAck, attack_processor) client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.SignTx), request_input(0), request_input(1), request_output(0), request_input(0), request_input(1), messages.Failure(code=messages.FailureType.ProcessError), ] ) with pytest.raises(TrezorFailure) as exc: btc.sign_tx(client, "Testnet", [inp1, inp2], [out1], prev_txes=TX_API) assert exc.value.code == messages.FailureType.ProcessError assert exc.value.message.endswith("Transaction has changed during signing")
def test_attack_path_segwit(client: Client): # Scenario: The attacker falsely claims that the transaction uses Testnet paths to # avoid the path warning dialog, but in step6_sign_segwit_inputs() uses Bitcoin paths # to get a valid signature. device.apply_settings( client, safety_checks=messages.SafetyCheckLevel.PromptTemporarily) # Generate keys address_a = btc.get_address( client, "Testnet", parse_path("m/84h/0h/0h/0/0"), script_type=messages.InputScriptType.SPENDWITNESS, ) address_b = btc.get_address( client, "Testnet", parse_path("m/84h/0h/1h/0/1"), script_type=messages.InputScriptType.SPENDWITNESS, ) prev_hash, prev_tx = forge_prevtx([(address_a, 9_426), (address_b, 7_086)], network="testnet") inp1 = messages.TxInputType( # The actual input that the attacker wants to get signed. address_n=parse_path("m/84h/0h/0h/0/0"), amount=9_426, prev_hash=prev_hash, prev_index=0, script_type=messages.InputScriptType.SPENDWITNESS, ) inp2 = messages.TxInputType( # The actual input that the attacker wants to get signed. # We need this one to be from a different account, so that the match checker # allows the transaction to pass. address_n=parse_path("m/84h/0h/1h/0/1"), amount=7_086, prev_hash=prev_hash, prev_index=1, script_type=messages.InputScriptType.SPENDWITNESS, ) out1 = messages.TxOutputType( # Attacker's Mainnet address encoded as Testnet. address="tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu", script_type=messages.OutputScriptType.PAYTOADDRESS, amount=9_426 + 7_086 - 500, ) attack_count = 6 def attack_processor(msg): nonlocal attack_count # Make the inputs look like they are coming from Testnet paths until we reach the # signing phase. if attack_count > 0 and msg.tx.inputs and msg.tx.inputs[0] in (inp1, inp2): attack_count -= 1 msg.tx.inputs[0].address_n[1] = H_(1) return msg with client: client.set_filter(messages.TxAck, attack_processor) with pytest.raises(TrezorFailure): btc.sign_tx(client, "Testnet", [inp1, inp2], [out1], prev_txes={prev_hash: prev_tx})