diff --git a/tests/fixtures/base-thorchain-real-deposits.json b/tests/fixtures/base-thorchain-real-deposits.json new file mode 100644 index 00000000..18c9d72e --- /dev/null +++ b/tests/fixtures/base-thorchain-real-deposits.json @@ -0,0 +1,32 @@ +[ + { + "txid": "0xd567902e8e057283c10c41cc5f95cc236d60eaea39834295a53851dee231d808", + "chain_id": 8453, + "to": "0x00dc6100103bc402d490aee3f9a5560cbd91f1d4", + "value": 41276400000000000, + "nonce": 49, + "gas_price": 50000000, + "gas_limit": 200000, + "data": "0x44bc937b0000000000000000000000004d39d5c55cfeb48f44d680fcac20493635dafdee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000092a4aac79b600000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006acd804100000000000000000000000000000000000000000000000000000000000000643d3a54524f4e2e555344542d5452374e48514a454b5158475443493851385a5934504c384f54535a474a4c4a36543a544d6f554a3245715645536763614435654a647670686b6b6e384b337563586454703a393332313030343337393a6b6565703a333000000000000000000000000000000000000000000000000000000000", + "memo": "=:TRON.USDT-TR7NHQJEKQXGTCI8Q8ZY4PL8OTSZGJLJ6T:TMoUJ2EqVESgcaD5eJdvphkkn8K3ucXdTp:9321004379:keep:30", + "memo_length": 100, + "calldata_length": 324, + "vault": "0x4d39d5c55cfeb48f44d680fcac20493635dafdee", + "expiry": 1791852609 + }, + { + "txid": "0x3492f35c2b19554d761dd342b8f2bc5d07f7dfac3a6118893c82d86aacd0e856", + "chain_id": 8453, + "to": "0x00dc6100103bc402d490aee3f9a5560cbd91f1d4", + "value": 8255225600000000, + "nonce": 50, + "gas_price": 50000000, + "gas_limit": 200000, + "data": "0x44bc937b0000000000000000000000004d39d5c55cfeb48f44d680fcac20493635dafdee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d54157d6dc00000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006acd810400000000000000000000000000000000000000000000000000000000000000403d3a54524f4e2e5452583a544d6f554a3245715645536763614435654a647670686b6b6e384b337563586454703a353734303833303833333a6b6565703a3330", + "memo": "=:TRON.TRX:TMoUJ2EqVESgcaD5eJdvphkkn8K3ucXdTp:5740830833:keep:30", + "memo_length": 64, + "calldata_length": 260, + "vault": "0x4d39d5c55cfeb48f44d680fcac20493635dafdee", + "expiry": 1791852804 + } +] diff --git a/tests/test_msg_ethereum_thorchain_deposit.py b/tests/test_msg_ethereum_thorchain_deposit.py index 027af7f5..b6da8acf 100644 --- a/tests/test_msg_ethereum_thorchain_deposit.py +++ b/tests/test_msg_ethereum_thorchain_deposit.py @@ -287,5 +287,55 @@ def test_deposit_new_routers_wrong_chain_blocked(self): data=data, ) + def test_base_real_deposits_without_advanced_mode(self): + """Replay the two Base swaps with their exact calldata and recover + the emulator's signer; no AdvancedMode or metadata is supplied.""" + self.requires_fullFeature() + self.requires_firmware("7.15.0") + self.requires_release_capability('thor-deposit-review') + self.setup_mnemonic_allallall() + + import json + from pathlib import Path + from keepkeylib.signed_metadata import eth_sighash_legacy, keccak256 + from keepkeylib.client import CallException + from ecdsa import VerifyingKey, SECP256k1, util + + self.client.init_device() + advanced = [p for p in self.client.features.policies + if p.policy_name == "AdvancedMode"] + self.assertTrue(advanced, "AdvancedMode policy must be reported") + self.assertFalse(advanced[0].enabled) + vectors = json.loads((Path(__file__).parent / "fixtures" / + "base-thorchain-real-deposits.json").read_text()) + n = parse_path("m/44'/60'/0'/0/0") + me = self.client.ethereum_get_address(n) + for tx in vectors: + with self.subTest(txid=tx["txid"]): + data = bytes.fromhex(tx["data"][2:]) + to = bytes.fromhex(tx["to"][2:]) + sig_v, sig_r, sig_s = self.client.ethereum_sign_tx( + n=n, nonce=tx["nonce"], gas_price=tx["gas_price"], + gas_limit=tx["gas_limit"], to=to, value=tx["value"], + chain_id=tx["chain_id"], data=data, + ) + self.assertIn(sig_v, [16941, 16942]) + digest = eth_sighash_legacy( + tx["nonce"], tx["gas_price"], tx["gas_limit"], to, + tx["value"], data, tx["chain_id"]) + keys = VerifyingKey.from_public_key_recovery_with_digest( + sig_r + sig_s, digest, SECP256k1, hashfunc=None, + sigdecode=util.sigdecode_string) + self.assertEqual(keccak256(keys[sig_v - 16941].to_string())[-20:], me) + # Avalanche shares the router identity; it is a valid pin. + for dest, chain in ((b"\x11" * 20, 8453), (to, 1), (to, 56), + (to, 42161)): + with self.assertRaises(CallException): + self.client.ethereum_sign_tx( + n=n, nonce=tx["nonce"], gas_price=tx["gas_price"], + gas_limit=tx["gas_limit"], to=dest, value=tx["value"], + chain_id=chain, data=data, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_msg_zcash_display_address.py b/tests/test_msg_zcash_display_address.py index 3de0e109..53112d57 100644 --- a/tests/test_msg_zcash_display_address.py +++ b/tests/test_msg_zcash_display_address.py @@ -11,6 +11,8 @@ import common from keepkeylib import messages_zcash_pb2 as zcash_proto +from keepkeylib import types_pb2 as proto_types +from keepkeylib.client import CallException from keepkeylib.tools import parse_path # Hardened offset @@ -62,15 +64,22 @@ def test_zcash_display_address_bad_path_rejected(self): is rejected with a SyntaxError (no silent wrong-account derivation).""" self.setup_mnemonic_allallall() - import pytest - from keepkeylib.client import CallException + # Without account=, the path itself selects the approved account. + valid = self.client.zcash_display_address( + address_n=[H + 32, H + 133, H + 0]) + explicit = self.client.zcash_display_address(account=0) + self.assertIsInstance(valid, zcash_proto.ZcashAddress) + self.assertEqual(valid.address, explicit.address) - with pytest.raises(CallException): + with self.assertRaises(CallException) as caught: self.client.call( zcash_proto.ZcashDisplayAddress( address_n=[H + 44, H + 133, H + 0], # wrong purpose (44') ) ) + self.assertEqual(caught.exception.args[0], proto_types.Failure_SyntaxError) + self.assertEqual(caught.exception.args[1], + "Require account field or ZIP-32 path m/32'/133'/account'") if __name__ == '__main__': diff --git a/tests/test_msg_zcash_seed_fingerprint.py b/tests/test_msg_zcash_seed_fingerprint.py index c2be4190..6f0810e0 100644 --- a/tests/test_msg_zcash_seed_fingerprint.py +++ b/tests/test_msg_zcash_seed_fingerprint.py @@ -9,9 +9,14 @@ import common from keepkeylib import messages_zcash_pb2 as zcash_proto +from keepkeylib import types_pb2 as proto_types from keepkeylib.client import CallException from keepkeylib.zcash import calculate_seed_fingerprint +from test_msg_zcash_sign_pczt_device import ( + PADDING_BUNDLE, bundle_digest, fabricated_action, sign_kwargs, +) + # Hardened offset H = 0x80000000 @@ -117,25 +122,64 @@ def test_device_fingerprint_matches_python_helper(self): # ── ZcashSignPCZT: through client.zcash_sign_pczt(...) ────────────── - def test_sign_pczt_helper_rejects_wrong_fingerprint(self): - """Helper passes expected_seed_fingerprint through; wrong fp rejected - before any signing crypto runs.""" + def _signing_request(self): + """A complete wallet spend accepted by the current firmware.""" + actions = [fabricated_action(v) for v in PADDING_BUNDLE] + actions[1]['is_spend'] = False + kwargs = sign_kwargs(actions, fee=10000, orchard_value_balance=10000, + total_amount=90000) + kwargs['orchard_digest'] = bundle_digest( + actions, False, 5, value_balance=10000) + return kwargs + + def test_sign_pczt_helper_accepts_matching_fingerprint(self): + """The same complete request signs with this seed's fingerprint.""" self.setup_mnemonic_allallall() + fvk = self.client.zcash_get_orchard_fvk( + address_n=[H + 32, H + 133, H], account=0) + result = self.client.zcash_sign_pczt( + **self._signing_request(), + expected_seed_fingerprint=fvk.seed_fingerprint, + ) + self.assertIsInstance(result, zcash_proto.ZcashSignedPCZT) + self.assertEqual(len(result.signatures), 1) + self.assertEqual(len(result.signatures[0]), 64) - wrong_fp = b"\x01" * 32 - - with pytest.raises(CallException): - self.client.zcash_sign_pczt( - address_n=[H + 32, H + 133, H + 0], - actions=[{"is_spend": False}], - # Explicit dummy action passes the helper's contract preflight; - # the bad fingerprint is still rejected by the initial device call. - account=0, - total_amount=100000, - fee=10000, - branch_id=0x37519621, - expected_seed_fingerprint=wrong_fp, - ) + def test_sign_pczt_helper_rejects_wrong_fingerprint(self): + """A changed fingerprint alone causes the precise initial refusal.""" + self.setup_mnemonic_allallall() + fvk = self.client.zcash_get_orchard_fvk( + address_n=[H + 32, H + 133, H], account=0) + wrong_fp = bytearray(fvk.seed_fingerprint) + wrong_fp[0] ^= 0x01 + + responses = [] + original = self.client.call_raw + + def capture(request): + response = original(request) + responses.append(response) + return response + + self.client.call_raw = capture + try: + with pytest.raises(CallException) as caught: + self.client.zcash_sign_pczt( + **self._signing_request(), + expected_seed_fingerprint=bytes(wrong_fp), + ) + finally: + self.client.call_raw = original + + self.assertEqual(caught.value.args[0], proto_types.Failure_Other) + self.assertEqual(caught.value.args[1], + 'Seed fingerprint mismatch — wrong device') + self.assertFalse(any(isinstance(response, ( + zcash_proto.ZcashPCZTActionAck, + zcash_proto.ZcashSignedPCZT, + zcash_proto.ZcashTransparentSigned, + )) for response in responses), + 'a mismatched seed must not start action streaming or release signatures') if __name__ == '__main__':