From 77ab865386b56f9ba0b33d31d17e3ddfebea3859 Mon Sep 17 00:00:00 2001 From: Alisina Bahadori Date: Tue, 30 Nov 2021 00:37:23 +0100 Subject: [PATCH] feat(legacy): Implement EIP-712 signing --- common/protob/messages-ethereum-eip712.proto | 9 --- common/protob/messages-ethereum.proto | 21 +++++++ common/protob/messages.proto | 1 + core/src/trezor/enums/MessageType.py | 1 + core/src/trezor/enums/__init__.py | 1 + core/src/trezor/messages.py | 50 ++++++++++----- legacy/firmware/.changelog.d/131.added | 1 + legacy/firmware/ethereum.c | 30 +++++++++ legacy/firmware/ethereum.h | 3 + legacy/firmware/fsm.h | 1 + legacy/firmware/fsm_msg_ethereum.h | 61 +++++++++++++++++++ legacy/firmware/layout2.c | 19 ++++++ legacy/firmware/layout2.h | 3 + legacy/firmware/protob/Makefile | 4 +- .../firmware/protob/messages-ethereum.options | 7 +++ python/.changelog.d/1970.added | 1 + python/src/trezorlib/cli/ethereum.py | 26 ++++++++ python/src/trezorlib/ethereum.py | 13 ++++ python/src/trezorlib/messages.py | 55 +++++++++++------ 19 files changed, 264 insertions(+), 43 deletions(-) create mode 100644 legacy/firmware/.changelog.d/131.added create mode 100644 python/.changelog.d/1970.added diff --git a/common/protob/messages-ethereum-eip712.proto b/common/protob/messages-ethereum-eip712.proto index 4cd23d648..48b2fb08a 100644 --- a/common/protob/messages-ethereum-eip712.proto +++ b/common/protob/messages-ethereum-eip712.proto @@ -87,12 +87,3 @@ message EthereumTypedDataValueAck { // * array types: number of elements, encoded as uint16. // * struct types: undefined, Trezor will not query a struct field. } - -/** - * Response: Signed typed data - * @end - */ -message EthereumTypedDataSignature { - required bytes signature = 1; // signature of the typed data - required string address = 2; // address used to sign the typed data -} diff --git a/common/protob/messages-ethereum.proto b/common/protob/messages-ethereum.proto index 8c779deda..1b0bd52bf 100644 --- a/common/protob/messages-ethereum.proto +++ b/common/protob/messages-ethereum.proto @@ -149,3 +149,24 @@ message EthereumVerifyMessage { required bytes message = 3; // message to verify required string address = 4; // address to verify } + +/** + * Request: Ask device to sign hash of typed data + * @start + * @next EthereumTypedDataSignature + * @next Failure + */ +message EthereumSignTypedHash { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes domain_separator_hash = 2; // Hash of domainSeparator of typed data to be signed + required bytes message_hash = 3; // Hash of the data of typed data to be signed +} + +/** + * Response: Signed typed data + * @end + */ +message EthereumTypedDataSignature { + required bytes signature = 1; // signature of the typed data + required string address = 2; // address used to sign the typed data +} diff --git a/common/protob/messages.proto b/common/protob/messages.proto index fa2f9fd63..4a7dbacaf 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -191,6 +191,7 @@ enum MessageType { MessageType_EthereumTypedDataValueRequest = 467 [(wire_out) = true]; MessageType_EthereumTypedDataValueAck = 468 [(wire_in) = true]; MessageType_EthereumTypedDataSignature = 469 [(wire_out) = true]; + MessageType_EthereumSignTypedHash = 470 [(wire_in) = true]; // NEM MessageType_NEMGetAddress = 67 [(wire_in) = true]; diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index 8d9ea143e..359c54506 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -103,6 +103,7 @@ if not utils.BITCOIN_ONLY: EthereumTypedDataValueRequest = 467 EthereumTypedDataValueAck = 468 EthereumTypedDataSignature = 469 + EthereumSignTypedHash = 470 NEMGetAddress = 67 NEMAddress = 68 NEMSignTx = 69 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 370bc2c56..aaf9e6efa 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -108,6 +108,7 @@ if TYPE_CHECKING: EthereumTypedDataValueRequest = 467 EthereumTypedDataValueAck = 468 EthereumTypedDataSignature = 469 + EthereumSignTypedHash = 470 NEMGetAddress = 67 NEMAddress = 68 NEMSignTx = 69 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index ee600be3a..24db97eb9 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -3235,6 +3235,40 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumVerifyMessage"]: return isinstance(msg, cls) + class EthereumSignTypedHash(protobuf.MessageType): + address_n: "list[int]" + domain_separator_hash: "bytes" + message_hash: "bytes" + + def __init__( + self, + *, + domain_separator_hash: "bytes", + message_hash: "bytes", + address_n: "list[int] | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumSignTypedHash"]: + return isinstance(msg, cls) + + class EthereumTypedDataSignature(protobuf.MessageType): + signature: "bytes" + address: "str" + + def __init__( + self, + *, + signature: "bytes", + address: "str", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumTypedDataSignature"]: + return isinstance(msg, cls) + class EthereumAccessList(protobuf.MessageType): address: "str" storage_keys: "list[bytes]" @@ -3325,22 +3359,6 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumTypedDataValueAck"]: return isinstance(msg, cls) - class EthereumTypedDataSignature(protobuf.MessageType): - signature: "bytes" - address: "str" - - def __init__( - self, - *, - signature: "bytes", - address: "str", - ) -> None: - pass - - @classmethod - def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumTypedDataSignature"]: - return isinstance(msg, cls) - class EthereumStructMember(protobuf.MessageType): type: "EthereumFieldType" name: "str" diff --git a/legacy/firmware/.changelog.d/131.added b/legacy/firmware/.changelog.d/131.added new file mode 100644 index 000000000..acfb00cb7 --- /dev/null +++ b/legacy/firmware/.changelog.d/131.added @@ -0,0 +1 @@ +Support for blindly signing EIP-712 data. diff --git a/legacy/firmware/ethereum.c b/legacy/firmware/ethereum.c index 437aef573..94563eb2f 100644 --- a/legacy/firmware/ethereum.c +++ b/legacy/firmware/ethereum.c @@ -976,6 +976,36 @@ int ethereum_message_verify(const EthereumVerifyMessage *msg) { return 0; } +static void ethereum_typed_hash(const uint8_t domain_separator_hash[32], + const uint8_t message_hash[32], + uint8_t hash[32]) { + struct SHA3_CTX ctx = {0}; + sha3_256_Init(&ctx); + sha3_Update(&ctx, (const uint8_t *)"\x19\x01", 2); + sha3_Update(&ctx, domain_separator_hash, 32); + sha3_Update(&ctx, message_hash, 32); + keccak_Final(&ctx, hash); +} + +void ethereum_typed_hash_sign(const EthereumSignTypedHash *msg, + const HDNode *node, + EthereumTypedDataSignature *resp) { + uint8_t hash[32] = {0}; + ethereum_typed_hash(msg->domain_separator_hash.bytes, msg->message_hash.bytes, + hash); + + uint8_t v = 0; + if (ecdsa_sign_digest(&secp256k1, node->private_key, hash, + resp->signature.bytes, &v, ethereum_is_canonic) != 0) { + fsm_sendFailure(FailureType_Failure_ProcessError, _("Signing failed")); + return; + } + + resp->signature.bytes[64] = 27 + v; + resp->signature.size = 65; + msg_write(MessageType_MessageType_EthereumTypedDataSignature, resp); +} + bool ethereum_parse(const char *address, uint8_t pubkeyhash[20]) { memzero(pubkeyhash, 20); size_t len = strlen(address); diff --git a/legacy/firmware/ethereum.h b/legacy/firmware/ethereum.h index 2dd406e7b..feb287925 100644 --- a/legacy/firmware/ethereum.h +++ b/legacy/firmware/ethereum.h @@ -34,6 +34,9 @@ void ethereum_signing_txack(const EthereumTxAck *msg); void ethereum_message_sign(const EthereumSignMessage *msg, const HDNode *node, EthereumMessageSignature *resp); int ethereum_message_verify(const EthereumVerifyMessage *msg); +void ethereum_typed_hash_sign(const EthereumSignTypedHash *msg, + const HDNode *node, + EthereumTypedDataSignature *resp); bool ethereum_parse(const char *address, uint8_t pubkeyhash[20]); #endif diff --git a/legacy/firmware/fsm.h b/legacy/firmware/fsm.h index 57a5e7ece..8c65dbfab 100644 --- a/legacy/firmware/fsm.h +++ b/legacy/firmware/fsm.h @@ -104,6 +104,7 @@ void fsm_msgEthereumSignTxEIP1559(const EthereumSignTxEIP1559 *msg); void fsm_msgEthereumTxAck(const EthereumTxAck *msg); void fsm_msgEthereumSignMessage(const EthereumSignMessage *msg); void fsm_msgEthereumVerifyMessage(const EthereumVerifyMessage *msg); +void fsm_msgEthereumSignTypedHash(const EthereumSignTypedHash *msg); // nem void fsm_msgNEMGetAddress( diff --git a/legacy/firmware/fsm_msg_ethereum.h b/legacy/firmware/fsm_msg_ethereum.h index e95637de8..638e5523f 100644 --- a/legacy/firmware/fsm_msg_ethereum.h +++ b/legacy/firmware/fsm_msg_ethereum.h @@ -212,3 +212,64 @@ void fsm_msgEthereumVerifyMessage(const EthereumVerifyMessage *msg) { layoutHome(); } + +void fsm_msgEthereumSignTypedHash(const EthereumSignTypedHash *msg) { + RESP_INIT(EthereumTypedDataSignature); + + CHECK_INITIALIZED + + CHECK_PIN + + if (msg->domain_separator_hash.size != 32 || msg->message_hash.size != 32) { + fsm_sendFailure(FailureType_Failure_DataError, _("Invalid hash length")); + return; + } + + layoutDialogSwipe(&bmp_icon_warning, _("Abort"), _("Continue"), NULL, + _("Unable to show"), _("EIP-712 data."), NULL, + _("Sign at your own risk."), NULL, NULL); + if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + const HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + uint8_t pubkeyhash[20] = {0}; + if (!hdnode_get_ethereum_pubkeyhash(node, pubkeyhash)) { + return; + } + + resp->address[0] = '0'; + resp->address[1] = 'x'; + ethereum_address_checksum(pubkeyhash, resp->address + 2, false, 0); + // ethereum_address_checksum adds trailing zero + + layoutVerifyAddress(NULL, resp->address); + if (!protectButton(ButtonRequestType_ButtonRequest_Other, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + layoutConfirmHash(&bmp_icon_warning, _("EIP-712 domain hash"), + msg->domain_separator_hash.bytes, 32); + if (!protectButton(ButtonRequestType_ButtonRequest_Other, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + layoutConfirmHash(&bmp_icon_warning, _("EIP-712 message hash"), + msg->message_hash.bytes, 32); + if (!protectButton(ButtonRequestType_ButtonRequest_Other, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + ethereum_typed_hash_sign(msg, node, resp); + layoutHome(); +} diff --git a/legacy/firmware/layout2.c b/legacy/firmware/layout2.c index 820053c5e..89daa1a26 100644 --- a/legacy/firmware/layout2.c +++ b/legacy/firmware/layout2.c @@ -1253,3 +1253,22 @@ void layoutConfirmSafetyChecks(SafetyCheckLevel safety_ckeck_level) { _("be unsafe?"), NULL); } } + +void layoutConfirmHash(const BITMAP *icon, const char *description, + const uint8_t *hash, uint32_t len) { + const char **str = split_message_hex(hash, len); + + layoutSwipe(); + oledClear(); + oledDrawBitmap(0, 0, icon); + oledDrawString(20, 0 * 9, description, FONT_STANDARD); + oledDrawString(20, 1 * 9, str[0], FONT_FIXED); + oledDrawString(20, 2 * 9, str[1], FONT_FIXED); + oledDrawString(20, 3 * 9, str[2], FONT_FIXED); + oledDrawString(20, 4 * 9, str[3], FONT_FIXED); + oledHLine(OLED_HEIGHT - 13); + + layoutButtonNo(_("Cancel"), &bmp_btn_cancel); + layoutButtonYes(_("Confirm"), &bmp_btn_confirm); + oledRefresh(); +} diff --git a/legacy/firmware/layout2.h b/legacy/firmware/layout2.h index 6c2869258..428d72562 100644 --- a/legacy/firmware/layout2.h +++ b/legacy/firmware/layout2.h @@ -110,6 +110,9 @@ void layoutCosiCommitSign(const uint32_t *address_n, size_t address_n_count, void layoutConfirmAutoLockDelay(uint32_t delay_ms); void layoutConfirmSafetyChecks(SafetyCheckLevel safety_checks_level); +void layoutConfirmHash(const BITMAP *icon, const char *description, + const uint8_t *hash, uint32_t len); + const char **split_message(const uint8_t *msg, uint32_t len, uint32_t rowlen); const char **split_message_hex(const uint8_t *msg, uint32_t len); diff --git a/legacy/firmware/protob/Makefile b/legacy/firmware/protob/Makefile index 8b45ac290..6f7a020bf 100644 --- a/legacy/firmware/protob/Makefile +++ b/legacy/firmware/protob/Makefile @@ -6,7 +6,9 @@ SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdPro DebugLinkRecordScreen DebugLinkReseedRandom DebugLinkShowText DebugLinkEraseSdCard DebugLinkWatchLayout \ GetOwnershipProof OwnershipProof GetOwnershipId OwnershipId AuthorizeCoinJoin DoPreauthorized \ CancelAuthorization DebugLinkLayout \ - TxAckInput TxAckOutput TxAckPrev EthereumSignTypedData EthereumTypedData + TxAckInput TxAckOutput TxAckPrev \ + EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \ + EthereumTypedDataValueRequest EthereumTypedDataValueAck ifeq ($(BITCOIN_ONLY), 1) SKIPPED_MESSAGES += Ethereum NEM Stellar diff --git a/legacy/firmware/protob/messages-ethereum.options b/legacy/firmware/protob/messages-ethereum.options index ead566378..e90950d30 100644 --- a/legacy/firmware/protob/messages-ethereum.options +++ b/legacy/firmware/protob/messages-ethereum.options @@ -34,6 +34,13 @@ EthereumVerifyMessage.message max_size:1024 EthereumMessageSignature.address max_size:43 EthereumMessageSignature.signature max_size:65 +EthereumSignTypedHash.address_n max_count:8 +EthereumSignTypedHash.domain_separator_hash max_size:32 +EthereumSignTypedHash.message_hash max_size:32 + +EthereumTypedDataSignature.address max_size:43 +EthereumTypedDataSignature.signature max_size:65 + EthereumGetAddress.address_n max_count:8 EthereumGetPublicKey.address_n max_count:8 diff --git a/python/.changelog.d/1970.added b/python/.changelog.d/1970.added new file mode 100644 index 000000000..d091c6d5e --- /dev/null +++ b/python/.changelog.d/1970.added @@ -0,0 +1 @@ +Add support for blind EIP-712 signing for Trezor One diff --git a/python/src/trezorlib/cli/ethereum.py b/python/src/trezorlib/cli/ethereum.py index 12af45c30..7d5d4c779 100644 --- a/python/src/trezorlib/cli/ethereum.py +++ b/python/src/trezorlib/cli/ethereum.py @@ -441,3 +441,29 @@ def verify_message( """Verify message signed with Ethereum address.""" signature_bytes = ethereum.decode_hex(signature) return ethereum.verify_message(client, address, signature_bytes, message) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.argument("domain_hash_hex") +@click.argument("message_hash_hex") +@with_client +def sign_typed_data_hash( + client: "TrezorClient", address: str, domain_hash_hex: str, message_hash_hex: str +) -> Dict[str, str]: + """ + Sign hash of typed data (EIP-712) with Ethereum address. + + For T1 backward compatibility. + """ + address_n = tools.parse_path(address) + domain_hash = ethereum.decode_hex(domain_hash_hex) + message_hash = ethereum.decode_hex(message_hash_hex) + ret = ethereum.sign_typed_data_hash(client, address_n, domain_hash, message_hash) + output = { + "domain_hash": domain_hash_hex, + "message_hash": message_hash_hex, + "address": ret.address, + "signature": f"0x{ret.signature.hex()}", + } + return output diff --git a/python/src/trezorlib/ethereum.py b/python/src/trezorlib/ethereum.py index fe0bbe6ec..bc3147e1d 100644 --- a/python/src/trezorlib/ethereum.py +++ b/python/src/trezorlib/ethereum.py @@ -357,3 +357,16 @@ def verify_message( except exceptions.TrezorFailure: return False return isinstance(resp, messages.Success) + + +@expect(messages.EthereumTypedDataSignature) +def sign_typed_data_hash( + client: "TrezorClient", n: "Address", domain_hash: bytes, message_hash: bytes +) -> "MessageType": + return client.call( + messages.EthereumSignTypedHash( + address_n=n, + domain_separator_hash=domain_hash, + message_hash=message_hash, + ) + ) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index c6ad0520c..809699544 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -129,6 +129,7 @@ class MessageType(IntEnum): EthereumTypedDataValueRequest = 467 EthereumTypedDataValueAck = 468 EthereumTypedDataSignature = 469 + EthereumSignTypedHash = 470 NEMGetAddress = 67 NEMAddress = 68 NEMSignTx = 69 @@ -4455,23 +4456,6 @@ class EthereumTypedDataValueAck(protobuf.MessageType): self.value = value -class EthereumTypedDataSignature(protobuf.MessageType): - MESSAGE_WIRE_TYPE = 469 - FIELDS = { - 1: protobuf.Field("signature", "bytes", repeated=False, required=True), - 2: protobuf.Field("address", "string", repeated=False, required=True), - } - - def __init__( - self, - *, - signature: "bytes", - address: "str", - ) -> None: - self.signature = signature - self.address = address - - class EthereumStructMember(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { @@ -4756,6 +4740,43 @@ class EthereumVerifyMessage(protobuf.MessageType): self.address = address +class EthereumSignTypedHash(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 470 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("domain_separator_hash", "bytes", repeated=False, required=True), + 3: protobuf.Field("message_hash", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + domain_separator_hash: "bytes", + message_hash: "bytes", + address_n: Optional[Sequence["int"]] = None, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.domain_separator_hash = domain_separator_hash + self.message_hash = message_hash + + +class EthereumTypedDataSignature(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 469 + FIELDS = { + 1: protobuf.Field("signature", "bytes", repeated=False, required=True), + 2: protobuf.Field("address", "string", repeated=False, required=True), + } + + def __init__( + self, + *, + signature: "bytes", + address: "str", + ) -> None: + self.signature = signature + self.address = address + + class EthereumAccessList(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = {