From 08ba57b18ffdc6b0de3d6cacb4f56217d58c5c03 Mon Sep 17 00:00:00 2001 From: Niklas Claesson Date: Mon, 3 Feb 2020 16:46:26 +0100 Subject: [PATCH] ETH: Add message signing --- messages/eth.options | 2 + messages/eth.proto | 7 + py/bitbox02/bitbox02/bitbox02/bitbox02.py | 10 + .../communication/generated/eth_pb2.py | 83 ++++++++- .../communication/generated/eth_pb2.pyi | 34 +++- py/send_message.py | 18 +- src/CMakeLists.txt | 1 + src/apps/eth/eth_sign_msg.c | 127 +++++++++++++ src/apps/eth/eth_sign_msg.h | 26 +++ src/commander/commander_eth.c | 18 ++ test/unit-test/CMakeLists.txt | 2 + test/unit-test/test_app_eth_sign_msg.c | 171 ++++++++++++++++++ 12 files changed, 484 insertions(+), 15 deletions(-) create mode 100644 src/apps/eth/eth_sign_msg.c create mode 100644 src/apps/eth/eth_sign_msg.h create mode 100644 test/unit-test/test_app_eth_sign_msg.c diff --git a/messages/eth.options b/messages/eth.options index 0f9bf2438..79e9eddec 100644 --- a/messages/eth.options +++ b/messages/eth.options @@ -21,4 +21,6 @@ ETHSignRequest.gas_limit max_size:16; ETHSignRequest.recipient fixed_length:true max_size:20 ETHSignRequest.value max_size:32 ETHSignRequest.data max_size:1024 +ETHSignMessageRequest.msg max_size:1024 +ETHSignMessageRequest.keypath max_count:10 ETHSignResponse.signature fixed_length:true max_size:65 diff --git a/messages/eth.proto b/messages/eth.proto index 98c49d5f4..99fc04612 100644 --- a/messages/eth.proto +++ b/messages/eth.proto @@ -45,6 +45,12 @@ message ETHSignRequest { bytes data = 8; } +message ETHSignMessageRequest { + ETHCoin coin = 1; + repeated uint32 keypath = 2; + bytes msg = 3; +} + message ETHSignResponse { bytes signature = 1; // 65 bytes, last byte is the recid } @@ -53,6 +59,7 @@ message ETHRequest { oneof request { ETHPubRequest pub = 1; ETHSignRequest sign = 2; + ETHSignMessageRequest sign_msg = 3; } } diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py index 43ac44041..6695c85d4 100644 --- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py +++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py @@ -475,6 +475,16 @@ def eth_sign( ) return self._eth_msg_query(request, expected_response="sign").sign.signature + def eth_sign_msg(self, msg: bytes, keypath: List[int], coin: eth.ETHCoin = eth.ETH) -> bytes: + """ + Signs message, the msg will be prefixed with "\x19Ethereum message\n" + len(msg) in the + hardware + """ + request = eth.ETHRequest() + # pylint: disable=no-member + request.sign_msg.CopyFrom(eth.ETHSignMessageRequest(coin=coin, keypath=keypath, msg=msg)) + return self._eth_msg_query(request, expected_response="sign").sign.signature + def reset(self) -> bool: """ Factory reset the device. Returns True on success. diff --git a/py/bitbox02/bitbox02/communication/generated/eth_pb2.py b/py/bitbox02/bitbox02/communication/generated/eth_pb2.py index e9efd2be3..840fe5387 100644 --- a/py/bitbox02/bitbox02/communication/generated/eth_pb2.py +++ b/py/bitbox02/bitbox02/communication/generated/eth_pb2.py @@ -21,7 +21,7 @@ name='eth.proto', package='', syntax='proto3', - serialized_pb=_b('\n\teth.proto\x1a\x0c\x63ommon.proto\"\xb8\x01\n\rETHPubRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12\x16\n\x04\x63oin\x18\x02 \x01(\x0e\x32\x08.ETHCoin\x12.\n\x0boutput_type\x18\x03 \x01(\x0e\x32\x19.ETHPubRequest.OutputType\x12\x0f\n\x07\x64isplay\x18\x04 \x01(\x08\x12\x18\n\x10\x63ontract_address\x18\x05 \x01(\x0c\"#\n\nOutputType\x12\x0b\n\x07\x41\x44\x44RESS\x10\x00\x12\x08\n\x04XPUB\x10\x01\"\x9e\x01\n\x0e\x45THSignRequest\x12\x16\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x08.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x11\n\tgas_price\x18\x04 \x01(\x0c\x12\x11\n\tgas_limit\x18\x05 \x01(\x0c\x12\x11\n\trecipient\x18\x06 \x01(\x0c\x12\r\n\x05value\x18\x07 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\"$\n\x0f\x45THSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"W\n\nETHRequest\x12\x1d\n\x03pub\x18\x01 \x01(\x0b\x32\x0e.ETHPubRequestH\x00\x12\x1f\n\x04sign\x18\x02 \x01(\x0b\x32\x0f.ETHSignRequestH\x00\x42\t\n\x07request\"X\n\x0b\x45THResponse\x12\x1b\n\x03pub\x18\x01 \x01(\x0b\x32\x0c.PubResponseH\x00\x12 \n\x04sign\x18\x02 \x01(\x0b\x32\x10.ETHSignResponseH\x00\x42\n\n\x08response*2\n\x07\x45THCoin\x12\x07\n\x03\x45TH\x10\x00\x12\x0e\n\nRopstenETH\x10\x01\x12\x0e\n\nRinkebyETH\x10\x02\x62\x06proto3') + serialized_pb=_b('\n\teth.proto\x1a\x0c\x63ommon.proto\"\xb8\x01\n\rETHPubRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12\x16\n\x04\x63oin\x18\x02 \x01(\x0e\x32\x08.ETHCoin\x12.\n\x0boutput_type\x18\x03 \x01(\x0e\x32\x19.ETHPubRequest.OutputType\x12\x0f\n\x07\x64isplay\x18\x04 \x01(\x08\x12\x18\n\x10\x63ontract_address\x18\x05 \x01(\x0c\"#\n\nOutputType\x12\x0b\n\x07\x41\x44\x44RESS\x10\x00\x12\x08\n\x04XPUB\x10\x01\"\x9e\x01\n\x0e\x45THSignRequest\x12\x16\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x08.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x11\n\tgas_price\x18\x04 \x01(\x0c\x12\x11\n\tgas_limit\x18\x05 \x01(\x0c\x12\x11\n\trecipient\x18\x06 \x01(\x0c\x12\r\n\x05value\x18\x07 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\"M\n\x15\x45THSignMessageRequest\x12\x16\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x08.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\"$\n\x0f\x45THSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x83\x01\n\nETHRequest\x12\x1d\n\x03pub\x18\x01 \x01(\x0b\x32\x0e.ETHPubRequestH\x00\x12\x1f\n\x04sign\x18\x02 \x01(\x0b\x32\x0f.ETHSignRequestH\x00\x12*\n\x08sign_msg\x18\x03 \x01(\x0b\x32\x16.ETHSignMessageRequestH\x00\x42\t\n\x07request\"X\n\x0b\x45THResponse\x12\x1b\n\x03pub\x18\x01 \x01(\x0b\x32\x0c.PubResponseH\x00\x12 \n\x04sign\x18\x02 \x01(\x0b\x32\x10.ETHSignResponseH\x00\x42\n\n\x08response*2\n\x07\x45THCoin\x12\x07\n\x03\x45TH\x10\x00\x12\x0e\n\nRopstenETH\x10\x01\x12\x0e\n\nRinkebyETH\x10\x02\x62\x06proto3') , dependencies=[common__pb2.DESCRIPTOR,]) _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -47,8 +47,8 @@ ], containing_type=None, options=None, - serialized_start=592, - serialized_end=642, + serialized_start=716, + serialized_end=766, ) _sym_db.RegisterEnumDescriptor(_ETHCOIN) @@ -221,6 +221,51 @@ ) +_ETHSIGNMESSAGEREQUEST = _descriptor.Descriptor( + name='ETHSignMessageRequest', + full_name='ETHSignMessageRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='coin', full_name='ETHSignMessageRequest.coin', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='keypath', full_name='ETHSignMessageRequest.keypath', index=1, + number=2, type=13, cpp_type=3, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='msg', full_name='ETHSignMessageRequest.msg', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=375, + serialized_end=452, +) + + _ETHSIGNRESPONSE = _descriptor.Descriptor( name='ETHSignResponse', full_name='ETHSignResponse', @@ -247,8 +292,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=375, - serialized_end=411, + serialized_start=454, + serialized_end=490, ) @@ -273,6 +318,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), + _descriptor.FieldDescriptor( + name='sign_msg', full_name='ETHRequest.sign_msg', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), ], extensions=[ ], @@ -288,8 +340,8 @@ name='request', full_name='ETHRequest.request', index=0, containing_type=None, fields=[]), ], - serialized_start=413, - serialized_end=500, + serialized_start=493, + serialized_end=624, ) @@ -329,22 +381,27 @@ name='response', full_name='ETHResponse.response', index=0, containing_type=None, fields=[]), ], - serialized_start=502, - serialized_end=590, + serialized_start=626, + serialized_end=714, ) _ETHPUBREQUEST.fields_by_name['coin'].enum_type = _ETHCOIN _ETHPUBREQUEST.fields_by_name['output_type'].enum_type = _ETHPUBREQUEST_OUTPUTTYPE _ETHPUBREQUEST_OUTPUTTYPE.containing_type = _ETHPUBREQUEST _ETHSIGNREQUEST.fields_by_name['coin'].enum_type = _ETHCOIN +_ETHSIGNMESSAGEREQUEST.fields_by_name['coin'].enum_type = _ETHCOIN _ETHREQUEST.fields_by_name['pub'].message_type = _ETHPUBREQUEST _ETHREQUEST.fields_by_name['sign'].message_type = _ETHSIGNREQUEST +_ETHREQUEST.fields_by_name['sign_msg'].message_type = _ETHSIGNMESSAGEREQUEST _ETHREQUEST.oneofs_by_name['request'].fields.append( _ETHREQUEST.fields_by_name['pub']) _ETHREQUEST.fields_by_name['pub'].containing_oneof = _ETHREQUEST.oneofs_by_name['request'] _ETHREQUEST.oneofs_by_name['request'].fields.append( _ETHREQUEST.fields_by_name['sign']) _ETHREQUEST.fields_by_name['sign'].containing_oneof = _ETHREQUEST.oneofs_by_name['request'] +_ETHREQUEST.oneofs_by_name['request'].fields.append( + _ETHREQUEST.fields_by_name['sign_msg']) +_ETHREQUEST.fields_by_name['sign_msg'].containing_oneof = _ETHREQUEST.oneofs_by_name['request'] _ETHRESPONSE.fields_by_name['pub'].message_type = common__pb2._PUBRESPONSE _ETHRESPONSE.fields_by_name['sign'].message_type = _ETHSIGNRESPONSE _ETHRESPONSE.oneofs_by_name['response'].fields.append( @@ -355,6 +412,7 @@ _ETHRESPONSE.fields_by_name['sign'].containing_oneof = _ETHRESPONSE.oneofs_by_name['response'] DESCRIPTOR.message_types_by_name['ETHPubRequest'] = _ETHPUBREQUEST DESCRIPTOR.message_types_by_name['ETHSignRequest'] = _ETHSIGNREQUEST +DESCRIPTOR.message_types_by_name['ETHSignMessageRequest'] = _ETHSIGNMESSAGEREQUEST DESCRIPTOR.message_types_by_name['ETHSignResponse'] = _ETHSIGNRESPONSE DESCRIPTOR.message_types_by_name['ETHRequest'] = _ETHREQUEST DESCRIPTOR.message_types_by_name['ETHResponse'] = _ETHRESPONSE @@ -374,6 +432,13 @@ )) _sym_db.RegisterMessage(ETHSignRequest) +ETHSignMessageRequest = _reflection.GeneratedProtocolMessageType('ETHSignMessageRequest', (_message.Message,), dict( + DESCRIPTOR = _ETHSIGNMESSAGEREQUEST, + __module__ = 'eth_pb2' + # @@protoc_insertion_point(class_scope:ETHSignMessageRequest) + )) +_sym_db.RegisterMessage(ETHSignMessageRequest) + ETHSignResponse = _reflection.GeneratedProtocolMessageType('ETHSignResponse', (_message.Message,), dict( DESCRIPTOR = _ETHSIGNRESPONSE, __module__ = 'eth_pb2' diff --git a/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi index 05a212ca2..c9811e565 100644 --- a/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi @@ -119,6 +119,26 @@ class ETHSignRequest(google___protobuf___message___Message): else: def ClearField(self, field_name: typing_extensions___Literal[u"coin",b"coin",u"data",b"data",u"gas_limit",b"gas_limit",u"gas_price",b"gas_price",u"keypath",b"keypath",u"nonce",b"nonce",u"recipient",b"recipient",u"value",b"value"]) -> None: ... +class ETHSignMessageRequest(google___protobuf___message___Message): + coin = ... # type: ETHCoin + keypath = ... # type: google___protobuf___internal___containers___RepeatedScalarFieldContainer[int] + msg = ... # type: bytes + + def __init__(self, + *, + coin : typing___Optional[ETHCoin] = None, + keypath : typing___Optional[typing___Iterable[int]] = None, + msg : typing___Optional[bytes] = None, + ) -> None: ... + @classmethod + def FromString(cls, s: bytes) -> ETHSignMessageRequest: ... + def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + if sys.version_info >= (3,): + def ClearField(self, field_name: typing_extensions___Literal[u"coin",u"keypath",u"msg"]) -> None: ... + else: + def ClearField(self, field_name: typing_extensions___Literal[u"coin",b"coin",u"keypath",b"keypath",u"msg",b"msg"]) -> None: ... + class ETHSignResponse(google___protobuf___message___Message): signature = ... # type: bytes @@ -143,22 +163,26 @@ class ETHRequest(google___protobuf___message___Message): @property def sign(self) -> ETHSignRequest: ... + @property + def sign_msg(self) -> ETHSignMessageRequest: ... + def __init__(self, *, pub : typing___Optional[ETHPubRequest] = None, sign : typing___Optional[ETHSignRequest] = None, + sign_msg : typing___Optional[ETHSignMessageRequest] = None, ) -> None: ... @classmethod def FromString(cls, s: bytes) -> ETHRequest: ... def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... if sys.version_info >= (3,): - def HasField(self, field_name: typing_extensions___Literal[u"pub",u"request",u"sign"]) -> bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"pub",u"request",u"sign"]) -> None: ... + def HasField(self, field_name: typing_extensions___Literal[u"pub",u"request",u"sign",u"sign_msg"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"pub",u"request",u"sign",u"sign_msg"]) -> None: ... else: - def HasField(self, field_name: typing_extensions___Literal[u"pub",b"pub",u"request",b"request",u"sign",b"sign"]) -> bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"pub",b"pub",u"request",b"request",u"sign",b"sign"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"request",b"request"]) -> typing_extensions___Literal["pub","sign"]: ... + def HasField(self, field_name: typing_extensions___Literal[u"pub",b"pub",u"request",b"request",u"sign",b"sign",u"sign_msg",b"sign_msg"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"pub",b"pub",u"request",b"request",u"sign",b"sign",u"sign_msg",b"sign_msg"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"request",b"request"]) -> typing_extensions___Literal["pub","sign","sign_msg"]: ... class ETHResponse(google___protobuf___message___Message): diff --git a/py/send_message.py b/py/send_message.py index 8207c9f97..ae24e2b16 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -21,6 +21,7 @@ from typing import List, Any, Optional, Callable, Union, Tuple, Sequence import hashlib import base64 +import binascii import hid from tzlocal import get_localzone @@ -325,6 +326,20 @@ def _sign_eth_tx(self) -> None: # fmt: on self._device.eth_sign(tx, keypath=[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0]) + def _sign_eth_message(self) -> None: + msg = input("Message to sign: ") + if msg.startswith("0x"): + msg_bytes = binascii.unhexlify(msg[2:]) + else: + msg_bytes = msg.encode("utf-8") + msg_hex = binascii.hexlify(msg_bytes).decode("utf-8") + print(f"signing\nbytes: {msg_bytes}\nhex: 0x{msg_hex}") + sig = self._device.eth_sign_msg( + msg=msg_bytes, keypath=[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0] + ) + + print("Signature: 0x{}".format(binascii.hexlify(sig).decode("utf-8"))) + def _reset_device(self) -> None: if self._device.reset(): print("Device RESET") @@ -373,7 +388,8 @@ def _menu_init(self) -> None: ("Toggle BIP39 Mnemonic Passphrase", self._toggle_mnemonic_passphrase), ("Retrieve Ethereum xpub", self._get_eth_xpub), ("Retrieve Ethereum address", self._display_eth_address), - ("Sign eth tx", self._sign_eth_tx), + ("Sign Ethereum tx", self._sign_eth_tx), + ("Sign Ethereum Message", self._sign_eth_message), ("Reset Device", self._reset_device), ) choice = ask_user(choices) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f1ed4ff1..3ce814a86 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -192,6 +192,7 @@ set(ETHEREUM-SOURCES ${CMAKE_SOURCE_DIR}/src/apps/eth/eth_params.c ${CMAKE_SOURCE_DIR}/src/apps/eth/eth_sighash.c ${CMAKE_SOURCE_DIR}/src/apps/eth/eth_sign.c + ${CMAKE_SOURCE_DIR}/src/apps/eth/eth_sign_msg.c ${CMAKE_SOURCE_DIR}/src/apps/eth/eth_verify.c ${CMAKE_SOURCE_DIR}/src/commander/commander_eth.c ) diff --git a/src/apps/eth/eth_sign_msg.c b/src/apps/eth/eth_sign_msg.c new file mode 100644 index 000000000..ace7e5d55 --- /dev/null +++ b/src/apps/eth/eth_sign_msg.c @@ -0,0 +1,127 @@ +// Copyright 2019 Shift Cryptosecurity AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "eth_sign_msg.h" +#include "eth_common.h" +#include "eth_sighash.h" +#include "eth_verify.h" +#include + +#include "eth.h" +#include +#include +#include +#include + +app_eth_sign_error_t app_eth_sign_msg( + const ETHSignMessageRequest* request, + ETHSignResponse* response) +{ + // To update this number you need to update the buffer size later. + if (request->msg.size > 9999) { + return APP_ETH_SIGN_ERR_INVALID_INPUT; + } + // Only support main net for now. Otherwise a user could be tricked into signing something for + // main net even if they believe they are signing for testnet. + if (request->coin != ETHCoin_ETH) { + return APP_ETH_SIGN_ERR_INVALID_INPUT; + } + + // Let user verify that it is signing for the expected address + char address[APP_ETH_ADDRESS_HEX_LEN] = {0}; + if (!app_eth_address( + request->coin, + ETHPubRequest_OutputType_ADDRESS, + request->keypath, + request->keypath_count, + address, + sizeof(address))) { + return APP_ETH_SIGN_ERR_INVALID_INPUT; + } + { + confirm_params_t params = { + .title = "Your\naddress", + .body = address, + .scrollable = true, + }; + if (!workflow_confirm(¶ms)) { + return APP_ETH_SIGN_ERR_USER_ABORT; + } + } + + const char msg_header[] = + "\x19" + "Ethereum Signed Message:\n"; + // sizeof(msg_header) includes null terminator + // the maximum length of the signed data is 1024, therefore 4 bytes might be needed as length + // prefix. + char msg[sizeof(msg_header) - 1 + sizeof(request->msg.bytes) + 4] = {0}; + + // payload_offset is also the length of the fixed header + payload size prefix + size_t payload_offset = snprintf(msg, sizeof(msg), "%s%d", msg_header, request->msg.size); + memcpy(&msg[payload_offset], request->msg.bytes, request->msg.size); + + // determine if the message is in ASCII + bool all_ascii = true; + for (size_t i = 0; i < request->msg.size; ++i) { + if (request->msg.bytes[i] < 20 || request->msg.bytes[i] > 127) { + all_ascii = false; + } + } + + char body[sizeof(request->msg.bytes) * 2 + 1] = {0}; + confirm_params_t params = { + .body = body, + .scrollable = true, + .shorten_body = true, + }; + if (all_ascii) { + // If it is all ASCII, copy the bytes over and ensure there is a null terminator + snprintf(body, sizeof(body), "%.*s", request->msg.size, request->msg.bytes); + params.title = "Sign\nETH Message"; + } else { + // If it is binary, convert to hex + util_uint8_to_hex(request->msg.bytes, request->msg.size, body); + params.title = "Sign\nETH Message (hex)"; + params.display_size = request->msg.size; + } + + if (!workflow_confirm(¶ms)) { + return APP_ETH_SIGN_ERR_USER_ABORT; + } + + // Calculate the hash + uint8_t sighash[sha3_256_hash_size]; + sha3_ctx ctx; + rhash_sha3_256_init(&ctx); + rhash_sha3_update(&ctx, (const unsigned char*)msg, payload_offset + request->msg.size); + rhash_keccak_final(&ctx, sighash); + + // Sign the hash and return the signature, with last byte set to recid. + // check assumption + if (sizeof(response->signature) != 65) { + Abort("unexpected signature size"); + } + int recid; + if (!keystore_secp256k1_sign( + request->keypath, request->keypath_count, sighash, response->signature, &recid)) { + return APP_ETH_SIGN_ERR_UNKNOWN; + } + if (recid > 0xFF) { + Abort("unexpected recid"); + } + response->signature[64] = (uint8_t)recid; + + return APP_ETH_SIGN_OK; +} diff --git a/src/apps/eth/eth_sign_msg.h b/src/apps/eth/eth_sign_msg.h new file mode 100644 index 000000000..5a78725e1 --- /dev/null +++ b/src/apps/eth/eth_sign_msg.h @@ -0,0 +1,26 @@ +// Copyright 2019 Shift Cryptosecurity AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef _APPS_ETH_SIGN_MSG_H +#define _APPS_ETH_SIGN_MSG_H + +#include "eth_verify.h" + +#include + +app_eth_sign_error_t app_eth_sign_msg( + const ETHSignMessageRequest* request, + ETHSignResponse* response); + +#endif diff --git a/src/commander/commander_eth.c b/src/commander/commander_eth.c index f146493fa..e15d64a38 100644 --- a/src/commander/commander_eth.c +++ b/src/commander/commander_eth.c @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -87,6 +88,20 @@ static commander_error_t _api_sign(const ETHSignRequest* request, ETHSignRespons return COMMANDER_OK; } +static commander_error_t _api_sign_msg( + const ETHSignMessageRequest* request, + ETHSignResponse* response) +{ + app_eth_sign_error_t result = app_eth_sign_msg(request, response); + if (result == APP_ETH_SIGN_ERR_USER_ABORT) { + return COMMANDER_ERR_USER_ABORT; + } + if (result != APP_ETH_SIGN_OK) { + return COMMANDER_ERR_GENERIC; + } + return COMMANDER_OK; +} + commander_error_t commander_eth(const ETHRequest* request, ETHResponse* response) { switch (request->which_request) { @@ -96,6 +111,9 @@ commander_error_t commander_eth(const ETHRequest* request, ETHResponse* response case ETHRequest_sign_tag: response->which_response = ETHResponse_sign_tag; return _api_sign(&(request->request.sign), &response->response.sign); + case ETHRequest_sign_msg_tag: + response->which_response = ETHResponse_sign_tag; + return _api_sign_msg(&(request->request.sign_msg), &response->response.sign); default: return COMMANDER_ERR_GENERIC; } diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index 9dfeec57f..83efebf1f 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -199,6 +199,8 @@ set(TEST_LIST "" app_eth_sign "-Wl,--wrap=app_eth_verify_standard_transaction,--wrap=app_eth_verify_erc20_transaction,--wrap=keystore_secp256k1_sign,--wrap=eth_common_is_valid_keypath_address" + app_eth_sign_msg + "-Wl,--wrap=keystore_secp256k1_sign,--wrap=keystore_secp256k1_pubkey,--wrap=eth_common_is_valid_keypath_address,--wrap=workflow_confirm" app_eth_verify "-Wl,--wrap=workflow_verify_recipient,--wrap=workflow_verify_total,--wrap=app_eth_erc20_params_get" usart diff --git a/test/unit-test/test_app_eth_sign_msg.c b/test/unit-test/test_app_eth_sign_msg.c new file mode 100644 index 000000000..4fc842ab5 --- /dev/null +++ b/test/unit-test/test_app_eth_sign_msg.c @@ -0,0 +1,171 @@ +// Copyright 2019 Shift Cryptosecurity AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +static uint8_t _recid = 3; + +// sig with _recid at the end +static uint8_t _sig[65] = + "\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55" + "\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55" + "\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x55\x03"; + +bool __wrap_workflow_confirm(confirm_params_t params) +{ + return true; +} + +bool __wrap_keystore_secp256k1_pubkey( + keystore_secp256k1_pubkey_format format, + const uint32_t* keypath, + size_t keypath_len, + uint8_t* pubkey_out, + size_t pubkey_out_len) +{ + return true; +} + +bool __wrap_keystore_secp256k1_sign( + const uint32_t* keypath, + size_t keypath_len, + const uint8_t* msg32, + uint8_t* sig_compact_out, + int* recid_out) +{ + memcpy(sig_compact_out, _sig, 64); + *recid_out = _recid; + return mock(); +} + +bool __real_eth_common_is_valid_keypath_address( + ETHCoin coin, + const uint32_t* keypath, + size_t keypath_len); +bool __wrap_eth_common_is_valid_keypath_address( + ETHCoin coin, + const uint32_t* keypath, + size_t keypath_len) +{ + assert_int_equal(coin, ETHCoin_ETH); + check_expected(keypath); + assert_int_equal(keypath_len, 5); + return __real_eth_common_is_valid_keypath_address(coin, keypath, keypath_len); +} + +static void _default_request(ETHSignMessageRequest* request) +{ + ETHSignMessageRequest r = { + .coin = ETHCoin_ETH, + .keypath_count = 5, + .keypath = + { + 44 + BIP32_INITIAL_HARDENED_CHILD, + 60 + BIP32_INITIAL_HARDENED_CHILD, + 0 + BIP32_INITIAL_HARDENED_CHILD, + 0, + 0, + }, + }; + r.msg.size = sizeof(request->msg.bytes); + memset(r.msg.bytes, 'a', sizeof(request->msg.bytes)); + memcpy(request, &r, sizeof(r)); +} + +static void _expect_keypath(const ETHSignMessageRequest* request) +{ + expect_memory( + __wrap_eth_common_is_valid_keypath_address, + keypath, + request->keypath, + request->keypath_count * sizeof(uint32_t)); +} + +static void _test_app_eth_sign_msg(void** state) +{ + ETHSignResponse response; + + { + // Test a long string message + ETHSignMessageRequest request; + _default_request(&request); + will_return(__wrap_keystore_secp256k1_sign, true); + _expect_keypath(&request); + assert_int_equal(APP_ETH_SIGN_OK, app_eth_sign_msg(&request, &response)); + assert_memory_equal(response.signature, _sig, sizeof(_sig)); + } + { + // Test a long binary message + ETHSignMessageRequest request; + _default_request(&request); + request.msg.size = 64; + memset(request.msg.bytes, '\01', 64); + will_return(__wrap_keystore_secp256k1_sign, true); + _expect_keypath(&request); + assert_int_equal(APP_ETH_SIGN_OK, app_eth_sign_msg(&request, &response)); + assert_memory_equal(response.signature, _sig, sizeof(_sig)); + } + { + // test a short string message + ETHSignMessageRequest request; + _default_request(&request); + request.msg.size = 64; + will_return(__wrap_keystore_secp256k1_sign, true); + _expect_keypath(&request); + assert_int_equal(APP_ETH_SIGN_OK, app_eth_sign_msg(&request, &response)); + assert_memory_equal(response.signature, _sig, sizeof(_sig)); + } + { + // test a short binary message + ETHSignMessageRequest request; + _default_request(&request); + request.msg.size = 16; + memset(request.msg.bytes, '\01', 16); + will_return(__wrap_keystore_secp256k1_sign, true); + _expect_keypath(&request); + assert_int_equal(APP_ETH_SIGN_OK, app_eth_sign_msg(&request, &response)); + assert_memory_equal(response.signature, _sig, sizeof(_sig)); + } +} + +static void _test_app_eth_sign_msg_unhappy(void** state) +{ + ETHSignResponse response; + + { + ETHSignMessageRequest request; + _default_request(&request); + request.msg.size = 10000; + assert_int_equal(APP_ETH_SIGN_ERR_INVALID_INPUT, app_eth_sign_msg(&request, &response)); + } +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(_test_app_eth_sign_msg), + cmocka_unit_test(_test_app_eth_sign_msg_unhappy), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}