Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support of EIP-712 into Ethereum app #1568

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions common/protob/messages-ethereum.proto
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,46 @@ message EthereumMessageSignature {
required string address = 3; // address used to sign the message
}


/**
* Request: Ask device to sign typed data
* @start
* @next EthereumTypedDataRequest
* @next Failure
*/
message EthereumSignTypedData {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool use_v4 = 2 [default=true]; // use EIP-712 (v4) for typed data signing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly does false mean, that v3 is used? From OP and metamask docs it appears that uint or enum field would make sense here if there's a chance of future versions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metamask has its own implementation history, all v1-v2-v3 were not compliant with the latest standard and should not be considered.

Their implementation v4 is widely adopted now, but still, has a bug described there:
MetaMask/eth-sig-util#107 (comment)

So at one day, it might be a v5.

I agree that having version enumeration is better than trying to maintain only a single or two "true" versions. I'm not a big fan of maintaining that as part of HW, but we have no choice there.

}

/**
* Response: Device asks for more values from typed data, or returns the signature and address.
* If member_path is not empty, device awaits the information for this value.
* Otherwise, the signature field contain the computed transaction signature.
* @end
* @next EthereumTypedDataAck
* @next Failure
*/
message EthereumTypedDataRequest {
repeated uint32 member_path = 1; // type or value member path requested by device
optional bool expect_type = 2; // if true, then device expects type info, otherwise value
optional bytes signature = 3; // signature of the message
optional string address = 4; // address used to sign the message
}

/**
* Request: Typed data value.
* If the member is a struct member_value should not be set. If the member is literal num_members should not be set.
* @next EthereumTypedDataRequest
*/
message EthereumTypedDataAck {
optional string member_name = 1; // if expect_type was true, should contain memeber name
optional string member_type = 2; // contains member type name
optional uint32 member_array_n = 3; // set to 0 when array is dynamic, None when member is not an array
optional uint32 member_children = 4; // if expect_type was true, should contain its children number
optional bytes member_value = 5; // if expect_type was false and value is not struct or array, should contain eth-abi encoded member value
}

