From 076c5b9cbeeba1f17aaf8e81c30c676c3eb143ce Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Thu, 21 Oct 2021 17:23:59 +0200 Subject: [PATCH] feat(legacy/ethereum): support for EIP-1559 transactions --- legacy/firmware/.changelog.d/1834.added | 1 + legacy/firmware/ethereum.c | 402 +++++++++++++++------ legacy/firmware/ethereum.h | 4 +- legacy/firmware/fsm.h | 4 +- legacy/firmware/fsm_msg_ethereum.h | 19 +- tests/device_tests/ethereum/test_signtx.py | 3 - 6 files changed, 310 insertions(+), 123 deletions(-) create mode 100644 legacy/firmware/.changelog.d/1834.added diff --git a/legacy/firmware/.changelog.d/1834.added b/legacy/firmware/.changelog.d/1834.added new file mode 100644 index 0000000000..75fb6a8d89 --- /dev/null +++ b/legacy/firmware/.changelog.d/1834.added @@ -0,0 +1 @@ +Support for Ethereum EIP-1559 transactions. diff --git a/legacy/firmware/ethereum.c b/legacy/firmware/ethereum.c index ef898641e2..1332f7e23c 100644 --- a/legacy/firmware/ethereum.c +++ b/legacy/firmware/ethereum.c @@ -41,15 +41,34 @@ uint32). chain_ids larger than this will only return one bit and the caller must recalculate the full value: v = 2 * chain_id + 35 + v_bit */ #define MAX_CHAIN_ID ((0xFFFFFFFF - 36) >> 1) +#define EIP1559_TX_TYPE 2 static bool ethereum_signing = false; static uint32_t data_total, data_left; static EthereumTxRequest msg_tx_request; static CONFIDENTIAL uint8_t privkey[32]; static uint64_t chain_id; -static uint32_t tx_type; +static bool eip1559; struct SHA3_CTX keccak_ctx = {0}; +struct signing_params { + bool pubkeyhash_set; + uint8_t pubkeyhash[20]; + uint64_t chain_id; + + uint32_t data_length; + uint32_t data_initial_chunk_size; + const uint8_t *data_initial_chunk_bytes; + + bool has_to; + const char *to; + + const TokenType *token; + + uint32_t value_size; + const uint8_t *value_bytes; +}; + static inline void hash_data(const uint8_t *buf, size_t size) { sha3_Update(&keccak_ctx, buf, size); } @@ -192,11 +211,15 @@ static void send_signature(void) { uint8_t v = 0; layoutProgress(_("Signing"), 1000); - /* eip-155 replay protection */ - /* hash v=chain_id, r=0, s=0 */ - hash_rlp_number(chain_id); - hash_rlp_length(0, 0); - hash_rlp_length(0, 0); + if (eip1559) { + hash_rlp_list_length(0); + } else { + /* eip-155 replay protection */ + /* hash v=chain_id, r=0, s=0 */ + hash_rlp_number(chain_id); + hash_rlp_length(0, 0); + hash_rlp_length(0, 0); + } keccak_Final(&keccak_ctx, hash); if (ecdsa_sign_digest(&secp256k1, privkey, hash, sig, &v, @@ -212,7 +235,7 @@ static void send_signature(void) { msg_tx_request.has_data_length = false; msg_tx_request.has_signature_v = true; - if (chain_id > MAX_CHAIN_ID) { + if (eip1559 || chain_id > MAX_CHAIN_ID) { msg_tx_request.signature_v = v; } else { msg_tx_request.signature_v = v + 2 * chain_id + 35; @@ -375,6 +398,34 @@ static void layoutEthereumFee(const uint8_t *value, uint32_t value_len, _("for gas?"), NULL); } +static void layoutEthereumFeeEIP1559(const char *description, + const uint8_t *amount_bytes, + uint32_t amount_len, + const uint8_t *multiplier_bytes, + uint32_t multiplier_len) { + bignum256 amount_val = {0}; + uint8_t padded[32] = {0}; + char amount_str[32] = {0}; + + memcpy(padded + (32 - amount_len), amount_bytes, amount_len); + bn_read_be(padded, &amount_val); + + if (multiplier_len > 0) { + bignum256 multiplier_val = {0}; + + memzero(padded, sizeof(padded)); + memcpy(padded + (32 - multiplier_len), multiplier_bytes, multiplier_len); + bn_read_be(padded, &multiplier_val); + bn_multiply(&multiplier_val, &amount_val, &secp256k1.prime); + } + + ethereumFormatAmount(&amount_val, NULL, amount_str, sizeof(amount_str)); + + layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, + _("Confirm fee"), description, amount_str, NULL, NULL, + NULL); +} + /* * RLP fields: * - nonce (0 .. 32) @@ -385,54 +436,136 @@ static void layoutEthereumFee(const uint8_t *value, uint32_t value_len, * - data (0 ..) */ -static bool ethereum_signing_check(const EthereumSignTx *msg) { - size_t tolen = msg->has_to ? strlen(msg->to) : 0; +static bool ethereum_signing_init_common(struct signing_params *params) { + ethereum_signing = true; + sha3_256_Init(&keccak_ctx); - if (tolen != 42 && tolen != 40 && tolen != 0) { - /* Address has wrong length */ + data_total = data_left = 0; + chain_id = 0; + + memzero(&msg_tx_request, sizeof(EthereumTxRequest)); + + /* eip-155 chain id */ + if (params->chain_id < 1) { + fsm_sendFailure(FailureType_Failure_DataError, _("Chain ID out of bounds")); return false; } + chain_id = params->chain_id; + + if (params->data_length > 0) { + if (params->data_initial_chunk_size == 0) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Data length provided, but no initial chunk")); + return false; + } + /* Our encoding only supports transactions up to 2^24 bytes. To + * prevent exceeding the limit we use a stricter limit on data length. + */ + if (params->data_length > 16000000) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Data length exceeds limit")); + return false; + } + data_total = params->data_length; + } else { + data_total = 0; + } + if (params->data_initial_chunk_size > data_total) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Invalid size of initial chunk")); + return false; + } + + // safety checks + + size_t tolen = params->has_to ? strlen(params->to) : 0; + /* Address has wrong length */ + bool wrong_length = (tolen != 42 && tolen != 40 && tolen != 0); // sending transaction to address 0 (contract creation) without a data field - if (tolen == 0 && (!msg->has_data_length || msg->data_length == 0)) { - return false; - } + bool contract_without_data = (tolen == 0 && params->data_length == 0); - if (msg->gas_price.size + msg->gas_limit.size > 30) { - // sanity check that fee doesn't overflow + if (wrong_length || contract_without_data) { + fsm_sendFailure(FailureType_Failure_DataError, _("Safety check failed")); return false; } return true; } -void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node) { - ethereum_signing = true; - sha3_256_Init(&keccak_ctx); - - memzero(&msg_tx_request, sizeof(EthereumTxRequest)); - /* set fields to 0, to avoid conditions later */ - if (!msg->has_value) msg->value.size = 0; - if (!msg->has_data_initial_chunk) msg->data_initial_chunk.size = 0; - bool toset; - uint8_t pubkeyhash[20] = {0}; - if (msg->has_to && ethereum_parse(msg->to, pubkeyhash)) { - toset = true; +static void ethereum_signing_handle_erc20(struct signing_params *params) { + if (params->has_to && ethereum_parse(params->to, params->pubkeyhash)) { + params->pubkeyhash_set = true; } else { - msg->to[0] = 0; - toset = false; - memzero(pubkeyhash, sizeof(pubkeyhash)); + params->pubkeyhash_set = false; + memzero(params->pubkeyhash, sizeof(params->pubkeyhash)); } - if (!msg->has_nonce) msg->nonce.size = 0; - /* eip-155 chain id */ - if (msg->chain_id < 1) { - fsm_sendFailure(FailureType_Failure_DataError, _("Chain ID out of bounds")); + // detect ERC-20 token + if (params->pubkeyhash_set && params->value_size == 0 && data_total == 68 && + params->data_initial_chunk_size == 68 && + memcmp(params->data_initial_chunk_bytes, + "\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 16) == 0) { + params->token = tokenByChainAddress(chain_id, params->pubkeyhash); + } +} + +static bool ethereum_signing_confirm_common( + const struct signing_params *params) { + if (params->token != NULL) { + layoutEthereumConfirmTx(params->data_initial_chunk_bytes + 16, 20, + params->data_initial_chunk_bytes + 36, 32, + params->token); + } else { + layoutEthereumConfirmTx(params->pubkeyhash, 20, params->value_bytes, + params->value_size, NULL); + } + + if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + return false; + } + + if (params->token == NULL && data_total > 0) { + layoutEthereumData(params->data_initial_chunk_bytes, + params->data_initial_chunk_size, data_total); + if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + return false; + } + } + + return true; +} + +void ethereum_signing_init(const EthereumSignTx *msg, const HDNode *node) { + struct signing_params params = { + .chain_id = msg->chain_id, + + .data_length = msg->data_length, + .data_initial_chunk_size = msg->data_initial_chunk.size, + .data_initial_chunk_bytes = msg->data_initial_chunk.bytes, + + .has_to = msg->has_to, + .to = msg->to, + + .value_size = msg->value.size, + .value_bytes = msg->value.bytes, + }; + + eip1559 = false; + if (!ethereum_signing_init_common(¶ms)) { ethereum_signing_abort(); return; } - chain_id = msg->chain_id; + // sanity check that fee doesn't overflow + if (msg->gas_price.size + msg->gas_limit.size > 30) { + fsm_sendFailure(FailureType_Failure_DataError, _("Safety check failed")); + ethereum_signing_abort(); + return; + } + + uint32_t tx_type = 0; /* Wanchain txtype */ if (msg->has_tx_type) { if (msg->tx_type == 1 || msg->tx_type == 6) { @@ -442,82 +575,19 @@ void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node) { ethereum_signing_abort(); return; } - } else { - tx_type = 0; } - if (msg->has_data_length && msg->data_length > 0) { - if (!msg->has_data_initial_chunk || msg->data_initial_chunk.size == 0) { - fsm_sendFailure(FailureType_Failure_DataError, - _("Data length provided, but no initial chunk")); - ethereum_signing_abort(); - return; - } - /* Our encoding only supports transactions up to 2^24 bytes. To - * prevent exceeding the limit we use a stricter limit on data length. - */ - if (msg->data_length > 16000000) { - fsm_sendFailure(FailureType_Failure_DataError, - _("Data length exceeds limit")); - ethereum_signing_abort(); - return; - } - data_total = msg->data_length; - } else { - data_total = 0; - } - if (msg->data_initial_chunk.size > data_total) { - fsm_sendFailure(FailureType_Failure_DataError, - _("Invalid size of initial chunk")); - ethereum_signing_abort(); - return; - } + ethereum_signing_handle_erc20(¶ms); - // safety checks - if (!ethereum_signing_check(msg)) { - fsm_sendFailure(FailureType_Failure_DataError, _("Safety check failed")); - ethereum_signing_abort(); - return; - } - - const TokenType *token = NULL; - - // detect ERC-20 token - if (toset && msg->value.size == 0 && data_total == 68 && - msg->data_initial_chunk.size == 68 && - memcmp(msg->data_initial_chunk.bytes, - "\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - 16) == 0) { - token = tokenByChainAddress(chain_id, pubkeyhash); - } - - if (token != NULL) { - layoutEthereumConfirmTx(msg->data_initial_chunk.bytes + 16, 20, - msg->data_initial_chunk.bytes + 36, 32, token); - } else { - layoutEthereumConfirmTx(pubkeyhash, 20, msg->value.bytes, msg->value.size, - NULL); - } - - if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + if (!ethereum_signing_confirm_common(¶ms)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); ethereum_signing_abort(); return; } - if (token == NULL && data_total > 0) { - layoutEthereumData(msg->data_initial_chunk.bytes, - msg->data_initial_chunk.size, data_total); - if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { - fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); - ethereum_signing_abort(); - return; - } - } - layoutEthereumFee(msg->value.bytes, msg->value.size, msg->gas_price.bytes, msg->gas_price.size, msg->gas_limit.bytes, - msg->gas_limit.size, token != NULL); + msg->gas_limit.size, params.token != NULL); if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); ethereum_signing_abort(); @@ -534,10 +604,11 @@ void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node) { rlp_calculate_length(msg->gas_price.size, msg->gas_price.bytes[0]); rlp_length += rlp_calculate_length(msg->gas_limit.size, msg->gas_limit.bytes[0]); - rlp_length += rlp_calculate_length(toset ? 20 : 0, pubkeyhash[0]); - rlp_length += rlp_calculate_length(msg->value.size, msg->value.bytes[0]); + rlp_length += rlp_calculate_length(params.pubkeyhash_set ? 20 : 0, + params.pubkeyhash[0]); + rlp_length += rlp_calculate_length(params.value_size, params.value_bytes[0]); rlp_length += - rlp_calculate_length(data_total, msg->data_initial_chunk.bytes[0]); + rlp_calculate_length(data_total, params.data_initial_chunk_bytes[0]); if (tx_type) { rlp_length += rlp_calculate_number_length(tx_type); } @@ -556,11 +627,122 @@ void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node) { hash_rlp_field(msg->nonce.bytes, msg->nonce.size); hash_rlp_field(msg->gas_price.bytes, msg->gas_price.size); hash_rlp_field(msg->gas_limit.bytes, msg->gas_limit.size); - hash_rlp_field(pubkeyhash, toset ? 20 : 0); - hash_rlp_field(msg->value.bytes, msg->value.size); - hash_rlp_length(data_total, msg->data_initial_chunk.bytes[0]); - hash_data(msg->data_initial_chunk.bytes, msg->data_initial_chunk.size); - data_left = data_total - msg->data_initial_chunk.size; + hash_rlp_field(params.pubkeyhash, params.pubkeyhash_set ? 20 : 0); + hash_rlp_field(params.value_bytes, params.value_size); + hash_rlp_length(data_total, params.data_initial_chunk_bytes[0]); + hash_data(params.data_initial_chunk_bytes, params.data_initial_chunk_size); + data_left = data_total - params.data_initial_chunk_size; + + memcpy(privkey, node->private_key, 32); + + if (data_left > 0) { + send_request_chunk(); + } else { + send_signature(); + } +} + +void ethereum_signing_init_eip1559(const EthereumSignTxEIP1559 *msg, + const HDNode *node) { + struct signing_params params = { + .chain_id = msg->chain_id, + + .data_length = msg->data_length, + .data_initial_chunk_size = msg->data_initial_chunk.size, + .data_initial_chunk_bytes = msg->data_initial_chunk.bytes, + + .has_to = msg->has_to, + .to = msg->to, + + .value_size = msg->value.size, + .value_bytes = msg->value.bytes, + }; + + eip1559 = true; + if (!ethereum_signing_init_common(¶ms)) { + ethereum_signing_abort(); + return; + } + + // sanity check that fee doesn't overflow + if (msg->max_gas_fee.size + msg->gas_limit.size > 30 || + msg->max_priority_fee.size + msg->gas_limit.size > 30) { + fsm_sendFailure(FailureType_Failure_DataError, _("Safety check failed")); + ethereum_signing_abort(); + return; + } + + ethereum_signing_handle_erc20(¶ms); + + if (!ethereum_signing_confirm_common(¶ms)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + ethereum_signing_abort(); + return; + } + + layoutEthereumFeeEIP1559(_("Maximum fee per gas"), msg->max_gas_fee.bytes, + msg->max_gas_fee.size, NULL, 0); + if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + ethereum_signing_abort(); + return; + } + + layoutEthereumFeeEIP1559(_("Priority fee per gas"), + msg->max_priority_fee.bytes, + msg->max_priority_fee.size, NULL, 0); + if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + ethereum_signing_abort(); + return; + } + + layoutEthereumFeeEIP1559(_("Maximum fee"), msg->gas_limit.bytes, + msg->gas_limit.size, msg->max_gas_fee.bytes, + msg->max_gas_fee.size); + if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + ethereum_signing_abort(); + return; + } + + /* Stage 1: Calculate total RLP length */ + uint32_t rlp_length = 0; + + layoutProgress(_("Signing"), 0); + + rlp_length += rlp_calculate_number_length(chain_id); + rlp_length += rlp_calculate_length(msg->nonce.size, msg->nonce.bytes[0]); + rlp_length += rlp_calculate_length(msg->max_priority_fee.size, + msg->max_priority_fee.bytes[0]); + rlp_length += + rlp_calculate_length(msg->max_gas_fee.size, msg->max_gas_fee.bytes[0]); + rlp_length += + rlp_calculate_length(msg->gas_limit.size, msg->gas_limit.bytes[0]); + rlp_length += rlp_calculate_length(params.pubkeyhash_set ? 20 : 0, + params.pubkeyhash[0]); + rlp_length += rlp_calculate_length(params.value_size, params.value_bytes[0]); + rlp_length += + rlp_calculate_length(data_total, params.data_initial_chunk_bytes[0]); + + rlp_length += rlp_calculate_length(0, 0xff); + + /* Stage 2: Store header fields */ + hash_rlp_number(EIP1559_TX_TYPE); + hash_rlp_list_length(rlp_length); + + layoutProgress(_("Signing"), 100); + + hash_rlp_number(chain_id); + hash_rlp_field(msg->nonce.bytes, msg->nonce.size); + hash_rlp_field(msg->max_priority_fee.bytes, msg->max_priority_fee.size); + hash_rlp_field(msg->max_gas_fee.bytes, msg->max_gas_fee.size); + hash_rlp_field(msg->gas_limit.bytes, msg->gas_limit.size); + hash_rlp_field(params.pubkeyhash, params.pubkeyhash_set ? 20 : 0); + hash_rlp_field(params.value_bytes, params.value_size); + hash_rlp_length(data_total, params.data_initial_chunk_bytes[0]); + hash_data(params.data_initial_chunk_bytes, params.data_initial_chunk_size); + data_left = data_total - params.data_initial_chunk_size; memcpy(privkey, node->private_key, 32); diff --git a/legacy/firmware/ethereum.h b/legacy/firmware/ethereum.h index a970e71db3..2dd406e7b4 100644 --- a/legacy/firmware/ethereum.h +++ b/legacy/firmware/ethereum.h @@ -25,7 +25,9 @@ #include "bip32.h" #include "messages-ethereum.pb.h" -void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node); +void ethereum_signing_init(const EthereumSignTx *msg, const HDNode *node); +void ethereum_signing_init_eip1559(const EthereumSignTxEIP1559 *msg, + const HDNode *node); void ethereum_signing_abort(void); void ethereum_signing_txack(const EthereumTxAck *msg); diff --git a/legacy/firmware/fsm.h b/legacy/firmware/fsm.h index 45d865ab26..156057c87b 100644 --- a/legacy/firmware/fsm.h +++ b/legacy/firmware/fsm.h @@ -99,9 +99,7 @@ void fsm_msgDebugLinkFlashErase(const DebugLinkFlashErase *msg); // ethereum void fsm_msgEthereumGetAddress(const EthereumGetAddress *msg); void fsm_msgEthereumGetPublicKey(const EthereumGetPublicKey *msg); -void fsm_msgEthereumSignTx( - EthereumSignTx - *msg); // not const because we mutate transaction during validation +void fsm_msgEthereumSignTx(const EthereumSignTx *msg); void fsm_msgEthereumSignTxEIP1559(const EthereumSignTxEIP1559 *msg); void fsm_msgEthereumTxAck(const EthereumTxAck *msg); void fsm_msgEthereumSignMessage(const EthereumSignMessage *msg); diff --git a/legacy/firmware/fsm_msg_ethereum.h b/legacy/firmware/fsm_msg_ethereum.h index f0eee1ebb0..a5a2f05b9f 100644 --- a/legacy/firmware/fsm_msg_ethereum.h +++ b/legacy/firmware/fsm_msg_ethereum.h @@ -60,7 +60,7 @@ void fsm_msgEthereumGetPublicKey(const EthereumGetPublicKey *msg) { layoutHome(); } -void fsm_msgEthereumSignTx(EthereumSignTx *msg) { +void fsm_msgEthereumSignTx(const EthereumSignTx *msg) { CHECK_INITIALIZED CHECK_PIN @@ -72,13 +72,20 @@ void fsm_msgEthereumSignTx(EthereumSignTx *msg) { ethereum_signing_init(msg, node); } -void fsm_msgEthereumTxAck(const EthereumTxAck *msg) { - ethereum_signing_txack(msg); +void fsm_msgEthereumSignTxEIP1559(const EthereumSignTxEIP1559 *msg) { + CHECK_INITIALIZED + + CHECK_PIN + + const HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + ethereum_signing_init_eip1559(msg, node); } -void fsm_msgEthereumSignTxEIP1559(const EthereumSignTxEIP1559 *msg) { - (void)msg; - fsm_sendFailure(FailureType_Failure_UnexpectedMessage, _("Not implemented")); +void fsm_msgEthereumTxAck(const EthereumTxAck *msg) { + ethereum_signing_txack(msg); } void fsm_msgEthereumGetAddress(const EthereumGetAddress *msg) { diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py index 94f121754a..eaf2e3d193 100644 --- a/tests/device_tests/ethereum/test_signtx.py +++ b/tests/device_tests/ethereum/test_signtx.py @@ -57,7 +57,6 @@ def test_signtx(client, parameters, result): @parametrize_using_common_fixtures("ethereum/sign_tx_eip1559.json") -@pytest.mark.skip_t1 def test_signtx_eip1559(client, parameters, result): with client: sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( @@ -213,7 +212,6 @@ def test_signtx_eip1559_access_list(client): ) -@pytest.mark.skip_t1 def test_signtx_eip1559_access_list_larger(client): with client: @@ -267,7 +265,6 @@ def test_signtx_eip1559_access_list_larger(client): ) -@pytest.mark.skip_t1 def test_sanity_checks_eip1559(client): """Is not vectorized because these are internal-only tests that do not need to be exposed to the public.