from common import * from trezor import wire from trezor.wire import context from trezor.enums import EthereumDataType as EDT from trezor.messages import ( EthereumFieldType as EFT, EthereumStructMember as ESM, EthereumTypedDataStructAck as ETDSA, EthereumTypedDataValueAck, ) if not utils.BITCOIN_ONLY: from apps.ethereum.sign_typed_data import ( encode_field, _validate_value, validate_field_type, keccak256, TypedDataEnvelope, ) from apps.ethereum.helpers import ( get_type_name, decode_typed_data, ) class MockContext: """Simulating the client sending us data values.""" def __init__(self, message_contents: list): # TODO: it could be worth (for better readability and quicker modification) # to accept a whole EIP712 JSON object and create the list internally self.message_contents = message_contents self.next_response = b"" async def write(self, request) -> None: entry = self.message_contents for index in request.member_path: entry = entry[index] if isinstance(entry, list): self.next_response = len(entry).to_bytes(2, "big") else: self.next_response = entry async def read(self, _resp_types, _resp_type): return EthereumTypedDataValueAck(value=self.next_response) # Helper functions from trezorctl to build expected type data structures # TODO: it could be better to group these functions into a class, to visibly differentiate it def get_type_definitions(types: dict) -> dict: result = {} for struct, fields in types.items(): members = [] for name, type in fields: field_type = get_field_type(type, types) struct_member = ESM( type=field_type, name=name, ) members.append(struct_member) result[struct] = ETDSA(members=members) return result def get_field_type(type_name: str, types: dict) -> EFT: data_type = None size = None entry_type = None struct_name = None if is_array(type_name): data_type = EDT.ARRAY array_size = parse_array_n(type_name) size = None if array_size == "dynamic" else array_size member_typename = typeof_array(type_name) entry_type = get_field_type(member_typename, types) elif type_name.startswith("uint"): data_type = EDT.UINT size = get_byte_size_for_int_type(type_name) elif type_name.startswith("int"): data_type = EDT.INT size = get_byte_size_for_int_type(type_name) elif type_name.startswith("bytes"): data_type = EDT.BYTES size = None if type_name == "bytes" else parse_type_n(type_name) elif type_name == "string": data_type = EDT.STRING elif type_name == "bool": data_type = EDT.BOOL elif type_name == "address": data_type = EDT.ADDRESS elif type_name in types: data_type = EDT.STRUCT size = len(types[type_name]) struct_name = type_name else: raise ValueError(f"Unsupported type name: {type_name}") return EFT( data_type=data_type, size=size, entry_type=entry_type, struct_name=struct_name, ) def is_array(type_name: str) -> bool: return type_name[-1] == "]" def typeof_array(type_name: str) -> str: return type_name[: type_name.rindex("[")] def parse_type_n(type_name: str) -> int: """Parse N from type. Example: "uint256" -> 256 """ # STRANGE: "ImportError: no module named 're'" in Micropython? buf = "" for char in reversed(type_name): if char.isdigit(): buf += char else: return int("".join(reversed(buf))) def parse_array_n(type_name: str) -> Union[int, str]: """Parse N in type[] where "type" can itself be an array type.""" if type_name.endswith("[]"): return "dynamic" start_idx = type_name.rindex("[") + 1 return int(type_name[start_idx:-1]) def get_byte_size_for_int_type(int_type: str) -> int: return parse_type_n(int_type) // 8 types_basic = { "EIP712Domain": [ ("name", "string"), ("version", "string"), ("chainId", "uint256"), ("verifyingContract", "address"), ], "Person": [ ("name", "string"), ("wallet", "address"), ], "Mail": [ ("from", "Person"), ("to", "Person"), ("contents", "string"), ], } TYPES_BASIC = get_type_definitions(types_basic) types_complex = { "EIP712Domain": [ ("name", "string"), ("version", "string"), ("chainId", "uint256"), ("verifyingContract", "address"), ("salt", "bytes32"), ], "Person": [ ("name", "string"), ("wallet", "address"), ("married", "bool"), ("kids", "uint8"), ("karma", "int16"), ("secret", "bytes"), ("small_secret", "bytes16"), ("pets", "string[]"), ("two_best_friends", "string[2]"), ], "Mail": [ ("from", "Person"), ("to", "Person"), ("messages", "string[]"), ], } TYPES_COMPLEX = get_type_definitions(types_complex) DOMAIN_VALUES = [ [ b"Ether Mail", b"1", # 1 b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", # 0x1e0Ae8205e9726E6F296ab8869160A6423E2337E b"\x1e\n\xe8 ^\x97&\xe6\xf2\x96\xab\x88i\x16\nd#\xe23~", ] ] MESSAGE_VALUES_BASIC = [ [ [ b"Cow", b"\xc0\x00Kb\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4\xa4\xe5K\x15\xad", ], [ b"Bob", b"T\xb0\xfaf\xa0et\x8c@\xdc\xa2\xc7\xfe\x12Z (\xcf\x99\x82", ], b"Hello, Bob!", ] ] MESSAGE_VALUES_COMPLEX = [ [ [ b"Amy", b"\xc0\x00Kb\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4\xa4\xe5K\x15\xad", b"\x01", b"\x02", b"\x00\x04", b"b\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4b\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4b\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4b\xc5\xa3\x9ar\x8eJ\xf5\xbe\xe0\xc6\xb4", b"\\\xcf\x0eT6q\x04yZG\xbc\x04\x81d]\x9e", [ b"parrot", ], [ b"Carl", b"Denis", ], ], [ b"Bob", b"T\xb0\xfaf\xa0et\x8c@\xdc\xa2\xc7\xfe\x12Z (\xcf\x99\x82", b"\x00", b"\x00", b"\xff\xfc", b"\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9\x7f\xe1%\xa2\x02\x8c\xf9", b"\xa5\xe5\xc4{dwZ\xbcGm)b@2X\xde", [ b"dog", b"cat", ], [ b"Emil", b"Franz", ], ], [b"Hello, Bob!", b"How are you?", b"Hope you're fine"], ] ] # Object for testing functionality not needing context # (Each test needs to assign EMPTY_ENVELOPE.types as needed) EMPTY_ENVELOPE = TypedDataEnvelope( primary_type="test", metamask_v4_compat=True, ) # TODO: validate it more by some third party app, like signing data by Metamask # ??? How to approach the testing ??? # - we could copy the most important test cases testing important functionality # - could also download the eth-sig-util package together witn node.js and validating it # Testcases are at: # https://github.com/MetaMask/eth-sig-util/blob/73ace3309bf4b97d901fb66cd61db15eede7afe9/src/sign-typed-data.test.ts # Worth testing/implementing: # should encode data with a recursive data type # should ignore extra unspecified message properties # should throw an error when an atomic property is set to null # Missing custom type properties are omitted in V3, but encoded as 0 (bytes32) in V4 @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") class TestEthereumSignTypedData(unittest.TestCase): def test_hash_struct(self): """These final expected results should be generated by some third party""" VECTORS = ( # primary_type, data, types, expected ( # Generated by eth_account "EIP712Domain", [ [ b"Ether Mail", b"1", # 1 b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", # 0x1e0Ae8205e9726E6F296ab8869160A6423E2337E b"\x1e\n\xe8 ^\x97&\xe6\xf2\x96\xab\x88i\x16\nd#\xe23~", ] ], TYPES_BASIC, b"\x97\xd6\xf57t\xb8\x10\xfb\xda'\xe0\x91\xc0