diff --git a/doc/ethapp.asc b/doc/ethapp.asc index 7ae5db3..6dda27a 100644 --- a/doc/ethapp.asc +++ b/doc/ethapp.asc @@ -1,7 +1,7 @@ Ethereum application : Common Technical Specifications ======================================================= Ledger Firmware Team -Application version 1.1.10 - 4th of February 2019 +Application version 1.5.0 - 25th of September 2020 ## 1.0 - Initial release @@ -16,6 +16,9 @@ Application version 1.1.10 - 4th of February 2019 ## 1.1.10 - Add PROVIDE ERC 20 TOKEN INFORMATION +## 1.5.0 + - Add SIGN ETH EIP 712 + ## About This application describes the APDU messages interface to communicate with the Ethereum application. @@ -268,6 +271,49 @@ None | Application patch version | 01 |============================================================================================================================== +### SIGN ETH EIP 712 + +#### Description + +This command signs an Ethereum message following the EIP 712 specification (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) + +For implementation version 0, the domain hash and message hash are provided to the device, which displays them and returns the signature + +This command has been supported since firmware version 1.5.0 + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 0C | 00 + | implementation version : 00 | variable | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +| Domain hash | 32 +| Message hash | 32 +|============================================================================================================================== + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| v | 1 +| r | 32 +| s | 32 +|============================================================================================================================== ## Transport protocol diff --git a/examples/signMessageEIP711v0.py b/examples/signMessageEIP711v0.py new file mode 100755 index 0000000..97b8ef1 --- /dev/null +++ b/examples/signMessageEIP711v0.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" +******************************************************************************* +* Ledger Ethereum App +* (c) 2016-2019 Ledger +* +* 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. +******************************************************************************** +""" +from __future__ import print_function + +from ledgerblue.comm import getDongle +from ledgerblue.commException import CommException +from decimal import Decimal +from Crypto.Hash import keccak +from eth_keys import KeyAPI +import argparse +import struct +import binascii + +# Define here Chain_ID +CHAIN_ID = 0 + +# Magic define +SIGN_MAGIC = b'\x19\x01' + +def parse_bip32_path(path): + if len(path) == 0: + return b"" + result = b"" + elements = path.split('/') + for pathElement in elements: + element = pathElement.split('\'') + if len(element) == 1: + result = result + struct.pack(">I", int(element[0])) + else: + result = result + struct.pack(">I", 0x80000000 | int(element[0])) + return result + + +parser = argparse.ArgumentParser() +parser.add_argument('--path', help="BIP 32 path to sign with") +parser.add_argument('--domainHash', help="Domain Hash (hex)", required=True) +parser.add_argument('--messageHash', help='Message Hash (hex)', required=True) +args = parser.parse_args() + +if args.path == None: + args.path = "44'/60'" +domainHash = binascii.unhexlify(args.domainHash) +messageHash = binascii.unhexlify(args.messageHash) + +encodedTx = domainHash + messageHash + +donglePath = parse_bip32_path(args.path) +apdu = bytearray.fromhex("e00c0000") +apdu.append(len(donglePath) + 1 + len(encodedTx)) +apdu.append(len(donglePath) // 4) +apdu += donglePath + encodedTx + +dongle = getDongle(True) +result = dongle.exchange(bytes(apdu)) + +v = int(result[0]) + +# Compute parity +if (CHAIN_ID*2 + 35) + 1 > 255: + ecc_parity = v - ((CHAIN_ID*2 + 35) % 256) +else: + ecc_parity = (v + 1) % 2 + +v = "%02X" % ecc_parity +r = binascii.hexlify(result[1:1 + 32]).decode() +s = binascii.hexlify(result[1 + 32: 1 + 32 + 32]).decode() +msg_to_sign = SIGN_MAGIC + domainHash + messageHash +hash = keccak.new(digest_bits=256, data=msg_to_sign).digest() + +signature = KeyAPI.Signature(vrs=(int(v, 16), int(r, 16), int(s, 16))) +pubkey = KeyAPI.PublicKey.recover_from_msg_hash(hash, signature) + +print("[INFO] Hash is: 0x", binascii.hexlify(hash).decode(), sep=''); +print('{') +print(' "address": "', pubkey.to_address(), '",', sep='') +print(' "domain hash": "', binascii.hexlify(domainHash),'",', sep='') +print(' "message hash": "', binascii.hexlify(messageHash),'",', sep='') +print(' "sig": "', signature, '",', sep = '') +print(' "version": "3"') +print(' "signed": "ledger"') +print('}') diff --git a/src/apdu_constants.h b/src/apdu_constants.h index c16e96d..2a24ff7 100644 --- a/src/apdu_constants.h +++ b/src/apdu_constants.h @@ -10,6 +10,7 @@ #define INS_GET_APP_CONFIGURATION 0x06 #define INS_SIGN_PERSONAL_MESSAGE 0x08 #define INS_PROVIDE_ERC20_TOKEN_INFORMATION 0x0A +#define INS_SIGN_EIP_712_MESSAGE 0x0C #define P1_CONFIRM 0x01 #define P1_NON_CONFIRM 0x00 #define P2_NO_CHAINCODE 0x00 diff --git a/src/main.c b/src/main.c index a64caff..614b171 100644 --- a/src/main.c +++ b/src/main.c @@ -544,6 +544,11 @@ void handleApdu(unsigned int *flags, unsigned int *tx) { handleSignPersonalMessage(G_io_apdu_buffer[OFFSET_P1], G_io_apdu_buffer[OFFSET_P2], G_io_apdu_buffer + OFFSET_CDATA, G_io_apdu_buffer[OFFSET_LC], flags, tx); break; + case INS_SIGN_EIP_712_MESSAGE: + os_memset(tmpCtx.transactionContext.tokenSet, 0, MAX_TOKEN); + handleSignEIP712Message(G_io_apdu_buffer[OFFSET_P1], G_io_apdu_buffer[OFFSET_P2], G_io_apdu_buffer + OFFSET_CDATA, G_io_apdu_buffer[OFFSET_LC], flags, tx); + break; + #if 0 case 0xFF: // return to dashboard goto return_to_dashboard; diff --git a/src/shared_context.h b/src/shared_context.h index adc3b9f..a89f2ad 100644 --- a/src/shared_context.h +++ b/src/shared_context.h @@ -71,10 +71,19 @@ typedef struct messageSigningContext_t { uint32_t remainingLength; } messageSigningContext_t; +typedef struct messageSigningContext712_t { + uint8_t pathLength; + uint32_t bip32Path[MAX_BIP32_PATH]; + uint8_t domainHash[32]; + uint8_t messageHash[32]; +} messageSigningContext712_t; + + typedef union { publicKeyContext_t publicKeyContext; transactionContext_t transactionContext; messageSigningContext_t messageSigningContext; + messageSigningContext712_t messageSigningContext712; } tmpCtx_t; typedef union { diff --git a/src/ui_callbacks.h b/src/ui_callbacks.h index bb291d5..72c09fa 100644 --- a/src/ui_callbacks.h +++ b/src/ui_callbacks.h @@ -10,6 +10,8 @@ unsigned int io_seproxyhal_touch_signMessage_ok(const bagl_element_t *e); unsigned int io_seproxyhal_touch_signMessage_cancel(const bagl_element_t *e); unsigned int io_seproxyhal_touch_data_ok(const bagl_element_t *e); unsigned int io_seproxyhal_touch_data_cancel(const bagl_element_t *e); +unsigned int io_seproxyhal_touch_signMessage712_v0_ok(const bagl_element_t *e); +unsigned int io_seproxyhal_touch_signMessage712_v0_cancel(const bagl_element_t *e); void ui_idle(void); diff --git a/src/ui_flow.h b/src/ui_flow.h index 9ef34cf..2a76204 100644 --- a/src/ui_flow.h +++ b/src/ui_flow.h @@ -20,10 +20,12 @@ extern const ux_flow_step_t * const ux_approval_allowance_flow []; extern const ux_flow_step_t * const ux_sign_flow []; -extern const ux_flow_step_t * const ux_display_stark_public_flow []; +extern const ux_flow_step_t * const ux_sign_712_v0_flow []; #ifdef HAVE_STARKWARE +extern const ux_flow_step_t * const ux_display_stark_public_flow []; + extern const ux_flow_step_t * const ux_stark_limit_order_flow []; extern const ux_flow_step_t * const ux_stark_transfer_flow []; diff --git a/src_features/signMessageEIP712/cmd_signMessage712.c b/src_features/signMessageEIP712/cmd_signMessage712.c new file mode 100644 index 0000000..aa073b6 --- /dev/null +++ b/src_features/signMessageEIP712/cmd_signMessage712.c @@ -0,0 +1,64 @@ +#include "shared_context.h" +#include "apdu_constants.h" +#include "utils.h" +#ifdef TARGET_BLUE +#include "ui_blue.h" +#endif +#ifdef HAVE_UX_FLOW +#include "ui_flow.h" +#endif + +static const char const SIGN_MAGIC[] = "\x19" + "Ethereum Signed Message:\n"; + +void handleSignEIP712Message(uint8_t p1, uint8_t p2, uint8_t *workBuffer, uint16_t dataLength, unsigned int *flags, unsigned int *tx) { + uint8_t i; + + UNUSED(tx); + if ((p1 != 00) || (p2 != 00)) { + THROW(0x6B00); + } + if (appState != APP_STATE_IDLE) { + reset_app_context(); + } + if (dataLength < 1) { + PRINTF("Invalid data\n"); + THROW(0x6a80); + } + tmpCtx.messageSigningContext712.pathLength = workBuffer[0]; + if ((tmpCtx.messageSigningContext712.pathLength < 0x01) || + (tmpCtx.messageSigningContext712.pathLength > MAX_BIP32_PATH)) { + PRINTF("Invalid path\n"); + THROW(0x6a80); + } + workBuffer++; + dataLength--; + for (i = 0; i < tmpCtx.messageSigningContext712.pathLength; i++) { + if (dataLength < 4) { + PRINTF("Invalid data\n"); + THROW(0x6a80); + } + tmpCtx.messageSigningContext712.bip32Path[i] = U4BE(workBuffer, 0); + workBuffer += 4; + dataLength -= 4; + } + if (dataLength < 32 + 32) { + PRINTF("Invalid data\n"); + THROW(0x6a80); + } + memmove(tmpCtx.messageSigningContext712.domainHash, workBuffer, 32); + memmove(tmpCtx.messageSigningContext712.messageHash, workBuffer + 32, 32); + +#ifdef NO_CONSENT + io_seproxyhal_touch_signMessage_ok(NULL); +#else //NO_CONSENT +#if defined(TARGET_BLUE) + // TODO implement + ui_approval_message_sign_blue_init(); +#else + ux_flow_init(0, ux_sign_712_v0_flow, NULL); +#endif // #if TARGET_ID +#endif // NO_CONSENT + + *flags |= IO_ASYNCH_REPLY; +} diff --git a/src_features/signMessageEIP712/ui_common_signMessage712.c b/src_features/signMessageEIP712/ui_common_signMessage712.c new file mode 100644 index 0000000..ce31234 --- /dev/null +++ b/src_features/signMessageEIP712/ui_common_signMessage712.c @@ -0,0 +1,64 @@ +#include "shared_context.h" +#include "ui_callbacks.h" + +static const uint8_t const EIP_712_MAGIC[] = { 0x19, 0x01 }; + +unsigned int io_seproxyhal_touch_signMessage712_v0_ok(const bagl_element_t *e) { + uint8_t privateKeyData[32]; + uint8_t hash[32]; + uint8_t signature[100]; + uint8_t signatureLength; + cx_ecfp_private_key_t privateKey; + uint32_t tx = 0; + io_seproxyhal_io_heartbeat(); + cx_keccak_init(&global_sha3, 256); + cx_hash((cx_hash_t *)&global_sha3, 0, (uint8_t*)EIP_712_MAGIC, sizeof(EIP_712_MAGIC), NULL, 0); + cx_hash((cx_hash_t *)&global_sha3, 0, tmpCtx.messageSigningContext712.domainHash, + sizeof(tmpCtx.messageSigningContext712.domainHash), NULL, 0); + cx_hash((cx_hash_t *)&global_sha3, CX_LAST, tmpCtx.messageSigningContext712.messageHash, + sizeof(tmpCtx.messageSigningContext712.messageHash), hash, sizeof(hash)); + PRINTF("EIP712 hash to sign %.*H\n", 32, hash); + io_seproxyhal_io_heartbeat(); + os_perso_derive_node_bip32( + CX_CURVE_256K1, tmpCtx.messageSigningContext712.bip32Path, + tmpCtx.messageSigningContext712.pathLength, privateKeyData, NULL); + io_seproxyhal_io_heartbeat(); + cx_ecfp_init_private_key(CX_CURVE_256K1, privateKeyData, 32, &privateKey); + os_memset(privateKeyData, 0, sizeof(privateKeyData)); + unsigned int info = 0; + io_seproxyhal_io_heartbeat(); + signatureLength = + cx_ecdsa_sign(&privateKey, CX_RND_RFC6979 | CX_LAST, CX_SHA256, + hash, + sizeof(hash), signature, sizeof(signature), &info); + os_memset(&privateKey, 0, sizeof(privateKey)); + G_io_apdu_buffer[0] = 27; + if (info & CX_ECCINFO_PARITY_ODD) { + G_io_apdu_buffer[0]++; + } + if (info & CX_ECCINFO_xGTn) { + G_io_apdu_buffer[0] += 2; + } + format_signature_out(signature); + tx = 65; + G_io_apdu_buffer[tx++] = 0x90; + G_io_apdu_buffer[tx++] = 0x00; + reset_app_context(); + // Send back the response, do not restart the event loop + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, tx); + // Display back the original UX + ui_idle(); + return 0; // do not redraw the widget +} + +unsigned int io_seproxyhal_touch_signMessage712_v0_cancel(const bagl_element_t *e) { + reset_app_context(); + G_io_apdu_buffer[0] = 0x69; + G_io_apdu_buffer[1] = 0x85; + // Send back the response, do not restart the event loop + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); + // Display back the original UX + ui_idle(); + return 0; // do not redraw the widget +} + diff --git a/src_features/signMessageEIP712/ui_flow_signMessage712.c b/src_features/signMessageEIP712/ui_flow_signMessage712.c new file mode 100644 index 0000000..1febe9a --- /dev/null +++ b/src_features/signMessageEIP712/ui_flow_signMessage712.c @@ -0,0 +1,63 @@ +#include "shared_context.h" +#include "ui_callbacks.h" + +void prepare_domain_hash_v0() { + snprintf(strings.tmp.tmp, 70, "0x%.*H", 32, tmpCtx.messageSigningContext712.domainHash); +} + +void prepare_message_hash_v0() { + snprintf(strings.tmp.tmp, 70, "0x%.*H", 32, tmpCtx.messageSigningContext712.messageHash); +} + +UX_FLOW_DEF_NOCB( + ux_sign_712_v0_flow_1_step, + pnn, + { + &C_icon_certificate, + "Sign", + "typed message", + }); +UX_STEP_NOCB_INIT( + ux_sign_712_v0_flow_2_step, + bnnn_paging, + prepare_domain_hash_v0(), + { + .title = "Domain hash", + .text = strings.tmp.tmp, + }); +UX_STEP_NOCB_INIT( + ux_sign_712_v0_flow_3_step, + bnnn_paging, + prepare_message_hash_v0(), + { + .title = "Message hash", + .text = strings.tmp.tmp, + }); +UX_FLOW_DEF_VALID( + ux_sign_712_v0_flow_4_step, + pbb, + io_seproxyhal_touch_signMessage712_v0_ok(NULL), + { + &C_icon_validate_14, + "Sign", + "message", + }); +UX_FLOW_DEF_VALID( + ux_sign_712_v0_flow_5_step, + pbb, + io_seproxyhal_touch_signMessage712_v0_cancel(NULL), + { + &C_icon_crossmark, + "Cancel", + "signature", + }); + +const ux_flow_step_t * const ux_sign_712_v0_flow [] = { + &ux_sign_712_v0_flow_1_step, + &ux_sign_712_v0_flow_2_step, + &ux_sign_712_v0_flow_3_step, + &ux_sign_712_v0_flow_4_step, + &ux_sign_712_v0_flow_5_step, + FLOW_END_STEP, +}; +