/**
* Request: Ask device to verify message
* @start
Expand Down
3 changes: 3 additions & 0 deletions common/protob/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ enum MessageType {
MessageType_EthereumSignMessage = 64 [(wire_in) = true];
MessageType_EthereumVerifyMessage = 65 [(wire_in) = true];
MessageType_EthereumMessageSignature = 66 [(wire_out) = true];
MessageType_EthereumSignTypedData = 464 [(wire_in) = true];
MessageType_EthereumTypedDataRequest = 465 [(wire_out) = true];
MessageType_EthereumTypedDataAck = 466 [(wire_in) = true];

// NEM
MessageType_NEMGetAddress = 67 [(wire_in) = true];
Expand Down
1 change: 1 addition & 0 deletions core/src/apps/ethereum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ def boot() -> None:
wire.add(MessageType.EthereumGetPublicKey, __name__, "get_public_key")
wire.add(MessageType.EthereumSignTx, __name__, "sign_tx")
wire.add(MessageType.EthereumSignMessage, __name__, "sign_message")
wire.add(MessageType.EthereumSignTypedData, __name__, "sign_typed_data")
wire.add(MessageType.EthereumVerifyMessage, __name__, "verify_message")
308 changes: 308 additions & 0 deletions core/src/apps/ethereum/abi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@

def abi_encode_single(type_name, arg) -> bytes:
if type_name is "address":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use == for comparing values such as strings or numbers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a05275a

return abi_encode_single("uint160", parse_number(arg))
elif type_name is "bool":
return abi_encode_single("uint256", 1 if arg is True else 0)
elif type_name is "string":
return abi_encode_single("bytes", bytes(arg, encoding="utf8"))
elif is_array(type_name):
# this part handles fixed-length ([2]) and variable length ([]) arrays
if not isinstance(arg, list):
raise ValueError("not an array")

size = parse_array_n(type_name)
if not (size is "dynamic") and not (size is 0) and len(arg) > size:
raise ValueError("elements exceed array size: %d" % size)

ret = []
type_name = type_name[:type_name.rindex('[')]
for item in arg:
ret.append(abi_encode_single(type_name, item))

if size is "dynamic":
ret = [abi_encode_single("uint256", len(arg))] + ret

return b"".join(ret)
elif type_name is "bytes":
ret = bytearray(0)
ret.extend(abi_encode_single("uint256", len(arg)))
ret.extend(arg)

if not (len(arg) % 32 is 0):
zeros_padding = bytearray(32 - (len(arg) % 32))
ret = b"".join([ret, zeros_padding])

return ret
elif type_name.startswith("bytes"):
size = parse_type_n(type_name)
if size < 1 or size > 32:
raise ValueError("invalid bytes<N> width: %d" % size)
if not (isinstance(arg, bytes) or isinstance(arg, bytearray)):
raise ValueError("arg for bytes is not bytes")

return bytearray(set_length_right(arg, 32))
elif type_name.startswith("uint"):
size = parse_type_n(type_name)

if (not size % 8 is 0) or (size < 8) or (size > 256):
raise ValueError("invalid uint<N> width: %d" % size)

num = parse_number(arg)
if num < 0:
raise ValueError("supplied uint is negative")

return num.to_bytes(length=32, byteorder="big")
elif type_name.startswith("int"):
size = parse_type_n(type_name)

if (not size % 8 is 0) or (size < 8) or (size > 256):
raise ValueError("invalid int<N> width: %d" % size)

num = parse_number(arg)
return num.to_bytes(length=32, byteorder="big", signed=True)

raise ValueError("unsupported or invalid type: %s" % type_name)


def abi_encode(types: list, values: list) -> bytearray:
output = []
data = []
head_len = 0

for type_name in types:
if is_array(type_name):
size = parse_array_n(type_name)
if not (size is "dynamic"):
head_len += 32 * size
else:
head_len += 32
else:
head_len += 32

for i in range(0, len(types)):
type_name = types[i]
value = values[i]
buf = abi_encode_single(type_name, value)

if isinstance(buf, bytes):
raise ValueError("encoded {} with {} as bytes! HALT".format(type_name, value))

# use the head/tail method for storing dynamic data
if is_dynamic(type_name):
output.append(abi_encode_single("uint256", head_len))
data.append(buf)
head_len += len(buf)
else:
output.append(buf)

res = bytearray(0)
for x in output + data:
res.extend(x)

return res


def abi_decode(types: list, data: list, packed: bool = True) -> []:
ret = []
offset = 0

for i in range(0, len(types)):
parsed_type = parse_type(types[i])
ret.append(abi_decode_single(parsed_type["name"], data[i], packed, offset))
offset += parsed_type["memory_usage"]

return ret


def abi_decode_single(parsed_type: str, data: bytes, packed: bool = True, offset: int = 0):
if isinstance(parsed_type, str):
parsed_type = parse_type(parsed_type)

type_name = parsed_type["name"]
raw_type = parsed_type["raw_type"]

print("abi_decode_single", parsed_type, "data:", data, "offset:", offset)
print("type_name", type_name, "raw_type", raw_type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No print()s please.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, leftovers from debug :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted in a05275a


if type_name == "address":
value = abi_decode_single(raw_type, data, packed, offset)
return "0x%s" % value.to_bytes(20, "big").hex()

elif type_name == "bool":
value = abi_decode_single(raw_type, data, packed, offset)
if "%d" % value == "1":
return True
elif "%d" % value == "0":
return False
else:
raise ValueError("cannot decode bool value from {}".format(value))

elif type_name == "string":
value = abi_decode_single(raw_type, data, packed, offset)
return value.decode("utf8")

elif parsed_type["is_array"]:
# this part handles fixed-length arrays ([2]) and variable length ([]) arrays
ret = []
size = parsed_type["size"]

if size == "dynamic":
offset = abi_decode_single("uint256", data, packed, offset)
size = abi_decode_single("uint256", data, packed, offset)
offset += 32

sub_array = parsed_type["sub_array"]
for i in range(0, size):
decoded = abi_decode_single(sub_array, data, packed, offset)
ret.append(decoded)
offset += sub_array["memory_usage"]

return ret

elif type_name == "bytes":
if packed:
return data

offset = abi_decode_single("uint256", data, packed, offset)
size = abi_decode_single("uint256", data, packed, offset)
return data[offset + 32: offset + 32 + size]

elif type_name.startswith("bytes"):
return data[offset: offset + parsed_type["size"]]

elif type_name.startswith("uint"):
print("INT FROM_BYTES DATA", data, "offsetted:", data[offset: offset + 32])
return int.from_bytes(data[offset: offset + 32], "big")

elif type_name.startswith("int"):
return signed_int(data[offset: offset + 32], 256)

raise ValueError("unsupported or invalid type: %s" % type_name)


def parse_type(type_name) -> dict:
"""
Parse the given type

Returns dict containing the type itself, `memory_usage` and (including `size` and `sub_array` if applicable)
"""
ret = {
"name": type_name,
"is_array": False,
"raw_type": None,
"size": None,
"memory_usage": None,
"sub_array": None,
}

if is_array(type_name):
size = parse_array_n(type_name)
sub_array = parse_type(typeof_array(type_name))
ret["memory_usage"] = 32 if size == "dynamic" else sub_array["memory_usage"] * size,
ret["sub_array"] = sub_array
ret["is_array"] = True
return ret
else:
ret["memory_usage"] = 32

if type_name == "address":
ret["raw_type"] = "uint160"
elif type_name == "bool":
ret["raw_type"] = "uint256"
elif type_name == "string":
ret["raw_type"] = "bytes"

if (type_name.startswith("bytes") and type_name != "bytes") or \
type_name.startswith("uint") or type_name.startswith("int"):

size = parse_type_n(type_name)

if (type_name.startswith("bytes") and type_name != "bytes") and (size < 1 or size > 32):
raise ValueError("invalid bytes<N> width: %d" % size)

if (type_name.startswith("uint") or type_name.startswith("int")) and (size % 8 != 0 or size < 8 or size > 256):
raise ValueError("invalid int/uint<N> width: %d" % size)

ret["size"] = size

return ret


def set_length_right(msg: bytes, length) -> bytes:
"""
Pads a `msg` with zeros till it has `length` bytes.
Truncates the end of input if its length exceeds `length`.
"""
if len(msg) < length:
buf = bytearray(length)
buf[:len(msg)] = msg
return buf

return msg[:length]


def parse_type_n(type_name):
"""Parse N from type<N>"""
accum = []
for c in type_name:
if c.isdigit():
accum.append(c)
else:
accum = []

# join collected digits into a number
return int("".join(accum))


def parse_number(arg):
if isinstance(arg, str):
return int(arg, 16)
elif isinstance(arg, int):
return arg

raise ValueError("arg is not a number")


def is_array(type_name: str) -> bool:
if type_name:
return type_name[len(type_name) - 1] == ']'

return False


def typeof_array(type_name) -> str:
return type_name[:type_name.rindex('[')]


def parse_array_n(type_name: str):
"""Parse N in type[<N>] where "type" can itself be an array type."""
if type_name.endswith("[]"):
return "dynamic"

start_idx = type_name.rindex('[')+1
end_idx = len(type_name) - 1

return int(type_name[start_idx:end_idx])


def is_dynamic(type_name: str) -> bool:
if type_name is "string":
return True
elif type_name is "bytes":
return True
elif is_array(type_name) and parse_array_n(type_name) is "dynamic":
return True

return False


def signed_int(val, bits):
if type(val) is bytes:
val = int.from_bytes(val, "big")
elif type(val) is str:
val = int(val, 16)
if (val & (1 << (bits - 1))) != 0:
val = val - (1 << bits)
return val

Loading