From 70ccc36ae4c85a690af01bc6329cad42f86d082a Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Sat, 12 Jul 2025 12:11:35 +1000 Subject: [PATCH] feat(core): support displaying EIP-712 message hash during signing --- common/protob/messages-ethereum-eip712.proto | 1 + core/.changelog.d/5344.added | 1 + core/embed/rust/librust_qstr.h | 1 + .../generated/translated_string.rs | 6 ++ core/mocks/trezortranslate_keys.pyi | 1 + core/src/apps/ethereum/layout.py | 18 ++++++ core/src/apps/ethereum/sign_typed_data.py | 12 +++- core/src/trezor/messages.py | 2 + core/translations/en.json | 1 + core/translations/order.json | 3 +- core/translations/signatures.json | 6 +- python/src/trezorlib/messages.py | 3 + .../generated/messages_ethereum_eip712.rs | 61 ++++++++++++++++++- 13 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 core/.changelog.d/5344.added diff --git a/common/protob/messages-ethereum-eip712.proto b/common/protob/messages-ethereum-eip712.proto index 63eb67b52d..95e515ffd5 100644 --- a/common/protob/messages-ethereum-eip712.proto +++ b/common/protob/messages-ethereum-eip712.proto @@ -26,6 +26,7 @@ message EthereumSignTypedData { required string primary_type = 2; // name of the root message struct optional bool metamask_v4_compat = 3 [default=true]; // use MetaMask v4 (see https://github.com/MetaMask/eth-sig-util/issues/106) optional ethereum.EthereumDefinitions definitions = 4; // network and/or token definitions + optional bytes show_message_hash = 5; // hash of the typed data to be signed (if set, user will be asked to confirm before signing) } /** diff --git a/core/.changelog.d/5344.added b/core/.changelog.d/5344.added new file mode 100644 index 0000000000..3e9e79123c --- /dev/null +++ b/core/.changelog.d/5344.added @@ -0,0 +1 @@ +Add support for displaying the message hash when signing Ethereum EIP-712 typed data. diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 665612fe7d..948d23ffe5 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -1105,6 +1105,7 @@ static void _librust_qstrs(void) { MP_QSTR_ethereum__title_all_input_data_template; MP_QSTR_ethereum__title_confirm_domain; MP_QSTR_ethereum__title_confirm_message; + MP_QSTR_ethereum__title_confirm_message_hash; MP_QSTR_ethereum__title_confirm_struct; MP_QSTR_ethereum__title_confirm_typed_data; MP_QSTR_ethereum__title_input_data; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 7a865c3831..189d175f0c 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -1450,6 +1450,8 @@ pub enum TranslatedString { address__title_provider_address = 1053, // "Provider address" address__title_refund_address = 1054, // "Refund address" words__assets = 1055, // "Assets" + #[cfg(feature = "universal_fw")] + ethereum__title_confirm_message_hash = 1056, // "Confirm message hash" } impl TranslatedString { @@ -3212,6 +3214,8 @@ impl TranslatedString { (Self::address__title_provider_address, "Provider address"), (Self::address__title_refund_address, "Refund address"), (Self::words__assets, "Assets"), + #[cfg(feature = "universal_fw")] + (Self::ethereum__title_confirm_message_hash, "Confirm message hash"), ]; #[cfg(feature = "micropython")] @@ -3738,6 +3742,8 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] (Qstr::MP_QSTR_ethereum__title_confirm_message, Self::ethereum__title_confirm_message), #[cfg(feature = "universal_fw")] + (Qstr::MP_QSTR_ethereum__title_confirm_message_hash, Self::ethereum__title_confirm_message_hash), + #[cfg(feature = "universal_fw")] (Qstr::MP_QSTR_ethereum__title_confirm_struct, Self::ethereum__title_confirm_struct), #[cfg(feature = "universal_fw")] (Qstr::MP_QSTR_ethereum__title_confirm_typed_data, Self::ethereum__title_confirm_typed_data), diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 794f35bc54..8ff2e9a2f6 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -330,6 +330,7 @@ class TR: ethereum__title_all_input_data_template: str = "All input data ({0} bytes)" ethereum__title_confirm_domain: str = "Confirm domain" ethereum__title_confirm_message: str = "Confirm message" + ethereum__title_confirm_message_hash: str = "Confirm message hash" ethereum__title_confirm_struct: str = "Confirm struct" ethereum__title_confirm_typed_data: str = "Confirm typed data" ethereum__title_input_data: str = "Input data" diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 70075b536e..c6ea0310dd 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -305,6 +305,24 @@ def require_confirm_other_data(data: bytes, data_total: int) -> Awaitable[None]: ) +async def confirm_message_hash(message_hash: bytes) -> None: + from ubinascii import hexlify + + from trezor.ui.layouts import confirm_value + + message_hash_hex = "0x" + hexlify(message_hash).decode() + + await confirm_value( + TR.ethereum__title_confirm_message_hash, + message_hash_hex, + "", + "confirm_message_hash", + verb=TR.buttons__confirm, + br_code=ButtonRequestType.SignTx, + cancel=True, + ) + + async def confirm_typed_data_final() -> None: from trezor.ui.layouts import confirm_action diff --git a/core/src/apps/ethereum/sign_typed_data.py b/core/src/apps/ethereum/sign_typed_data.py index 3256c0a4ac..b8c1fe4ed7 100644 --- a/core/src/apps/ethereum/sign_typed_data.py +++ b/core/src/apps/ethereum/sign_typed_data.py @@ -45,7 +45,7 @@ async def sign_typed_data( await require_confirm_address(address_bytes) data_hash = await _generate_typed_data_hash( - msg.primary_type, msg.metamask_v4_compat + msg.primary_type, msg.metamask_v4_compat, msg.show_message_hash ) signature = secp256k1.sign( @@ -59,7 +59,9 @@ async def sign_typed_data( async def _generate_typed_data_hash( - primary_type: str, metamask_v4_compat: bool = True + primary_type: str, + metamask_v4_compat: bool = True, + show_message_hash: bytes | None = None, ) -> bytes: """ Generate typed data hash according to EIP-712 specification @@ -71,6 +73,7 @@ async def _generate_typed_data_hash( from .layout import ( confirm_empty_typed_message, + confirm_message_hash, confirm_typed_data_final, should_show_domain, ) @@ -110,6 +113,11 @@ async def _generate_typed_data_hash( [primary_type], ) + if show_message_hash is not None: + if message_hash != show_message_hash: + raise DataError("Message hash mismatch") + await confirm_message_hash(message_hash) + await confirm_typed_data_final() return keccak256(b"\x19\x01" + domain_separator + message_hash) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 2cd2a8158a..2da75ea1c1 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -3877,6 +3877,7 @@ if TYPE_CHECKING: primary_type: "str" metamask_v4_compat: "bool" definitions: "EthereumDefinitions | None" + show_message_hash: "bytes | None" def __init__( self, @@ -3885,6 +3886,7 @@ if TYPE_CHECKING: address_n: "list[int] | None" = None, metamask_v4_compat: "bool | None" = None, definitions: "EthereumDefinitions | None" = None, + show_message_hash: "bytes | None" = None, ) -> None: pass diff --git a/core/translations/en.json b/core/translations/en.json index f10f665de9..788aca5f33 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -371,6 +371,7 @@ "ethereum__title_all_input_data_template": "All input data ({0} bytes)", "ethereum__title_confirm_domain": "Confirm domain", "ethereum__title_confirm_message": "Confirm message", + "ethereum__title_confirm_message_hash": "Confirm message hash", "ethereum__title_confirm_struct": "Confirm struct", "ethereum__title_confirm_typed_data": "Confirm typed data", "ethereum__title_input_data": "Input data", diff --git a/core/translations/order.json b/core/translations/order.json index 1e3c13183f..2e09694791 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -1054,5 +1054,6 @@ "1052": "words__swap", "1053": "address__title_provider_address", "1054": "address__title_refund_address", - "1055": "words__assets" + "1055": "words__assets", + "1056": "ethereum__title_confirm_message_hash" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index bf74e1d6ce..7bc5489286 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "e2482ef9c1fe4243754ccd9898b3c05ab854d59bf5b9afae67630262bdc47d4e", - "datetime": "2025-07-14T09:23:54.196279+00:00", - "commit": "265104d00d8ddbc47753055021ae67fffa67423f" + "merkle_root": "aa1b72313fa6ff028138ae1e6f26362107ffa41f4d745aa4170036a15b6d7a6f", + "datetime": "2025-07-17T02:01:07.922808+00:00", + "commit": "6cc9c629414e9cf8bc66b22580555c098e0fb3eb" }, "history": [ { diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 6a28496cc4..e43a43ecd2 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -5293,6 +5293,7 @@ class EthereumSignTypedData(protobuf.MessageType): 2: protobuf.Field("primary_type", "string", repeated=False, required=True), 3: protobuf.Field("metamask_v4_compat", "bool", repeated=False, required=False, default=True), 4: protobuf.Field("definitions", "EthereumDefinitions", repeated=False, required=False, default=None), + 5: protobuf.Field("show_message_hash", "bytes", repeated=False, required=False, default=None), } def __init__( @@ -5302,11 +5303,13 @@ class EthereumSignTypedData(protobuf.MessageType): address_n: Optional[Sequence["int"]] = None, metamask_v4_compat: Optional["bool"] = True, definitions: Optional["EthereumDefinitions"] = None, + show_message_hash: Optional["bytes"] = None, ) -> None: self.address_n: Sequence["int"] = address_n if address_n is not None else [] self.primary_type = primary_type self.metamask_v4_compat = metamask_v4_compat self.definitions = definitions + self.show_message_hash = show_message_hash class EthereumTypedDataStructRequest(protobuf.MessageType): diff --git a/rust/trezor-client/src/protos/generated/messages_ethereum_eip712.rs b/rust/trezor-client/src/protos/generated/messages_ethereum_eip712.rs index 17e8478f03..5460564755 100644 --- a/rust/trezor-client/src/protos/generated/messages_ethereum_eip712.rs +++ b/rust/trezor-client/src/protos/generated/messages_ethereum_eip712.rs @@ -36,6 +36,8 @@ pub struct EthereumSignTypedData { pub metamask_v4_compat: ::std::option::Option, // @@protoc_insertion_point(field:hw.trezor.messages.ethereum_eip712.EthereumSignTypedData.definitions) pub definitions: ::protobuf::MessageField, + // @@protoc_insertion_point(field:hw.trezor.messages.ethereum_eip712.EthereumSignTypedData.show_message_hash) + pub show_message_hash: ::std::option::Option<::std::vec::Vec>, // special fields // @@protoc_insertion_point(special_field:hw.trezor.messages.ethereum_eip712.EthereumSignTypedData.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -107,8 +109,44 @@ impl EthereumSignTypedData { self.metamask_v4_compat = ::std::option::Option::Some(v); } + // optional bytes show_message_hash = 5; + + pub fn show_message_hash(&self) -> &[u8] { + match self.show_message_hash.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_show_message_hash(&mut self) { + self.show_message_hash = ::std::option::Option::None; + } + + pub fn has_show_message_hash(&self) -> bool { + self.show_message_hash.is_some() + } + + // Param is passed by value, moved + pub fn set_show_message_hash(&mut self, v: ::std::vec::Vec) { + self.show_message_hash = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_show_message_hash(&mut self) -> &mut ::std::vec::Vec { + if self.show_message_hash.is_none() { + self.show_message_hash = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.show_message_hash.as_mut().unwrap() + } + + // Take field + pub fn take_show_message_hash(&mut self) -> ::std::vec::Vec { + self.show_message_hash.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); + let mut fields = ::std::vec::Vec::with_capacity(5); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( "address_n", @@ -130,6 +168,11 @@ impl EthereumSignTypedData { |m: &EthereumSignTypedData| { &m.definitions }, |m: &mut EthereumSignTypedData| { &mut m.definitions }, )); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "show_message_hash", + |m: &EthereumSignTypedData| { &m.show_message_hash }, + |m: &mut EthereumSignTypedData| { &mut m.show_message_hash }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "EthereumSignTypedData", fields, @@ -171,6 +214,9 @@ impl ::protobuf::Message for EthereumSignTypedData { 34 => { ::protobuf::rt::read_singular_message_into_field(is, &mut self.definitions)?; }, + 42 => { + self.show_message_hash = ::std::option::Option::Some(is.read_bytes()?); + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -196,6 +242,9 @@ impl ::protobuf::Message for EthereumSignTypedData { let len = v.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; } + if let Some(v) = self.show_message_hash.as_ref() { + my_size += ::protobuf::rt::bytes_size(5, &v); + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -214,6 +263,9 @@ impl ::protobuf::Message for EthereumSignTypedData { if let Some(v) = self.definitions.as_ref() { ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; } + if let Some(v) = self.show_message_hash.as_ref() { + os.write_bytes(5, v)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -235,6 +287,7 @@ impl ::protobuf::Message for EthereumSignTypedData { self.primary_type = ::std::option::Option::None; self.metamask_v4_compat = ::std::option::Option::None; self.definitions.clear(); + self.show_message_hash = ::std::option::Option::None; self.special_fields.clear(); } @@ -244,6 +297,7 @@ impl ::protobuf::Message for EthereumSignTypedData { primary_type: ::std::option::Option::None, metamask_v4_compat: ::std::option::Option::None, definitions: ::protobuf::MessageField::none(), + show_message_hash: ::std::option::Option::None, special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -1399,12 +1453,13 @@ impl ::protobuf::reflect::ProtobufValue for EthereumTypedDataValueAck { static file_descriptor_proto_data: &'static [u8] = b"\ \n\x1emessages-ethereum-eip712.proto\x12\"hw.trezor.messages.ethereum_ei\ - p712\x1a\x17messages-ethereum.proto\"\xdf\x01\n\x15EthereumSignTypedData\ + p712\x1a\x17messages-ethereum.proto\"\x8b\x02\n\x15EthereumSignTypedData\ \x12\x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\x12!\n\x0cprimary_\ type\x18\x02\x20\x02(\tR\x0bprimaryType\x122\n\x12metamask_v4_compat\x18\ \x03\x20\x01(\x08:\x04trueR\x10metamaskV4Compat\x12R\n\x0bdefinitions\ \x18\x04\x20\x01(\x0b20.hw.trezor.messages.ethereum.EthereumDefinitionsR\ - \x0bdefinitions\"4\n\x1eEthereumTypedDataStructRequest\x12\x12\n\x04name\ + \x0bdefinitions\x12*\n\x11show_message_hash\x18\x05\x20\x01(\x0cR\x0fsho\ + wMessageHash\"4\n\x1eEthereumTypedDataStructRequest\x12\x12\n\x04name\ \x18\x01\x20\x02(\tR\x04name\"\xb4\x05\n\x1aEthereumTypedDataStructAck\ \x12m\n\x07members\x18\x01\x20\x03(\x0b2S.hw.trezor.messages.ethereum_ei\ p712.EthereumTypedDataStructAck.EthereumStructMemberR\x07members\x1a\x90\