from common import * from trezor import wire from trezor.messages import EthereumTypedDataStructAck as ETDSA from trezor.messages import EthereumStructMember as ESM from trezor.messages import EthereumFieldType as EFT from trezor.messages import EthereumTypedDataValueAck from trezor.enums import EthereumDataType as EDT 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 async def call(self, request, _resp_type) -> bytes: entry = self.message_contents for index in request.member_path: entry = entry[index] if isinstance(entry, list): value = len(entry).to_bytes(2, "big") else: value = entry return EthereumTypedDataValueAck(value=value) # 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( ctx=None, 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