1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-28 09:28:13 +00:00

feat(core): support displaying EIP-712 message hash during signing

This commit is contained in:
Michael de Hoog 2025-07-12 12:11:35 +10:00 committed by Vít Obrusník
parent 11bad835f1
commit 70ccc36ae4
13 changed files with 107 additions and 9 deletions

View File

@ -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)
}
/**

View File

@ -0,0 +1 @@
Add support for displaying the message hash when signing Ethereum EIP-712 typed data.

View File

@ -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;

View File

@ -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),

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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"
}

View File

@ -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": [
{

View File

@ -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):

View File

@ -36,6 +36,8 @@ pub struct EthereumSignTypedData {
pub metamask_v4_compat: ::std::option::Option<bool>,
// @@protoc_insertion_point(field:hw.trezor.messages.ethereum_eip712.EthereumSignTypedData.definitions)
pub definitions: ::protobuf::MessageField<super::messages_ethereum::EthereumDefinitions>,
// @@protoc_insertion_point(field:hw.trezor.messages.ethereum_eip712.EthereumSignTypedData.show_message_hash)
pub show_message_hash: ::std::option::Option<::std::vec::Vec<u8>>,
// 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<u8>) {
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<u8> {
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<u8> {
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>(
"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\