# This file is part of the Trezor project. # # Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 # as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the License along with this library. # If not, see . import pytest from trezorlib import btc, device, messages from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.exceptions import TrezorFailure from trezorlib.tools import H_, parse_path from .signtx import ( forge_prevtx, request_finished, request_input, request_meta, request_output, ) B = messages.ButtonRequestType # address at seed "all all all..." path m/44h/0h/0h/0/0 INPUT_ADDRESS = "1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL" PREV_HASH, PREV_TX = forge_prevtx([(INPUT_ADDRESS, 390_000)]) PREV_TXES = {PREV_HASH: PREV_TX} # Litecoin does not have strong replay protection using SIGHASH_FORKID, # spending from Bitcoin path should fail. @pytest.mark.altcoin def test_invalid_path_fail(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/0h/0h/0/0"), amount=390_000, prev_hash=PREV_HASH, prev_index=0, ) # address is converted from 1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1 by changing the version out1 = messages.TxOutputType( address="LfWz9wLHmqU9HoDkMg9NqbRosrHvEixeVZ", amount=390_000 - 10_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) with pytest.raises(TrezorFailure) as exc: btc.sign_tx(client, "Litecoin", [inp1], [out1], prev_txes=PREV_TXES) assert exc.value.code == messages.FailureType.DataError assert exc.value.message.endswith("Forbidden key path") # Litecoin does not have strong replay protection using SIGHASH_FORKID, but # spending from Bitcoin path should pass with safety checks set to prompt. @pytest.mark.altcoin def test_invalid_path_prompt(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/0h/0h/0/0"), amount=390_000, prev_hash=PREV_HASH, prev_index=0, ) # address is converted from 1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1 by changing the version out1 = messages.TxOutputType( address="LfWz9wLHmqU9HoDkMg9NqbRosrHvEixeVZ", amount=390_000 - 10_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) device.apply_settings( client, safety_checks=messages.SafetyCheckLevel.PromptTemporarily ) btc.sign_tx(client, "Litecoin", [inp1], [out1], prev_txes=PREV_TXES) # Bcash does have strong replay protection using SIGHASH_FORKID, # spending from Bitcoin path should work. @pytest.mark.altcoin def test_invalid_path_pass_forkid(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/0h/0h/0/0"), amount=390_000, prev_hash=PREV_HASH, prev_index=0, ) # address is converted from 1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1 to cashaddr format out1 = messages.TxOutputType( address="bitcoincash:qr0fk25d5zygyn50u5w7h6jkvctas52n0qxff9ja6r", amount=390_000 - 10_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) btc.sign_tx(client, "Bcash", [inp1], [out1], prev_txes=PREV_TXES) 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. # Generate keys address_a = btc.get_address( client, "Testnet", parse_path("m/84h/1h/0h/0/0"), script_type=messages.InputScriptType.SPENDWITNESS, ) address_b = btc.get_address( client, "Testnet", parse_path("m/84h/1h/1h/0/1"), script_type=messages.InputScriptType.SPENDWITNESS, ) prev_hash, prev_tx = forge_prevtx( [(address_a, 9_426), (address_b, 7_086)], network="testnet" ) device.apply_settings( client, safety_checks=messages.SafetyCheckLevel.PromptTemporarily ) 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) client.set_expected_responses( [ # Step: process inputs request_input(0), # Attacker bypasses warning about non-standard path. request_input(1), # Attacker bypasses warning about non-standard path. # Step: approve outputs request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), messages.ButtonRequest(code=B.SignTx), # Step: verify inputs request_input(0), request_meta(prev_hash), request_input(0, prev_hash), request_output(0, prev_hash), request_output(1, prev_hash), request_input(1), request_meta(prev_hash), request_input(0, prev_hash), request_output(0, prev_hash), request_output(1, prev_hash), # Step: serialize inputs request_input(0), request_input(1), # Step: serialize outputs request_output(0), # Step: sign segwit inputs request_input(0), # Trezor must warn about non-standard path before signing. messages.ButtonRequest(code=B.UnknownDerivationPath), request_input(1), # Trezor must warn about non-standard path before signing. messages.ButtonRequest(code=B.UnknownDerivationPath), request_finished(), ] ) btc.sign_tx( client, "Testnet", [inp1, inp2], [out1], prev_txes={prev_hash: prev_tx} ) @pytest.mark.skip_t1(reason="T1 only prevents using paths known to be altcoins") def test_invalid_path_fail_asap(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/0"), amount=1_000_000, prev_hash=b"\x42" * 32, prev_index=0, script_type=messages.InputScriptType.SPENDWITNESS, sequence=4_294_967_293, ) out1 = messages.TxOutputType( address_n=parse_path("m/84h/0h/0h/1/0"), amount=1_000_000, script_type=messages.OutputScriptType.PAYTOWITNESS, ) with client: client.set_expected_responses( [ request_input(0), messages.Failure(code=messages.FailureType.DataError), ] ) try: btc.sign_tx(client, "Testnet", [inp1], [out1]) except TrezorFailure: pass