diff --git a/src/apdu_parser.c b/src/apdu_parser.c new file mode 100644 index 00000000..e9f48995 --- /dev/null +++ b/src/apdu_parser.c @@ -0,0 +1,261 @@ +/***************************************************************************** + * Ledger + * (c) 2023 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. + *****************************************************************************/ + +#include "os.h" +#include "ux.h" +#include "os_io_seproxyhal.h" +#include "init.h" +#include "io.h" +#include "menu.h" +#include "globals.h" +#include "commands.h" +#include "command_dispatcher.h" +#include "apdu_offsets.h" +#include "swap_errors.h" +#include "apdu_parser.h" + +// Keep as global the received fields of the command +// We need to remember them in case we receive a split command +typedef struct apdu_s { + bool expecting_more; + uint8_t instruction; + uint8_t rate; + uint8_t subcommand; + uint16_t data_length; + // We could have put the recontructed apdu buffer here but it would increase the RAM usage by + // 512 bytes which is a lot on NANOS + // Instead the recontructed apdu buffer is G_swap_ctx.raw_transaction + // It is unionized with the decoded protobuf transaction requests + // Pro: less memory usage + // Cons: use cautiously and only during the command PROCESS_TRANSACTION_RESPONSE_COMMAND + // The split reception is only made for this command anyway +} apdu_t; +apdu_t G_received_apdu; + +// Dedicated function for instruction checking as it's self contained +static uint16_t check_instruction(uint8_t instruction, uint8_t subcommand) { + bool check_subcommand_context = false; + bool allowed_during_waiting_for_signing = false; + int check_current_state = -1; + + switch (instruction) { + case GET_VERSION_COMMAND: + // We ignore the current context for this command as it doesn't modify anything + check_subcommand_context = false; + // No strict dependancy on the current state as long as it is not a protected state + // (WAITING_USER_VALIDATION and WAITING_SIGNING) + check_current_state = -1; + break; + case START_NEW_TRANSACTION_COMMAND: + // We can always restart a new transaction + allowed_during_waiting_for_signing = true; + check_subcommand_context = false; + check_current_state = -1; + break; + case SET_PARTNER_KEY_COMMAND: + check_current_state = WAITING_TRANSACTION; + check_subcommand_context = true; + break; + case CHECK_PARTNER_COMMAND: + check_current_state = PROVIDER_SET; + check_subcommand_context = true; + break; + case PROCESS_TRANSACTION_RESPONSE_COMMAND: + check_current_state = PROVIDER_CHECKED; + check_subcommand_context = true; + break; + case CHECK_TRANSACTION_SIGNATURE_COMMAND: + check_current_state = TRANSACTION_RECEIVED; + check_subcommand_context = true; + break; + case CHECK_PAYOUT_ADDRESS: + check_current_state = SIGNATURE_CHECKED; + check_subcommand_context = true; + break; + case CHECK_REFUND_ADDRESS: + check_current_state = TO_ADDR_CHECKED; + check_subcommand_context = true; + break; + case START_SIGNING_TRANSACTION: + check_current_state = WAITING_SIGNING; + allowed_during_waiting_for_signing = true; + check_subcommand_context = true; + break; + default: + PRINTF("Received unknown subcommand %d\n", subcommand); + return INVALID_INSTRUCTION; + } + + if (G_swap_ctx.state == WAITING_USER_VALIDATION) { + PRINTF("Refuse all APDUs during UI display\n"); + return UNEXPECTED_INSTRUCTION; + } + + if (!allowed_during_waiting_for_signing && G_swap_ctx.state == WAITING_SIGNING) { + PRINTF("Received subcommand %d, not allowed during WAITING_SIGNING state\n", subcommand); + return UNEXPECTED_INSTRUCTION; + } + + if (check_subcommand_context && subcommand != G_swap_ctx.subcommand) { + PRINTF("Received subcommand %d, current flow is %d\n", subcommand, G_swap_ctx.subcommand); + return UNEXPECTED_INSTRUCTION; + } + + if (check_current_state != -1 && G_swap_ctx.state != check_current_state) { + PRINTF("Received subcommand %d requiring state %d, but current state is %d\n", + subcommand, + check_current_state, + G_swap_ctx.state); + return UNEXPECTED_INSTRUCTION; + } + + return 0; +} + +// Return 0 if we can proceed with this APDU, return a status code if we can not +uint16_t apdu_parser(uint8_t *apdu, size_t apdu_length, command_t *command) { + if (apdu_length < OFFSET_CDATA) { + PRINTF("Error: malformed APDU, length is too short %d\n", apdu_length); + return MALFORMED_APDU; + } + + uint8_t data_length = apdu[OFFSET_LC]; + if (data_length != apdu_length - OFFSET_CDATA) { + PRINTF("Error: malformed APDU, recv %d bytes, claiming %d header bytes and %d data bytes\n", + apdu_length, + OFFSET_CDATA, + data_length); + return MALFORMED_APDU; + } + + if (apdu[OFFSET_CLA] != CLA) { + PRINTF("Error: invalid CLA %d\n", apdu[OFFSET_CLA]); + return CLASS_NOT_SUPPORTED; + } + + // Get rate from P1 + uint8_t rate = apdu[OFFSET_P1]; + if (rate != FIXED && rate != FLOATING) { + PRINTF("Incorrect P1 %d\n", rate); + return WRONG_P1; + } + + // Extract subcommand from P2 + uint8_t subcommand = apdu[OFFSET_P2] & SUBCOMMAND_MASK; + if (subcommand != SWAP && subcommand != SELL && subcommand != FUND && subcommand != SWAP_NG && + subcommand != SELL_NG && subcommand != FUND_NG) { + PRINTF("Incorrect subcommand %d\n", subcommand); + return WRONG_P2_SUBCOMMAND; + } + + // Get instruction and ensure it makes sense in the current context + uint8_t instruction = apdu[OFFSET_INS]; + uint16_t err = check_instruction(instruction, subcommand); + if (err != 0) { + return err; + } + + // Extract extension from P2 + uint8_t extension = apdu[OFFSET_P2] & EXTENSION_MASK; + if ((extension & ~(P2_NONE | P2_MORE | P2_EXTEND)) != 0) { + PRINTF("Incorrect extension %d\n", extension); + return WRONG_P2_EXTENSION; + } + + bool is_first_data_chunk = !(extension & P2_EXTEND); + bool is_last_data_chunk = !(extension & P2_MORE); + bool is_whole_apdu = is_first_data_chunk && is_last_data_chunk; + // Split reception is only for NG flows + if (subcommand != SWAP_NG && subcommand != SELL_NG && subcommand != FUND_NG && !is_whole_apdu) { + PRINTF("Extension %d refused, only allowed for unified flows\n", extension); + return WRONG_P2_EXTENSION; + } + // Split reception is only for PROCESS_TRANSACTION_RESPONSE_COMMAND + if (instruction != PROCESS_TRANSACTION_RESPONSE_COMMAND && !is_whole_apdu) { + PRINTF("Extension %d refused, only allowed for PROCESS_TRANSACTION_RESPONSE instruction\n", + extension); + return WRONG_P2_EXTENSION; + } + + if (is_first_data_chunk) { + G_received_apdu.instruction = instruction; + G_received_apdu.rate = rate; + G_received_apdu.subcommand = subcommand; + G_received_apdu.data_length = data_length; + if (!is_last_data_chunk) { + // Use the raw_transaction buffer as temporary storage. + // It's unionized with the decoded transaction but we are currently handling + // PROCESS_TRANSACTION_RESPONSE_COMMAND instruction (checked previously). + // After this command is done it will contain the decoded PB data, don't use it anymore + memcpy(G_swap_ctx.raw_transaction, apdu + OFFSET_CDATA, data_length); + G_received_apdu.expecting_more = true; + } + } else { + // The command received claims to extend a previously received one. Check if it's expected + if (!G_received_apdu.expecting_more) { + PRINTF("Previous command did not indicate a followup\n"); + return INVALID_P2_EXTENSION; + } + // Also ensure they are the same kind + if (G_received_apdu.instruction != instruction || G_received_apdu.rate != rate || + G_received_apdu.subcommand != subcommand) { + PRINTF("Refusing to extend a different apdu, exp (%d, %d, %d), recv (%d, %d, %d)\n", + G_received_apdu.instruction, + G_received_apdu.rate, + G_received_apdu.subcommand, + instruction, + rate, + subcommand); + return INVALID_P2_EXTENSION; + } + if (G_received_apdu.data_length + data_length > sizeof(G_swap_ctx.raw_transaction)) { + PRINTF("Reception buffer size %d is not sufficient to receive more data (%d + %d)\n", + sizeof(G_swap_ctx.raw_transaction), + G_received_apdu.data_length, + data_length); + return INVALID_P2_EXTENSION; + } + // Extend the already received buffer + memcpy(G_swap_ctx.raw_transaction + G_received_apdu.data_length, + apdu + OFFSET_CDATA, + data_length); + G_received_apdu.data_length += data_length; + } + + if (!is_last_data_chunk) { + // Reply a blank success to indicate that we await the followup part + // Do NOT update any kind of internal state machine, we have not validated what we have + // received + PRINTF("Split APDU successfully initiated, size %d\n", G_received_apdu.data_length); + return SUCCESS; + } else { + // The APDU is valid and complete, signal caller that it can proceed + command->ins = G_received_apdu.instruction; + command->rate = G_received_apdu.rate; + command->subcommand = G_received_apdu.subcommand; + command->data.size = G_received_apdu.data_length; + if (is_whole_apdu) { + // No split has taken place, data is still in the APDU reception buffer + command->data.bytes = apdu + OFFSET_CDATA; + } else { + // Split has taken place, data is in the split buffer + PRINTF("Split APDU successfully recreated, size %d\n", G_received_apdu.data_length); + command->data.bytes = G_swap_ctx.raw_transaction; + } + return 0; + } +} diff --git a/src/apdu_parser.h b/src/apdu_parser.h new file mode 100644 index 00000000..d5f2d64e --- /dev/null +++ b/src/apdu_parser.h @@ -0,0 +1,5 @@ +#pragma once + +#include "commands.h" + +uint16_t apdu_parser(uint8_t *apdu, size_t apdu_length, command_t *command); diff --git a/src/buffer.c b/src/buffer.c index 75a4d9e1..0aeb4660 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -1,21 +1,42 @@ #include "buffer.h" +#include "os.h" // Parse a buffer at a given offset to read a buf_t, the offset is incremented accordingly // param[in] the total buffer to parse // param[in] the total size of the buffer to parse // param[out] the buf_t read from in_buffer at offset, can by 0 sized // param[in/out] the current offset at wich we are in -bool parse_to_sized_buffer(uint8_t *in_buffer, uint16_t in_size, buf_t *out, uint16_t *offset) { - if (*offset + 1 > in_size) { +bool parse_to_sized_buffer(uint8_t *in_buffer, + uint16_t in_size, + uint8_t size_of_lenght_field, + buf_t *out, + uint16_t *offset) { + if (*offset + size_of_lenght_field > in_size) { // We can't even read the size + PRINTF("Failed to read the header sized %d, only %d bytes available\n", + size_of_lenght_field, + in_size); return false; } // Read the size - out->size = in_buffer[*offset]; - ++*offset; + if (size_of_lenght_field == 1) { + out->size = in_buffer[*offset]; + } else if (size_of_lenght_field == 2) { + out->size = U2BE(in_buffer, *offset); + } else if (size_of_lenght_field == 4) { + out->size = U4BE(in_buffer, *offset); + } else { + PRINTF("Unable to read a %d sized header\n", size_of_lenght_field); + return false; + } + *offset += size_of_lenght_field; + if (*offset + out->size > in_size) { - // Not enough bytes to read buffer size + PRINTF("Not enough remaining bytes to read. Total %d, offset %d, claims %d bytes\n", + in_size, + *offset, + out->size); return false; } diff --git a/src/buffer.h b/src/buffer.h index 018f137e..ee1dfea7 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -9,4 +9,8 @@ typedef struct buf_s { uint16_t size; } buf_t; -bool parse_to_sized_buffer(uint8_t *in_buffer, uint16_t in_size, buf_t *out, uint16_t *offset); +bool parse_to_sized_buffer(uint8_t *in_buffer, + uint16_t in_size, + uint8_t size_of_lenght_field, + buf_t *out, + uint16_t *offset); diff --git a/src/check_addresses_and_amounts.c b/src/check_addresses_and_amounts.c new file mode 100644 index 00000000..655dd1b0 --- /dev/null +++ b/src/check_addresses_and_amounts.c @@ -0,0 +1,281 @@ +#include +#include + +#include "swap_errors.h" +#include "globals.h" +#include "currency_lib_calls.h" +#include "io.h" +#include "parse_check_address_message.h" +#include "parse_coin_config.h" +#include "printable_amount.h" +#include "validate_transaction.h" +#include "menu.h" +#include "pb_structs.h" +#include "ticker_normalization.h" + +#include "check_addresses_and_amounts.h" + +static bool check_coin_configuration_signature(buf_t config, buf_t der) { + uint8_t hash[CURVE_SIZE_BYTES]; + cx_hash_sha256(config.bytes, config.size, hash, CURVE_SIZE_BYTES); + return cx_ecdsa_verify(&G_swap_ctx.ledger_public_key, + CX_LAST, + CX_SHA256, + hash, + CURVE_SIZE_BYTES, + der.bytes, + der.size); +} + +static bool check_received_ticker_matches_context(buf_t ticker, const command_t *cmd) { + char *in_currency; + if (cmd->subcommand == SWAP || cmd->subcommand == SWAP_NG) { + if (cmd->ins == CHECK_PAYOUT_ADDRESS) { + in_currency = G_swap_ctx.swap_transaction.currency_to; + } else { + in_currency = G_swap_ctx.swap_transaction.currency_from; + } + } else if (cmd->subcommand == SELL || cmd->subcommand == SELL_NG) { + in_currency = G_swap_ctx.sell_transaction.in_currency; + } else { + in_currency = G_swap_ctx.fund_transaction.in_currency; + } + return check_matching_ticker(ticker, in_currency); +} + +static uint16_t check_payout_or_refund_address(command_e ins, + buf_t sub_coin_config, + buf_t address_parameters, + char *appname) { + uint8_t address_max_size; + char *address_to_check; + char *extra_id_to_check; + + // Depending on the current command, check either PAYOUT or REFUND + if (ins == CHECK_PAYOUT_ADDRESS) { + address_to_check = G_swap_ctx.swap_transaction.payout_address; + address_max_size = sizeof(G_swap_ctx.swap_transaction.payout_address); + extra_id_to_check = G_swap_ctx.swap_transaction.payout_extra_id; + } else { + address_to_check = G_swap_ctx.swap_transaction.refund_address; + address_max_size = sizeof(G_swap_ctx.swap_transaction.refund_address); + extra_id_to_check = G_swap_ctx.swap_transaction.refund_extra_id; + } + if (address_to_check[address_max_size - 1] != '\0') { + PRINTF("Address to check is not NULL terminated\n"); + return INCORRECT_COMMAND_DATA; + } + + if (check_address(&sub_coin_config, + &address_parameters, + appname, + address_to_check, + extra_id_to_check) != 1) { + PRINTF("Error: Address validation failed\n"); + return INVALID_ADDRESS; + } + return 0; +} + +static bool format_relevant_amount(command_e ins, buf_t sub_coin_config, char *appname) { + pb_bytes_array_16_t *amount; + char *dest; + uint8_t dest_size; + if (G_swap_ctx.subcommand == SWAP || G_swap_ctx.subcommand == SWAP_NG) { + if (ins == CHECK_PAYOUT_ADDRESS) { + amount = (pb_bytes_array_16_t *) &G_swap_ctx.swap_transaction.amount_to_wallet; + dest = G_swap_ctx.printable_get_amount; + dest_size = sizeof(G_swap_ctx.printable_get_amount); + } else { + amount = (pb_bytes_array_16_t *) &G_swap_ctx.swap_transaction.amount_to_provider; + dest = G_swap_ctx.printable_send_amount; + dest_size = sizeof(G_swap_ctx.printable_send_amount); + } + } else if (G_swap_ctx.subcommand == SELL || G_swap_ctx.subcommand == SELL_NG) { + amount = (pb_bytes_array_16_t *) &G_swap_ctx.sell_transaction.in_amount; + dest = G_swap_ctx.printable_send_amount; + dest_size = sizeof(G_swap_ctx.printable_send_amount); + } else { + amount = (pb_bytes_array_16_t *) &G_swap_ctx.fund_transaction.in_amount; + dest = G_swap_ctx.printable_send_amount; + dest_size = sizeof(G_swap_ctx.printable_send_amount); + } + if (get_printable_amount(&sub_coin_config, + appname, + amount->bytes, + amount->size, + dest, + dest_size, + false) < 0) { + PRINTF("Error: Failed to get printable amount\n"); + return false; + } + PRINTF("Formatted amount: %s\n", dest); + return true; +} + +static bool format_fees(buf_t sub_coin_config, char *appname) { + if (get_printable_amount(&sub_coin_config, + appname, + G_swap_ctx.transaction_fee, + G_swap_ctx.transaction_fee_length, + G_swap_ctx.printable_fees_amount, + sizeof(G_swap_ctx.printable_fees_amount), + true) < 0) { + PRINTF("Error: Failed to get printable fees amount\n"); + return false; + } + PRINTF("Fees: %s\n", G_swap_ctx.printable_fees_amount); + return true; +} + +static bool format_fiat_amount(void) { + size_t len = strlen(G_swap_ctx.sell_transaction.out_currency); + if (len + 1 >= sizeof(G_swap_ctx.printable_get_amount)) { + return false; + } + + strncpy(G_swap_ctx.printable_get_amount, + G_swap_ctx.sell_transaction.out_currency, + sizeof(G_swap_ctx.printable_get_amount)); + G_swap_ctx.printable_get_amount[len] = ' '; + G_swap_ctx.printable_get_amount[len + 1] = '\x00'; + + if (get_fiat_printable_amount(G_swap_ctx.sell_transaction.out_amount.coefficient.bytes, + G_swap_ctx.sell_transaction.out_amount.coefficient.size, + G_swap_ctx.sell_transaction.out_amount.exponent, + G_swap_ctx.printable_get_amount + len + 1, + sizeof(G_swap_ctx.printable_get_amount) - (len + 1)) < 0) { + PRINTF("Error: Failed to get source currency printable amount\n"); + return false; + } + + PRINTF("%s\n", G_swap_ctx.printable_get_amount); + return true; +} + +static void format_account_name(void) { + strncpy(G_swap_ctx.account_name, + G_swap_ctx.fund_transaction.account_name, + sizeof(G_swap_ctx.account_name)); + G_swap_ctx.account_name[sizeof(G_swap_ctx.account_name) - 1] = '\x00'; +} + +// Three possibilities in this function: +// - we are in CHECK_ASSET_IN (FUND or SELL flows) +// - we will ask the FROM app to format the FROM amount and the fees +// - we will format the FIAT amount TO (SELL flow) +// - we will format the UI account field (FUND flow) +// - we are in CHECK_PAYOUT_ADDRESS (SWAP flow) +// - we will ask the TO app to format the TO amount +// - we will ask the TO app to check the payout address +// - we are in CHECK_REFUND_ADDRESS (SWAP flow) +// - we will ask the FROM app to format the FROM amount and the fees +// - we will ask the FROM app to check the refund address +int check_addresses_and_amounts(const command_t *cmd) { + buf_t config; + buf_t der; + buf_t address_parameters; + buf_t ticker; + buf_t parsed_application_name; + buf_t sub_coin_config; + char application_name[BOLOS_APPNAME_MAX_SIZE_B + 1]; + + if (parse_check_address_message(cmd, &config, &der, &address_parameters) == 0) { + PRINTF("Error: Can't parse command\n"); + return reply_error(INCORRECT_COMMAND_DATA); + } + + // We received the coin configuration from the CAL and its signature. Check the signature + if (!check_coin_configuration_signature(config, der)) { + PRINTF("Error: Fail to verify signature of coin config\n"); + return reply_error(SIGN_VERIFICATION_FAIL); + } + + // Break up the configuration into its individual elements + if (parse_coin_config(config, &ticker, &parsed_application_name, &sub_coin_config) == 0) { + PRINTF("Error: Can't parse coin config command\n"); + return reply_error(INCORRECT_COMMAND_DATA); + } + // We can't use the pointer to the parsed application name as it is not NULL terminated + // We have to make a local copy + memset(application_name, 0, sizeof(application_name)); + memcpy(application_name, parsed_application_name.bytes, parsed_application_name.size); + + // Ensure we received a coin configuration that actually serves us in the current TX context + if (!check_received_ticker_matches_context(ticker, cmd)) { + PRINTF("Error: received ticker doesn't match saved ticker\n"); + return reply_error(INCORRECT_COMMAND_DATA); + } + + // On SWAP flows we need to check refund or payout address (depending on step) + // We received them as part of the TX but we couldn't check then as we did not have the + // application_name yet + if (G_swap_ctx.subcommand == SWAP || G_swap_ctx.subcommand == SWAP_NG) { + uint16_t ret = check_payout_or_refund_address(cmd->ins, + sub_coin_config, + address_parameters, + application_name); + if (ret != 0) { + return reply_error(ret); + } + } + + // Call the lib app to format the amount according to its coin. + // It can be the OUT going amount or IN coming amount for SWAP + if (!format_relevant_amount(cmd->ins, sub_coin_config, application_name)) { + PRINTF("Error: Failed to format printable amount\n"); + return reply_error(INTERNAL_ERROR); + } + + // Format the fees, except during CHECK_PAYOUT_ADDRESS for SWAP, (it's done in + // CHECK_REFUND_ADDRESS as the fees are in the OUT going currency) + if (!((G_swap_ctx.subcommand == SWAP || G_swap_ctx.subcommand == SWAP_NG) && + cmd->ins == CHECK_PAYOUT_ADDRESS)) { + if (!format_fees(sub_coin_config, application_name)) { + PRINTF("Error: Failed to format fees amount\n"); + return reply_error(INTERNAL_ERROR); + } + } + + // On SELL flows we receive a FIAT amount, format it to display it on screen + if (G_swap_ctx.subcommand == SELL || G_swap_ctx.subcommand == SELL_NG) { + if (!format_fiat_amount()) { + PRINTF("Error: Failed to format FIAT amount\n"); + return reply_error(INTERNAL_ERROR); + } + } + + // On FUND flows we display the account name that will receive the funds + if (G_swap_ctx.subcommand == FUND || G_swap_ctx.subcommand == FUND_NG) { + format_account_name(); + } + + // If we are in a SWAP flow at step CHECK_PAYOUT_ADDRESS, we are still waiting for + // CHECK_REFUND_ADDRESS + // Otherwise we can trigger the UI to get user validation now + if ((G_swap_ctx.subcommand == SWAP || G_swap_ctx.subcommand == SWAP_NG) && + cmd->ins == CHECK_PAYOUT_ADDRESS) { + if (reply_success() < 0) { + PRINTF("Error: failed to send\n"); + return -1; + } + G_swap_ctx.state = TO_ADDR_CHECKED; + } else { + // Save the paying coin application_name, we'll need it to start the app during + // START_SIGNING step + memcpy(G_swap_ctx.payin_binary_name, application_name, sizeof(application_name)); + + // Save the paying sub coin configuration as the lib app will need it to sign + G_swap_ctx.paying_sub_coin_config_size = sub_coin_config.size; + memset(G_swap_ctx.paying_sub_coin_config, 0, sizeof(G_swap_ctx.paying_sub_coin_config)); + memcpy(G_swap_ctx.paying_sub_coin_config, sub_coin_config.bytes, sub_coin_config.size); + + G_swap_ctx.state = WAITING_USER_VALIDATION; + G_swap_ctx.rate = cmd->rate; + + ui_validate_amounts(); + } + + return 0; +} diff --git a/src/check_addresses_and_amounts.h b/src/check_addresses_and_amounts.h new file mode 100644 index 00000000..a92129bf --- /dev/null +++ b/src/check_addresses_and_amounts.h @@ -0,0 +1,5 @@ +#pragma once + +#include "commands.h" + +int check_addresses_and_amounts(const command_t *cmd); diff --git a/src/check_asset_in.c b/src/check_asset_in.c deleted file mode 100644 index c19a38b8..00000000 --- a/src/check_asset_in.c +++ /dev/null @@ -1,140 +0,0 @@ -#include -#include - -#include "check_asset_in.h" -#include "swap_errors.h" -#include "globals.h" -#include "currency_lib_calls.h" -#include "io.h" -#include "parse_check_address_message.h" -#include "parse_coin_config.h" -#include "printable_amount.h" -#include "validate_transaction.h" -#include "menu.h" -#include "pb_structs.h" -#include "ticker_normalization.h" - -int check_asset_in(const command_t *cmd) { - static buf_t config; - static buf_t der; - static buf_t address_parameters; - static buf_t ticker; - static buf_t application_name; - - if (parse_check_address_message(cmd, &config, &der, &address_parameters) == 0) { - PRINTF("Error: Can't parse CHECK_ASSET_IN command\n"); - - return reply_error(INCORRECT_COMMAND_DATA); - } - - uint8_t hash[CURVE_SIZE_BYTES]; - - cx_hash_sha256(config.bytes, config.size, hash, CURVE_SIZE_BYTES); - - if (cx_ecdsa_verify(&G_swap_ctx.ledger_public_key, - CX_LAST, - CX_SHA256, - hash, - CURVE_SIZE_BYTES, - der.bytes, - der.size) == 0) { - PRINTF("Error: Fail to verify signature of coin config\n"); - - return reply_error(SIGN_VERIFICATION_FAIL); - } - - if (parse_coin_config(&config, &ticker, &application_name, &G_swap_ctx.payin_coin_config) == - 0) { - PRINTF("Error: Can't parse CRYPTO coin config command\n"); - - return reply_error(INCORRECT_COMMAND_DATA); - } - - // Check that given ticker match current context - char *in_currency = (G_swap_ctx.subcommand == SELL ? G_swap_ctx.sell_transaction.in_currency - : G_swap_ctx.fund_transaction.in_currency); - - // Check that ticker matches the current context - if (!check_matching_ticker(&ticker, in_currency)) { - PRINTF("Error: ticker doesn't match configuration ticker\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - - PRINTF("Coin configuration parsed: OK\n"); - - // creating 0-terminated application name - memset(G_swap_ctx.payin_binary_name, 0, sizeof(G_swap_ctx.payin_binary_name)); - memcpy(G_swap_ctx.payin_binary_name, PIC(application_name.bytes), application_name.size); - - PRINTF("PATH inside the SWAP = %.*H\n", address_parameters.size, address_parameters.bytes); - - pb_bytes_array_16_t *in_amount; - if (G_swap_ctx.subcommand == SELL) { - in_amount = (pb_bytes_array_16_t *) &G_swap_ctx.sell_transaction.in_amount; - } else { - in_amount = (pb_bytes_array_16_t *) &G_swap_ctx.fund_transaction.in_amount; - } - - // getting printable amount - if (get_printable_amount(&G_swap_ctx.payin_coin_config, - G_swap_ctx.payin_binary_name, - (uint8_t *) in_amount->bytes, - in_amount->size, - G_swap_ctx.printable_send_amount, - sizeof(G_swap_ctx.printable_send_amount), - false) < 0) { - PRINTF("Error: Failed to get CRYPTO currency printable amount\n"); - - return reply_error(INTERNAL_ERROR); - } - - PRINTF("Amount = %s\n", G_swap_ctx.printable_send_amount); - - if (get_printable_amount(&G_swap_ctx.payin_coin_config, - G_swap_ctx.payin_binary_name, - (uint8_t *) G_swap_ctx.transaction_fee, - G_swap_ctx.transaction_fee_length, - G_swap_ctx.printable_fees_amount, - sizeof(G_swap_ctx.printable_fees_amount), - true) < 0) { - PRINTF("Error: Failed to get CRYPTO currency fees amount"); - return reply_error(INTERNAL_ERROR); - } - - if (cmd->subcommand == SELL) { - size_t len = strlen(G_swap_ctx.sell_transaction.out_currency); - if (len + 1 >= sizeof(G_swap_ctx.printable_get_amount)) { - return reply_error(INTERNAL_ERROR); - } - - strncpy(G_swap_ctx.printable_get_amount, - G_swap_ctx.sell_transaction.out_currency, - sizeof(G_swap_ctx.printable_get_amount)); - G_swap_ctx.printable_get_amount[len] = ' '; - G_swap_ctx.printable_get_amount[len + 1] = '\x00'; - - if (get_fiat_printable_amount(G_swap_ctx.sell_transaction.out_amount.coefficient.bytes, - G_swap_ctx.sell_transaction.out_amount.coefficient.size, - G_swap_ctx.sell_transaction.out_amount.exponent, - G_swap_ctx.printable_get_amount + len + 1, - sizeof(G_swap_ctx.printable_get_amount) - (len + 1)) < 0) { - PRINTF("Error: Failed to get source currency printable amount\n"); - return reply_error(INTERNAL_ERROR); - } - - PRINTF("%s\n", G_swap_ctx.printable_get_amount); - } else { - // Prepare message for account funding - strncpy(G_swap_ctx.printable_get_amount, - G_swap_ctx.fund_transaction.account_name, - sizeof(G_swap_ctx.printable_get_amount)); - G_swap_ctx.printable_get_amount[sizeof(G_swap_ctx.printable_get_amount) - 1] = '\x00'; - } - - G_swap_ctx.state = WAITING_USER_VALIDATION; - G_swap_ctx.rate = cmd->rate; - - ui_validate_amounts(); - - return 0; -} diff --git a/src/check_asset_in.h b/src/check_asset_in.h deleted file mode 100644 index 20535277..00000000 --- a/src/check_asset_in.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "commands.h" - -int check_asset_in(const command_t *cmd); diff --git a/src/check_partner.c b/src/check_partner.c index 2f0aac98..5edc4fb9 100644 --- a/src/check_partner.c +++ b/src/check_partner.c @@ -12,7 +12,6 @@ int check_partner(const command_t *cmd) { if (cmd->data.size < MIN_DER_SIGNATURE_LENGTH || cmd->data.size > MAX_DER_SIGNATURE_LENGTH) { PRINTF("Error: Input buffer length don't correspond to DER length\n"); - return reply_error(INCORRECT_COMMAND_DATA); } @@ -24,7 +23,6 @@ int check_partner(const command_t *cmd) { cmd->data.bytes, cmd->data.size) == 0) { PRINTF("Error: Failed to verify signature of partner data\n"); - return reply_error(SIGN_VERIFICATION_FAIL); } diff --git a/src/check_payout_address.c b/src/check_payout_address.c deleted file mode 100644 index 3887451e..00000000 --- a/src/check_payout_address.c +++ /dev/null @@ -1,104 +0,0 @@ -#include -#include - -#include "check_payout_address.h" -#include "swap_errors.h" -#include "globals.h" -#include "currency_lib_calls.h" -#include "io.h" -#include "parse_check_address_message.h" -#include "parse_coin_config.h" -#include "printable_amount.h" -#include "menu.h" -#include "ticker_normalization.h" - -int check_payout_address(const command_t *cmd) { - static buf_t config; - static buf_t der; - static buf_t address_parameters; - static buf_t ticker; - static buf_t application_name; - - if (parse_check_address_message(cmd, &config, &der, &address_parameters) == 0) { - PRINTF("Error: Can't parse CHECK_PAYOUT_ADDRESS command\n"); - - return reply_error(INCORRECT_COMMAND_DATA); - } - - PRINTF("CHECK_PAYOUT_ADDRESS parsed OK\n"); - - uint8_t hash[CURVE_SIZE_BYTES]; - - cx_hash_sha256(config.bytes, config.size, hash, CURVE_SIZE_BYTES); - - if (cx_ecdsa_verify(&G_swap_ctx.ledger_public_key, - CX_LAST, - CX_SHA256, - hash, - CURVE_SIZE_BYTES, - der.bytes, - der.size) == 0) { - PRINTF("Error: Fail to verify signature of coin config\n"); - return reply_error(SIGN_VERIFICATION_FAIL); - } - - if (parse_coin_config(&config, &ticker, &application_name, &config) == 0) { - PRINTF("Error: Can't parse payout coin config command\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - - // Check that payout ticker matches the current context - if (!check_matching_ticker(&ticker, G_swap_ctx.received_transaction.currency_to)) { - PRINTF("Error: Payout ticker doesn't match configuration ticker\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - - PRINTF("Coin config parsed OK\n"); - - // creating 0-terminated application name - memset(G_swap_ctx.payin_binary_name, 0, sizeof(G_swap_ctx.payin_binary_name)); - memcpy(G_swap_ctx.payin_binary_name, PIC(application_name.bytes), application_name.size); - - PRINTF("PATH inside the SWAP = %.*H\n", address_parameters.size, address_parameters.bytes); - - if (G_swap_ctx.received_transaction - .payout_address[sizeof(G_swap_ctx.received_transaction.payout_address) - 1] != '\0') { - PRINTF("Address to check is not NULL terminated\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - - // check address - if (check_address(&config, - &address_parameters, - G_swap_ctx.payin_binary_name, - G_swap_ctx.received_transaction.payout_address, - G_swap_ctx.received_transaction.payout_extra_id) != 1) { - PRINTF("Error: Payout address validation failed\n"); - return reply_error(INVALID_ADDRESS); - } - - PRINTF("Payout address is OK\n"); - - // getting printable amount - if (get_printable_amount(&config, - G_swap_ctx.payin_binary_name, - (uint8_t *) G_swap_ctx.received_transaction.amount_to_wallet.bytes, - G_swap_ctx.received_transaction.amount_to_wallet.size, - G_swap_ctx.printable_get_amount, - sizeof(G_swap_ctx.printable_get_amount), - false) < 0) { - PRINTF("Error: Failed to get destination currency printable amount\n"); - return reply_error(INTERNAL_ERROR); - } - - PRINTF("Amount = %s\n", G_swap_ctx.printable_get_amount); - - if (reply_success() < 0) { - PRINTF("Error: failed to send\n"); - return -1; - } - - G_swap_ctx.state = TO_ADDR_CHECKED; - - return 0; -} diff --git a/src/check_payout_address.h b/src/check_payout_address.h deleted file mode 100644 index 62969c25..00000000 --- a/src/check_payout_address.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "commands.h" - -int check_payout_address(const command_t *cmd); diff --git a/src/check_refund_address.c b/src/check_refund_address.c deleted file mode 100644 index dcb3ac80..00000000 --- a/src/check_refund_address.c +++ /dev/null @@ -1,110 +0,0 @@ -#include -#include - -#include "check_refund_address.h" -#include "currency_lib_calls.h" -#include "globals.h" -#include "swap_errors.h" -#include "io.h" -#include "parse_check_address_message.h" -#include "menu.h" -#include "validate_transaction.h" -#include "process_transaction.h" -#include "parse_coin_config.h" -#include "ticker_normalization.h" - -int check_refund_address(const command_t *cmd) { - static buf_t config; - static buf_t der; - static buf_t address_parameters; - static buf_t ticker; - static buf_t application_name; - - if (parse_check_address_message(cmd, &config, &der, &address_parameters) == 0) { - return reply_error(INCORRECT_COMMAND_DATA); - } - - uint8_t hash[CURVE_SIZE_BYTES]; - - memset(hash, 0, sizeof(hash)); - - cx_hash_sha256(config.bytes, config.size, hash, CURVE_SIZE_BYTES); - - if (cx_ecdsa_verify(&G_swap_ctx.ledger_public_key, - CX_LAST, - CX_SHA256, - hash, - CURVE_SIZE_BYTES, - der.bytes, - der.size) == 0) { - PRINTF("Error: Fail to verify signature of coin config\n"); - - return reply_error(SIGN_VERIFICATION_FAIL); - } - - if (parse_coin_config(&config, &ticker, &application_name, &G_swap_ctx.payin_coin_config) == - 0) { - PRINTF("Error: Can't parse refund coin config command\n"); - - return reply_error(INCORRECT_COMMAND_DATA); - } - - // Check that refund ticker matches the current context - if (!check_matching_ticker(&ticker, G_swap_ctx.received_transaction.currency_from)) { - PRINTF("Error: Refund ticker doesn't match configuration ticker\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - - // creating 0-terminated application name - memset(G_swap_ctx.payin_binary_name, 0, sizeof(G_swap_ctx.payin_binary_name)); - memcpy(G_swap_ctx.payin_binary_name, PIC(application_name.bytes), application_name.size); - - if (G_swap_ctx.received_transaction - .refund_address[sizeof(G_swap_ctx.received_transaction.refund_address) - 1] != '\0') { - PRINTF("Address to check is not NULL terminated\n"); - return reply_error(INCORRECT_COMMAND_DATA); - } - // check address - if (check_address(&G_swap_ctx.payin_coin_config, - &address_parameters, - G_swap_ctx.payin_binary_name, - G_swap_ctx.received_transaction.refund_address, - G_swap_ctx.received_transaction.refund_extra_id) != 1) { - PRINTF("Error: Refund address validation failed\n"); - - return reply_error(INVALID_ADDRESS); - } - - if (get_printable_amount(&G_swap_ctx.payin_coin_config, - G_swap_ctx.payin_binary_name, - G_swap_ctx.received_transaction.amount_to_provider.bytes, - G_swap_ctx.received_transaction.amount_to_provider.size, - G_swap_ctx.printable_send_amount, - sizeof(G_swap_ctx.printable_send_amount), - false) < 0) { - PRINTF("Error: Failed to get source currency printable amount\n"); - - return reply_error(INTERNAL_ERROR); - } - PRINTF("Send amount: %s\n", G_swap_ctx.printable_send_amount); - - if (get_printable_amount(&G_swap_ctx.payin_coin_config, - G_swap_ctx.payin_binary_name, - G_swap_ctx.transaction_fee, - G_swap_ctx.transaction_fee_length, - G_swap_ctx.printable_fees_amount, - sizeof(G_swap_ctx.printable_fees_amount), - true) < 0) { - PRINTF("Error: Failed to get source currency fees amount\n"); - - return reply_error(INTERNAL_ERROR); - } - PRINTF("Fees: %s\n", G_swap_ctx.printable_fees_amount); - - G_swap_ctx.state = WAITING_USER_VALIDATION; - G_swap_ctx.rate = cmd->rate; - - ui_validate_amounts(); - - return 0; -} diff --git a/src/check_refund_address.h b/src/check_refund_address.h deleted file mode 100644 index 5d241eaf..00000000 --- a/src/check_refund_address.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "commands.h" - -int check_refund_address(const command_t *cmd); diff --git a/src/check_tx_signature.c b/src/check_tx_signature.c index 4e05d9e4..bc2b411b 100644 --- a/src/check_tx_signature.c +++ b/src/check_tx_signature.c @@ -7,63 +7,76 @@ #include "io.h" #include "der.h" -#define MAX_DER_INT_SIZE(size) (1 + 3 + (size) + 1) +// One int (r or s) in DER is (1 byte prefix + 1 byte r/s length + r/s + 1 optional ending byte) +// Keep the legacy additional '+2' just in case, it has no side effect. +#define MAX_DER_INT_SIZE(size) (1 + 1 + (size) + 1 + 2) +#define R_S_INT_SIZE 32 +#define DER_HEADER_SIZE 2 +#define DER_OFFSET_LENGTH 1 // This function receive transaction signature // Input should be in the form of DER serialized signature // the length should be CURVE_SIZE_BYTES * 2 + 6 (DER encoding) int check_tx_signature(const command_t *cmd) { + // If we received the data in (R,S) format we will encode it here in DER before veryfing the + // signature + uint8_t der_sig[DER_HEADER_SIZE + MAX_DER_INT_SIZE(R_S_INT_SIZE) * 2]; + buf_t signature; + if (cmd->subcommand == SWAP || cmd->subcommand == FUND) { + // We received the signature in DER format, just perform some sanity checks if (cmd->data.size < MIN_DER_SIGNATURE_LENGTH || - cmd->data.size > MAX_DER_SIGNATURE_LENGTH || cmd->data.bytes[1] + 2 != cmd->data.size) { + cmd->data.size > MAX_DER_SIGNATURE_LENGTH) { PRINTF("Error: Input buffer length don't correspond to DER length\n"); return reply_error(INCORRECT_COMMAND_DATA); } - if (cx_ecdsa_verify(&G_swap_ctx.partner.public_key, - CX_LAST, - CX_SHA256, - G_swap_ctx.sha256_digest, - CURVE_SIZE_BYTES, - cmd->data.bytes, - cmd->data.size) == 0) { - PRINTF("Error: Failed to verify signature of received transaction\n"); - return reply_error(SIGN_VERIFICATION_FAIL); - } - } - if (cmd->subcommand == SELL) { - if (cmd->data.size != 64) { + uint16_t payload_size = cmd->data.bytes[DER_OFFSET_LENGTH]; + if (payload_size + DER_HEADER_SIZE != cmd->data.size) { + PRINTF("DER signature header advertizes %d bytes, we received %d\n", + payload_size, + cmd->data.size); + return reply_error(INCORRECT_COMMAND_DATA); + } + signature = cmd->data; + } else { + // We received the signature in (R,S) format, perform some sanity checks then encode in DER + if (cmd->data.size != R_S_INT_SIZE * 2) { PRINTF("Error: Input buffer length don't correspond to (R, S) length\n"); return reply_error(INCORRECT_COMMAND_DATA); } - size_t der_r_len = asn1_get_encoded_integer_size(cmd->data.bytes, 32); - size_t der_s_len = asn1_get_encoded_integer_size(cmd->data.bytes + 32, 32); - size_t size = der_r_len + der_s_len + 2; + uint8_t *r = cmd->data.bytes; + uint8_t *s = cmd->data.bytes + R_S_INT_SIZE; + size_t der_r_len = asn1_get_encoded_integer_size(r, R_S_INT_SIZE); + size_t der_s_len = asn1_get_encoded_integer_size(s, R_S_INT_SIZE); + size_t size = der_r_len + der_s_len + DER_HEADER_SIZE; - unsigned char der_sig[MAX_DER_INT_SIZE(32) * 2 + 2]; + // First check the size to ensure it fits and we can cast down if (size > sizeof(der_sig)) { PRINTF("Error: Unexpected der integer size\n"); return reply_error(SIGN_VERIFICATION_FAIL); } - encode_sig_der(der_sig, size, cmd->data.bytes, 32, cmd->data.bytes + 32, 32); + encode_sig_der(der_sig, size, r, R_S_INT_SIZE, s, R_S_INT_SIZE); - PRINTF("DER sig: %.*H\n", size, der_sig); - PRINTF("SHA256(payload): %.*H\n", - sizeof(G_swap_ctx.sha256_digest), - G_swap_ctx.sha256_digest); + signature.size = size; + signature.bytes = der_sig; + } - if (cx_ecdsa_verify(&G_swap_ctx.partner.public_key, - CX_LAST, - CX_SHA256, - G_swap_ctx.sha256_digest, - CURVE_SIZE_BYTES, - der_sig, - size) == 0) { - PRINTF("Error: Failed to verify signature of received transaction\n"); - return reply_error(SIGN_VERIFICATION_FAIL); - } + PRINTF("DER sig: %.*H\n", signature.size, signature.bytes); + PRINTF("SHA256(payload): %.*H\n", sizeof(G_swap_ctx.sha256_digest), G_swap_ctx.sha256_digest); + + // Check the signature of the sha256_digest we computed from the tx payload + if (cx_ecdsa_verify(&G_swap_ctx.partner.public_key, + CX_LAST, + CX_SHA256, + G_swap_ctx.sha256_digest, + CURVE_SIZE_BYTES, + signature.bytes, + signature.size) == 0) { + PRINTF("Error: Failed to verify signature of received transaction\n"); + return reply_error(SIGN_VERIFICATION_FAIL); } if (reply_success() < 0) { diff --git a/src/command_dispatcher.c b/src/command_dispatcher.c index 652d1d5b..688edf24 100644 --- a/src/command_dispatcher.c +++ b/src/command_dispatcher.c @@ -7,97 +7,51 @@ #include "set_partner_key.h" #include "process_transaction.h" #include "check_tx_signature.h" -#include "check_payout_address.h" -#include "check_refund_address.h" -#include "check_asset_in.h" #include "apdu_offsets.h" #include "check_partner.h" #include "start_signing_transaction.h" +#include "check_addresses_and_amounts.h" #include "io.h" int dispatch_command(const command_t *cmd) { - PRINTF("command: %d, subcommand: %d, state: %d\n", cmd->ins, cmd->subcommand, G_swap_ctx.state); - - if (cmd->rate != FIXED && cmd->rate != FLOATING) { - return reply_error(WRONG_P1); - } - if (cmd->subcommand != SWAP && cmd->subcommand != SELL && cmd->subcommand != FUND) { - return reply_error(WRONG_P2); - } + PRINTF("command: %d, subcommand: %d, current state: %d\n", + cmd->ins, + cmd->subcommand, + G_swap_ctx.state); int ret = -1; - bool valid_command_received = false; switch (cmd->ins) { case GET_VERSION_COMMAND: - if (G_swap_ctx.state != WAITING_USER_VALIDATION && - G_swap_ctx.state != WAITING_SIGNING) { - ret = get_version_handler(); - valid_command_received = true; - } + ret = get_version_handler(); break; case START_NEW_TRANSACTION_COMMAND: - if (G_swap_ctx.state != WAITING_USER_VALIDATION) { - ret = start_new_transaction(cmd); - valid_command_received = true; - } + ret = start_new_transaction(cmd); break; case SET_PARTNER_KEY_COMMAND: - if (G_swap_ctx.state == WAITING_TRANSACTION && - cmd->subcommand == G_swap_ctx.subcommand) { - ret = set_partner_key(cmd); - valid_command_received = true; - } + ret = set_partner_key(cmd); break; case CHECK_PARTNER_COMMAND: - if (G_swap_ctx.state == PROVIDER_SET && cmd->subcommand == G_swap_ctx.subcommand) { - ret = check_partner(cmd); - valid_command_received = true; - } + ret = check_partner(cmd); break; case PROCESS_TRANSACTION_RESPONSE_COMMAND: - if (G_swap_ctx.state == PROVIDER_CHECKED && cmd->subcommand == G_swap_ctx.subcommand) { - ret = process_transaction(cmd); - valid_command_received = true; - } + ret = process_transaction(cmd); break; case CHECK_TRANSACTION_SIGNATURE_COMMAND: - if (G_swap_ctx.state == TRANSACTION_RECEIVED && - cmd->subcommand == G_swap_ctx.subcommand) { - ret = check_tx_signature(cmd); - valid_command_received = true; - } + ret = check_tx_signature(cmd); break; case CHECK_PAYOUT_ADDRESS: - if (G_swap_ctx.state == SIGNATURE_CHECKED && cmd->subcommand == G_swap_ctx.subcommand) { - if (cmd->subcommand == SELL || cmd->subcommand == FUND) { - ret = check_asset_in(cmd); - } else { - ret = check_payout_address(cmd); - } - valid_command_received = true; - } - break; case CHECK_REFUND_ADDRESS: - if (G_swap_ctx.state == TO_ADDR_CHECKED && cmd->subcommand == G_swap_ctx.subcommand) { - ret = check_refund_address(cmd); - valid_command_received = true; - } + ret = check_addresses_and_amounts(cmd); break; case START_SIGNING_TRANSACTION: - if (G_swap_ctx.state == WAITING_SIGNING && cmd->subcommand == G_swap_ctx.subcommand) { - ret = start_signing_transaction(cmd); - valid_command_received = true; - } + ret = start_signing_transaction(cmd); break; default: + __builtin_unreachable(); break; } - if (!valid_command_received) { - ret = reply_error(INVALID_INSTRUCTION); - } - return ret; } diff --git a/src/commands.h b/src/commands.h index d403b9d1..3aee856e 100644 --- a/src/commands.h +++ b/src/commands.h @@ -1,7 +1,10 @@ -#ifndef _COMMANDS_H_ -#define _COMMANDS_H_ +#pragma once #include "buffer.h" + +// CLA to use when communicating with Exchange +#define CLA 0xE0 + // commands typedef enum { GET_VERSION_COMMAND = 0x02, @@ -10,25 +13,46 @@ typedef enum { CHECK_PARTNER_COMMAND = 0x05, PROCESS_TRANSACTION_RESPONSE_COMMAND = 0x06, CHECK_TRANSACTION_SIGNATURE_COMMAND = 0x07, - CHECK_PAYOUT_ADDRESS = 0x08, // CHECK_ASSET_IN for SELL + CHECK_PAYOUT_ADDRESS = 0x08, // CHECK_ASSET_IN for SELL and FUND CHECK_REFUND_ADDRESS = 0x09, START_SIGNING_TRANSACTION = 0x0A, } command_e; -// subcommands -typedef enum { SWAP = 0x00, SELL = 0x01, FUND = 0x02 } subcommand_e; +// Different rates possible for the transaction. They are given to the app as P1 of an APDU +typedef enum { + FIXED = 0x00, + FLOATING = 0x01, +} rate_e; + +// Different flows possible. They are given to the app as the P2 of an APDU +typedef enum { + SWAP = 0x00, + SELL = 0x01, + FUND = 0x02, + SWAP_NG = 0x03, + SELL_NG = 0x04, + FUND_NG = 0x05, +} subcommand_e; +// As P2 can hold more information, we use a mask to access the subcommand part of P2 +#define SUBCOMMAND_MASK 0x0F -// Different rates possible -typedef enum { FIXED = 0x00, FLOATING = 0x01 } rate_e; +// Extension values to signal that an APDU is split +// Only supported for new unified flows during PROCESS_TRANSACTION_RESPONSE_COMMAND +#define P2_NONE (0x00 << 4) +// P2_EXTEND is set to signal that this APDU buffer extends a previous one +#define P2_EXTEND (0x01 << 4) +// P2_MORE is set to signal that this APDU buffer is not complete +#define P2_MORE (0x02 << 4) +// As P2 can hold more information, we use a mask to access the extension part of P2 +#define EXTENSION_MASK 0xF0 /** * Structure with fields of APDU command. */ typedef struct { - command_e ins; /// Instruction code - rate_e rate; /// P1 - subcommand_e subcommand; /// P2 - buf_t data; /// Command data + command_e ins; // Instruction code + rate_e rate; // P1 + subcommand_e subcommand; // P2, we don't care for the extension here as this structure is for + // command handling, not apdu reception + buf_t data; // Command data } command_t; - -#endif //_COMMANDS_H_ diff --git a/src/globals.h b/src/globals.h index 0c9df802..19474953 100644 --- a/src/globals.h +++ b/src/globals.h @@ -8,11 +8,6 @@ #include "buffer.h" #include "swap_lib_calls.h" -#define P1_CONFIRM 0x01 -#define P1_NON_CONFIRM 0x00 -#define P1_FIRST 0x00 -#define P1_MORE 0x80 - #define CURVE_SIZE_BYTES 32U #define UNCOMPRESSED_KEY_LENGTH 65U #define MIN_DER_SIGNATURE_LENGTH 67U @@ -22,6 +17,8 @@ #define TICKER_MAX_SIZE_B 9 #define APPNAME_MIN_SIZE_B 3 +#define MAX_COIN_SUB_CONFIG_SIZE 64 + extern uint8_t G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; #define MIN_PARTNER_NAME_LENGHT 3 @@ -32,7 +29,7 @@ extern uint8_t G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; #pragma pack(push, 1) typedef struct partner_data_s { - unsigned char name_length; + uint8_t name_length; union { // SELL and SWAP flows display nothing // FUND flow displays "To xyz" @@ -48,12 +45,12 @@ typedef struct partner_data_s { typedef struct swap_app_context_s { union { - unsigned char sell_fund[32]; // device_transaction_id (SELL && FUND) - char swap[10]; // device_transaction_id (SWAP) + uint8_t unified[32]; // device_transaction_id (SELL && FUND && NG) + char swap[10]; // device_transaction_id (SWAP) } device_transaction_id; - unsigned char transaction_fee[16]; - unsigned char transaction_fee_length; + uint8_t transaction_fee[16]; + uint8_t transaction_fee_length; partner_data_t partner; state_e state; @@ -62,7 +59,9 @@ typedef struct swap_app_context_s { // SWAP, SELL, and FUND flows are unionized as they cannot be used in the same context union { - ledger_swap_NewTransactionResponse received_transaction; + // This is the raw received APDU + uint8_t raw_transaction[256 * 2]; + ledger_swap_NewTransactionResponse swap_transaction; struct { ledger_swap_NewSellResponse sell_transaction; // Field not received from protobuf but needed by the application called as lib @@ -75,14 +74,20 @@ typedef struct swap_app_context_s { }; }; - unsigned char sha256_digest[32]; + uint8_t sha256_digest[32]; cx_ecfp_256_public_key_t ledger_public_key; - buf_t payin_coin_config; // serialized coin configuration + uint8_t paying_sub_coin_config_size; + uint8_t paying_sub_coin_config[MAX_COIN_SUB_CONFIG_SIZE]; char payin_binary_name[BOLOS_APPNAME_MAX_SIZE_B + 1]; - char printable_get_amount[MAX_PRINTABLE_AMOUNT_SIZE]; + union { + // Amount we will gain, either in crypto (SWAP) or FIAT (SELL) + char printable_get_amount[MAX_PRINTABLE_AMOUNT_SIZE]; + // Amount name to fund + char account_name[MAX_PRINTABLE_AMOUNT_SIZE]; + }; char printable_send_amount[MAX_PRINTABLE_AMOUNT_SIZE]; char printable_fees_amount[MAX_PRINTABLE_AMOUNT_SIZE]; } swap_app_context_t; diff --git a/src/io.c b/src/io.c index 6f7b9f47..a15feb68 100644 --- a/src/io.c +++ b/src/io.c @@ -175,6 +175,11 @@ int reply_error(swap_error_e error) { } int reply_success(void) { - uint8_t output_buffer[2] = {0x90, 0x00}; - return send_apdu(output_buffer, 2); + return reply_error(SUCCESS); +} + +int instant_reply_success(void) { + G_io_apdu_buffer[0] = 0x90; + G_io_apdu_buffer[1] = 0x00; + return io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); } diff --git a/src/io.h b/src/io.h index 161168eb..1c135ed3 100644 --- a/src/io.h +++ b/src/io.h @@ -30,3 +30,5 @@ int send_apdu(uint8_t *buffer, size_t buffer_length); int reply_error(swap_error_e error); int reply_success(void); + +int instant_reply_success(void); diff --git a/src/main.c b/src/main.c index 832b63c0..9b3be195 100644 --- a/src/main.c +++ b/src/main.c @@ -26,42 +26,38 @@ #include "command_dispatcher.h" #include "apdu_offsets.h" #include "swap_errors.h" +#include "apdu_parser.h" #include "usbd_core.h" -#define CLA 0xE0 ux_state_t G_ux; bolos_ux_params_t G_ux_params; -unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; +uint8_t G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; + swap_app_context_t G_swap_ctx; void app_main(void) { int input_length = 0; + command_t cmd; init_io(); for (;;) { input_length = recv_apdu(); PRINTF("New APDU received:\n%.*H\n", input_length, G_io_apdu_buffer); - if (input_length == -1) // there were an error, lets start from the beginning + // there was a fatal error during APDU reception, restart from the beginning + // Don't bother trying to send a status code, IOs are probably out + if (input_length == -1) { return; - if (input_length < OFFSET_CDATA || G_io_apdu_buffer[OFFSET_CLA] != CLA) { - PRINTF("Error: bad APDU\n"); - reply_error(INVALID_INSTRUCTION); - continue; } - const command_t cmd = { - .ins = (command_e) G_io_apdu_buffer[OFFSET_INS], - .rate = G_io_apdu_buffer[OFFSET_P1], - .subcommand = G_io_apdu_buffer[OFFSET_P2], - .data = - { - .bytes = G_io_apdu_buffer + OFFSET_CDATA, - .size = input_length - OFFSET_CDATA, - }, - }; + uint16_t ret = apdu_parser(G_io_apdu_buffer, input_length, &cmd); + if (ret != 0) { + PRINTF("Sending early reply 0x%4x\n", ret); + reply_error(ret); + continue; + } if (dispatch_command(&cmd) < 0) { // some non recoverable error happened diff --git a/src/parse_check_address_message.c b/src/parse_check_address_message.c index ed4387dd..67979161 100644 --- a/src/parse_check_address_message.c +++ b/src/parse_check_address_message.c @@ -9,7 +9,7 @@ static bool parse_der_signature(uint8_t *in, uint16_t in_size, buf_t *der, uint1 ++*offset; // Read compound object - if (!parse_to_sized_buffer(in, in_size, der, offset)) { + if (!parse_to_sized_buffer(in, in_size, 1, der, offset)) { return false; } // Adapt buffer to encompass the full DER signature @@ -34,7 +34,7 @@ int parse_check_address_message(const command_t *cmd, uint16_t read = 0; // Read currency configuration - if (!parse_to_sized_buffer(cmd->data.bytes, cmd->data.size, config, &read)) { + if (!parse_to_sized_buffer(cmd->data.bytes, cmd->data.size, 1, config, &read)) { PRINTF("Cannot read the config\n"); return 0; } @@ -53,7 +53,7 @@ int parse_check_address_message(const command_t *cmd, } // Read address parameters - if (!parse_to_sized_buffer(cmd->data.bytes, cmd->data.size, address_parameters, &read)) { + if (!parse_to_sized_buffer(cmd->data.bytes, cmd->data.size, 1, address_parameters, &read)) { PRINTF("Cannot read the address_parameters\n"); return 0; } diff --git a/src/parse_coin_config.c b/src/parse_coin_config.c index 596419d6..2d3c04ec 100644 --- a/src/parse_coin_config.c +++ b/src/parse_coin_config.c @@ -26,18 +26,16 @@ const app_name_alias_t appnames_aliases[] = { * With: * - T the ticker symbol, Lt its size * - A the application name, La its size - * - C the configuration, Lc its size + * - C the sub configuration, Lc its size */ -int parse_coin_config(const buf_t *const orig_buffer, +int parse_coin_config(buf_t input, buf_t *ticker, buf_t *application_name, - buf_t *configuration) { + buf_t *sub_configuration) { uint16_t total_read = 0; - // This function can be called with orig_buffer == configuration, so making a copy - const buf_t input = *orig_buffer; // Read ticker - if (!parse_to_sized_buffer(input.bytes, input.size, ticker, &total_read)) { + if (!parse_to_sized_buffer(input.bytes, input.size, 1, ticker, &total_read)) { PRINTF("Cannot read the ticker\n"); return 0; } @@ -46,7 +44,7 @@ int parse_coin_config(const buf_t *const orig_buffer, } // Read application_name - if (!parse_to_sized_buffer(input.bytes, input.size, application_name, &total_read)) { + if (!parse_to_sized_buffer(input.bytes, input.size, 1, application_name, &total_read)) { PRINTF("Cannot read the application_name\n"); return 0; } @@ -54,9 +52,13 @@ int parse_coin_config(const buf_t *const orig_buffer, return 0; } - // Read configuration - if (!parse_to_sized_buffer(input.bytes, input.size, configuration, &total_read)) { - PRINTF("Cannot read the configuration\n"); + // Read sub configuration + if (!parse_to_sized_buffer(input.bytes, input.size, 1, sub_configuration, &total_read)) { + PRINTF("Cannot read the sub_configuration\n"); + return 0; + } + if (sub_configuration->size > MAX_COIN_SUB_CONFIG_SIZE) { + PRINTF("Sub coin sub_configuration size %d is too big\n", sub_configuration->size); return 0; } @@ -72,14 +74,10 @@ int parse_coin_config(const buf_t *const orig_buffer, strncmp((const char *) application_name->bytes, (char *) (PIC(appnames_aliases[i].foreign_name)), application_name->size) == 0) { - PRINTF("Aliased appname, from '%.*s'\n", - application_name->size, - application_name->bytes); - application_name->bytes = (uint8_t *) appnames_aliases[i].app_name; - application_name->size = strlen((char *) PIC(appnames_aliases[i].app_name)); - PRINTF("Aliased appname, to '%.*s'\n", - application_name->size, - PIC(application_name->bytes)); + PRINTF("Aliased from '%.*s'\n", application_name->size, application_name->bytes); + application_name->bytes = (uint8_t *) PIC(appnames_aliases[i].app_name); + application_name->size = strlen((char *) application_name->bytes); + PRINTF("to '%.*s'\n", application_name->size, application_name->bytes); break; } } diff --git a/src/parse_coin_config.h b/src/parse_coin_config.h index 5326c421..d39c9402 100644 --- a/src/parse_coin_config.h +++ b/src/parse_coin_config.h @@ -1,11 +1,5 @@ -#ifndef _PARSE_COIN_CONFIG_H_ -#define _PARSE_COIN_CONFIG_H_ +#pragma once #include "buffer.h" -int parse_coin_config(const buf_t *const config, - buf_t *ticker, - buf_t *application_name, - buf_t *pure_config); - -#endif // _PARSE_COIN_CONFIG_H_ +int parse_coin_config(buf_t config, buf_t *ticker, buf_t *application_name, buf_t *sub_config); diff --git a/src/process_transaction.c b/src/process_transaction.c index 5f75637e..e91d3298 100644 --- a/src/process_transaction.c +++ b/src/process_transaction.c @@ -38,218 +38,266 @@ static void trim_pb_bytes_array(pb_bytes_array_16_t *transaction) { memmove(transaction->bytes, transaction->bytes + i, transaction->size); } -static void normalize_currencies(void) { - to_uppercase(G_swap_ctx.received_transaction.currency_from, - sizeof(G_swap_ctx.received_transaction.currency_from)); - to_uppercase(G_swap_ctx.received_transaction.currency_to, - sizeof(G_swap_ctx.received_transaction.currency_to)); - set_ledger_currency_name(G_swap_ctx.received_transaction.currency_from, - sizeof(G_swap_ctx.received_transaction.currency_from) / - sizeof(G_swap_ctx.received_transaction.currency_from[0])); - set_ledger_currency_name(G_swap_ctx.received_transaction.currency_to, - sizeof(G_swap_ctx.received_transaction.currency_to) / - sizeof(G_swap_ctx.received_transaction.currency_to[0])); - // triming leading 0s - trim_pb_bytes_array( - (pb_bytes_array_16_t *) &(G_swap_ctx.received_transaction.amount_to_provider)); - trim_pb_bytes_array( - (pb_bytes_array_16_t *) &(G_swap_ctx.received_transaction.amount_to_wallet)); - - // strip bcash CashAddr header, and other bicmd->subcommand1 like headers - for (size_t i = 0; i < sizeof(G_swap_ctx.received_transaction.payin_address); i++) { - if (G_swap_ctx.received_transaction.payin_address[i] == ':') { - memmove(G_swap_ctx.received_transaction.payin_address, - G_swap_ctx.received_transaction.payin_address + i + 1, - sizeof(G_swap_ctx.received_transaction.payin_address) - i - 1); - break; - } +static bool parse_transaction(uint8_t *in, + size_t in_size, + subcommand_e subcommand, + buf_t *payload, + buf_t *fees) { + // On legacy flows the length field is 1 byte + uint8_t payload_length_field_size = 2; + if (subcommand == SWAP || subcommand == SELL || subcommand == FUND) { + payload_length_field_size = 1; } -} -int process_transaction(const command_t *cmd) { - if (cmd->data.size < 1) { - PRINTF("Error: Can't parse process_transaction message, length should be more than 1\n"); + uint16_t offset = 0; + if (!parse_to_sized_buffer(in, in_size, payload_length_field_size, payload, &offset)) { + PRINTF("Failed to parse payload\n"); + return false; + } - return reply_error(DESERIALIZATION_FAILED); + if (!parse_to_sized_buffer(in, in_size, 1, fees, &offset)) { + PRINTF("Failed to parse fees\n"); + return false; } - size_t payload_length = cmd->data.bytes[0]; - if (cmd->data.size < 1 + payload_length) { - PRINTF("Error: Can't parse process_transaction message, invalid payload length\n"); + if (offset != in_size) { + PRINTF("Leftover data to read, received %d bytes, read %d bytes\n", in_size, offset); + return false; + } - return reply_error(DESERIALIZATION_FAILED); + if (fees->size > sizeof(G_swap_ctx.transaction_fee)) { + PRINTF("Error: Transaction fee buffer is too long, received %d, max is %d\n", + fees->size, + sizeof(G_swap_ctx.transaction_fee)); + return false; } - pb_istream_t stream; + return true; +} + +static bool calculate_sha256_digest(buf_t payload, subcommand_e subcommand) { cx_sha256_t sha256; cx_sha256_init(&sha256); - PRINTF("len(payload): %d\n", payload_length); - PRINTF("payload (%d): %.*H\n", payload_length, payload_length, cmd->data.bytes + 1); - - if (cmd->subcommand == SWAP) { - stream = pb_istream_from_buffer(cmd->data.bytes + 1, payload_length); - - if (!pb_decode(&stream, - ledger_swap_NewTransactionResponse_fields, - &G_swap_ctx.received_transaction)) { - PRINTF("Error: Can't parse SWAP transaction protobuf\n%.*H\n", - payload_length, - cmd->data.bytes + 1); - - return reply_error(DESERIALIZATION_FAILED); - } - - if (memcmp(G_swap_ctx.device_transaction_id.swap, - G_swap_ctx.received_transaction.device_transaction_id, - sizeof(G_swap_ctx.device_transaction_id.swap)) != 0) { - PRINTF( - "Error: Device transaction IDs (SWAP) doesn't match. Expected: {%.*H}, got " - "{%.*H}\n", - sizeof(G_swap_ctx.device_transaction_id.swap), - G_swap_ctx.device_transaction_id.swap, - sizeof(G_swap_ctx.device_transaction_id.swap), - G_swap_ctx.received_transaction.device_transaction_id); - - return reply_error(WRONG_TRANSACTION_ID); + if (subcommand != SWAP) { + unsigned char dot = '.'; + if (cx_hash_no_throw(&sha256.header, 0, &dot, 1, NULL, 0) != CX_OK) { + PRINTF("Error: cx_hash_no_throw\n"); + return false; } - - normalize_currencies(); } - if (cmd->subcommand == SELL || cmd->subcommand == FUND) { - // arbitrary maximum payload size - unsigned char payload[256]; - - int n = base64_decode(payload, - sizeof(payload), - (const unsigned char *) cmd->data.bytes + 1, - payload_length); + if (cx_hash_no_throw(&sha256.header, + CX_LAST, + payload.bytes, + payload.size, + G_swap_ctx.sha256_digest, + sizeof(G_swap_ctx.sha256_digest)) != CX_OK) { + PRINTF("Error: cx_hash_no_throw\n"); + return false; + } - PRINTF("len(base64_decode(payload)) = %d\n", n); + PRINTF("sha256_digest: %.*H\n", 32, G_swap_ctx.sha256_digest); - if (n < 0) { - PRINTF("Error: Can't decode SELL/FUND transaction base64\n"); + return true; +} - return reply_error(DESERIALIZATION_FAILED); - } +static bool deserialize_protobuf_payload(buf_t payload, subcommand_e subcommand) { + pb_istream_t stream; + const pb_field_t *fields; + void *dest_struct; - PRINTF("decode_base64(payload): %.*H\n", n, payload); + // Temporary buffer if the received payload is encoded. Size is arbitrary + unsigned char decoded[256]; - unsigned char dot = '.'; + buf_t to_deserialize; - if (cx_hash_no_throw(&sha256.header, 0, &dot, 1, NULL, 0) != CX_OK) { - PRINTF("Error: cx_hash_no_throw\n"); - return reply_error(INTERNAL_ERROR); + if (subcommand != SWAP) { + int n = base64_decode(decoded, + sizeof(decoded), + (const unsigned char *) payload.bytes, + payload.size); + if (n < 0) { + PRINTF("Error: Can't decode SELL/FUND/NG transaction base64\n"); + return false; } + to_deserialize.bytes = decoded; + to_deserialize.size = n; + } else { + to_deserialize = payload; + } - stream = pb_istream_from_buffer(payload, n); + stream = pb_istream_from_buffer(to_deserialize.bytes, to_deserialize.size); + + if (subcommand == SWAP || subcommand == SWAP_NG) { + fields = ledger_swap_NewTransactionResponse_fields; + dest_struct = &G_swap_ctx.swap_transaction; + } else if (subcommand == SELL || subcommand == SELL_NG) { + fields = ledger_swap_NewSellResponse_fields; + dest_struct = &G_swap_ctx.sell_transaction; + } else { + fields = ledger_swap_NewFundResponse_fields; + dest_struct = &G_swap_ctx.fund_transaction; + } - const pb_field_t *pb_fields = - (cmd->subcommand == SELL ? ledger_swap_NewSellResponse_fields - : ledger_swap_NewFundResponse_fields); + if (!pb_decode(&stream, fields, dest_struct)) { + PRINTF("Error: Can't deserialize protobuf payload\n"); + return false; + } - if (cmd->subcommand == SELL) { - if (!pb_decode(&stream, pb_fields, &G_swap_ctx.sell_transaction)) { - PRINTF("Error: Can't parse SELL transaction protobuf\n"); - return reply_error(DESERIALIZATION_FAILED); - } - // Field not received from protobuf - G_swap_ctx.sell_transaction_extra_id[0] = '\0'; - } else { - if (!pb_decode(&stream, pb_fields, &G_swap_ctx.fund_transaction)) { - PRINTF("Error: Can't parse FUND transaction protobuf\n"); - return reply_error(DESERIALIZATION_FAILED); - } - // Field not received from protobuf - G_swap_ctx.fund_transaction_extra_id[0] = '\0'; - } + // Field not received from protobuf + if (subcommand == SELL || subcommand == SELL_NG) { + G_swap_ctx.sell_transaction_extra_id[0] = '\0'; + } else if (subcommand == FUND || subcommand == FUND_NG) { + G_swap_ctx.fund_transaction_extra_id[0] = '\0'; + } - // trim leading 0s - pb_bytes_array_16_t *in_amount; - if (G_swap_ctx.subcommand == SELL) { - in_amount = (pb_bytes_array_16_t *) &G_swap_ctx.sell_transaction.in_amount; - } else { - in_amount = (pb_bytes_array_16_t *) &G_swap_ctx.fund_transaction.in_amount; - } - trim_pb_bytes_array(in_amount); + return true; +} +static bool check_transaction_id(subcommand_e subcommand) { + if (subcommand == SWAP) { + if (memcmp(G_swap_ctx.device_transaction_id.swap, + G_swap_ctx.swap_transaction.device_transaction_id, + sizeof(G_swap_ctx.device_transaction_id.swap)) != 0) { + PRINTF("Error: Device transaction IDs don't match, expected %.*H, received %.*H\n", + sizeof(G_swap_ctx.device_transaction_id.swap), + G_swap_ctx.device_transaction_id.swap, + sizeof(G_swap_ctx.device_transaction_id.swap), + G_swap_ctx.swap_transaction.device_transaction_id); + return false; + } + } else { pb_bytes_array_32_t *tx_id; - if (G_swap_ctx.subcommand == SELL) { + if (subcommand == SELL || subcommand == SELL_NG) { tx_id = (pb_bytes_array_32_t *) &G_swap_ctx.sell_transaction.device_transaction_id; - PRINTF("G_swap_ctx.sell_transaction->device_transaction_id @%p: %.*H\n", - G_swap_ctx.sell_transaction.device_transaction_id.bytes, - G_swap_ctx.sell_transaction.device_transaction_id.size, - G_swap_ctx.sell_transaction.device_transaction_id.bytes); - } else { + } else if (subcommand == FUND || subcommand == FUND_NG) { tx_id = (pb_bytes_array_32_t *) &G_swap_ctx.fund_transaction.device_transaction_id; - PRINTF("G_swap_ctx.fund_transaction->device_transaction_id @%p: %.*H\n", - G_swap_ctx.fund_transaction.device_transaction_id.bytes, - G_swap_ctx.fund_transaction.device_transaction_id.size, - G_swap_ctx.fund_transaction.device_transaction_id.bytes); + } else { + tx_id = (pb_bytes_array_32_t *) &G_swap_ctx.swap_transaction.device_transaction_id_ng; } - if (tx_id->size != sizeof(G_swap_ctx.device_transaction_id.sell_fund)) { - PRINTF("Error: Device transaction ID (SELL/FUND) size doesn't match\n"); - PRINTF("tx_id->size = %d\n", tx_id->size); - PRINTF("sizeof(G_swap_ctx.device_transaction_id.sell_fund) = %d\n", - sizeof(G_swap_ctx.device_transaction_id.sell_fund)); + if (tx_id->size != sizeof(G_swap_ctx.device_transaction_id.unified)) { + PRINTF("Error: Device transaction ID size doesn't match, exp %d bytes, recv %d bytes\n", + sizeof(G_swap_ctx.device_transaction_id.unified), + tx_id->size); + return false; } - if (memcmp(G_swap_ctx.device_transaction_id.sell_fund, tx_id->bytes, tx_id->size) != 0) { - PRINTF("Error: Device transaction IDs (SELL/FUND) don't match\n"); - PRINTF("G_swap_ctx.device_transaction_id @%p: %.*H\n", - G_swap_ctx.device_transaction_id.sell_fund, - sizeof(G_swap_ctx.device_transaction_id.sell_fund), - G_swap_ctx.device_transaction_id.sell_fund); - - return reply_error(WRONG_TRANSACTION_ID); + if (memcmp(G_swap_ctx.device_transaction_id.unified, tx_id->bytes, tx_id->size) != 0) { + PRINTF("Error: Device transaction IDs don't match, expected %.*H, received %.*H\n", + tx_id->size, + tx_id->bytes, + sizeof(G_swap_ctx.device_transaction_id.unified), + G_swap_ctx.device_transaction_id.unified); + return false; } } - if (cx_hash_no_throw(&sha256.header, - CX_LAST, - cmd->data.bytes + 1, - payload_length, - G_swap_ctx.sha256_digest, - sizeof(G_swap_ctx.sha256_digest)) != CX_OK) { - PRINTF("Error: cx_hash_no_throw\n"); - return reply_error(INTERNAL_ERROR); + return true; +} + +static void normalize_currencies(subcommand) { + if (subcommand == SWAP || subcommand == SWAP_NG) { + to_uppercase(G_swap_ctx.swap_transaction.currency_from, + sizeof(G_swap_ctx.swap_transaction.currency_from)); + set_ledger_currency_name(G_swap_ctx.swap_transaction.currency_from, + sizeof(G_swap_ctx.swap_transaction.currency_from) / + sizeof(G_swap_ctx.swap_transaction.currency_from[0])); + + to_uppercase(G_swap_ctx.swap_transaction.currency_to, + sizeof(G_swap_ctx.swap_transaction.currency_to)); + set_ledger_currency_name(G_swap_ctx.swap_transaction.currency_to, + sizeof(G_swap_ctx.swap_transaction.currency_to) / + sizeof(G_swap_ctx.swap_transaction.currency_to[0])); + + // strip bcash CashAddr header, and other bicmd->subcommand1 like headers + for (size_t i = 0; i < sizeof(G_swap_ctx.swap_transaction.payin_address); i++) { + if (G_swap_ctx.swap_transaction.payin_address[i] == ':') { + memmove(G_swap_ctx.swap_transaction.payin_address, + G_swap_ctx.swap_transaction.payin_address + i + 1, + sizeof(G_swap_ctx.swap_transaction.payin_address) - i - 1); + break; + } + } + } else if (subcommand == SELL || subcommand == SELL_NG) { + to_uppercase(G_swap_ctx.sell_transaction.in_currency, + sizeof(G_swap_ctx.sell_transaction.in_currency)); + set_ledger_currency_name(G_swap_ctx.sell_transaction.in_currency, + sizeof(G_swap_ctx.sell_transaction.in_currency) / + sizeof(G_swap_ctx.sell_transaction.in_currency[0])); + } else if (subcommand == FUND || subcommand == FUND_NG) { + to_uppercase(G_swap_ctx.fund_transaction.in_currency, + sizeof(G_swap_ctx.fund_transaction.in_currency)); + set_ledger_currency_name(G_swap_ctx.fund_transaction.in_currency, + sizeof(G_swap_ctx.fund_transaction.in_currency) / + sizeof(G_swap_ctx.fund_transaction.in_currency[0])); } +} - PRINTF("sha256_digest: %.*H\n", 32, G_swap_ctx.sha256_digest); +// triming leading 0s +static void trim_amounts(subcommand) { + if (subcommand == SWAP || subcommand == SWAP_NG) { + trim_pb_bytes_array( + (pb_bytes_array_16_t *) &G_swap_ctx.swap_transaction.amount_to_provider); + trim_pb_bytes_array((pb_bytes_array_16_t *) &G_swap_ctx.swap_transaction.amount_to_wallet); + } else if (subcommand == SELL || subcommand == SELL_NG) { + trim_pb_bytes_array((pb_bytes_array_16_t *) &G_swap_ctx.sell_transaction.in_amount); + } else if (subcommand == FUND || subcommand == FUND_NG) { + trim_pb_bytes_array((pb_bytes_array_16_t *) &G_swap_ctx.fund_transaction.in_amount); + } +} - if (cmd->data.size < 1 + payload_length + 1) { - PRINTF("Error: Can't parse process_transaction message, should include fee\n"); +// Extract the fees from the apdus and store them in the global context +static void save_fees(buf_t fees) { + G_swap_ctx.transaction_fee_length = fees.size; + memset(G_swap_ctx.transaction_fee, 0, sizeof(G_swap_ctx.transaction_fee)); + memcpy(G_swap_ctx.transaction_fee, fees.bytes, G_swap_ctx.transaction_fee_length); + PRINTF("Transaction fees BE = %.*H\n", + G_swap_ctx.transaction_fee_length, + G_swap_ctx.transaction_fee); +} - return reply_error(DESERIALIZATION_FAILED); +int process_transaction(const command_t *cmd) { + uint8_t undecoded_transaction[sizeof(G_swap_ctx.raw_transaction)]; + uint8_t *data; + // For memory optimization, the undecoded protobuf apdu may have been stored in an union + // with the decoded protobuf transaction. + if (cmd->data.bytes == G_swap_ctx.raw_transaction) { + // Copy locally the apdu to avoid problems during protobuf decode and fees extraction + PRINTF("Copying locally, the APDU has been received split\n"); + memcpy(undecoded_transaction, G_swap_ctx.raw_transaction, cmd->data.size); + data = undecoded_transaction; + } else { + data = cmd->data.bytes; } - G_swap_ctx.transaction_fee_length = cmd->data.bytes[1 + payload_length]; + buf_t fees; + buf_t payload; + if (!parse_transaction(data, cmd->data.size, cmd->subcommand, &payload, &fees)) { + return reply_error(INCORRECT_COMMAND_DATA); + } - if (G_swap_ctx.transaction_fee_length > sizeof(G_swap_ctx.transaction_fee)) { - PRINTF("Error: Transaction fee is to long\n"); + if (!calculate_sha256_digest(payload, cmd->subcommand)) { + return reply_error(INTERNAL_ERROR); + } + if (!deserialize_protobuf_payload(payload, cmd->subcommand)) { return reply_error(DESERIALIZATION_FAILED); } - if (cmd->data.size < 1 + payload_length + 1 + G_swap_ctx.transaction_fee_length) { - PRINTF("Error: Input buffer is too small"); - - return reply_error(DESERIALIZATION_FAILED); + if (!check_transaction_id(cmd->subcommand)) { + return reply_error(WRONG_TRANSACTION_ID); } - memset(G_swap_ctx.transaction_fee, 0, sizeof(G_swap_ctx.transaction_fee)); - memcpy(G_swap_ctx.transaction_fee, - cmd->data.bytes + 1 + payload_length + 1, - G_swap_ctx.transaction_fee_length); - PRINTF("Transaction fees BE = %.*H\n", - G_swap_ctx.transaction_fee_length, - G_swap_ctx.transaction_fee); + normalize_currencies(cmd->subcommand); + + trim_amounts(cmd->subcommand); + + save_fees(fees); if (reply_success() < 0) { PRINTF("Error: failed to send response\n"); - return -1; } diff --git a/src/proto/protocol.options b/src/proto/protocol.options index 08e05926..358a0323 100644 --- a/src/proto/protocol.options +++ b/src/proto/protocol.options @@ -9,6 +9,8 @@ ledger_swap.NewTransactionResponse.currency_to max_size:10; ledger_swap.NewTransactionResponse.amount_to_provider max_size:16; ledger_swap.NewTransactionResponse.amount_to_wallet max_size:16; ledger_swap.NewTransactionResponse.device_transaction_id max_size:11; +ledger_swap.NewTransactionResponse.device_transaction_id_ng max_size:32; + ledger_swap.NewSellResponse.trader_email max_size:50; ledger_swap.NewSellResponse.in_currency max_size:10; ledger_swap.NewSellResponse.in_amount max_size:16; diff --git a/src/proto/protocol.pb.c b/src/proto/protocol.pb.c index 8423ce5d..8c304e63 100644 --- a/src/proto/protocol.pb.c +++ b/src/proto/protocol.pb.c @@ -1,5 +1,5 @@ /* Automatically generated nanopb constant definitions */ -/* Generated by nanopb-0.3.9 at Mon Jan 16 15:05:55 2023. */ +/* Generated by nanopb-0.3.9 at Mon Jul 31 14:13:55 2023. */ #include "protocol.pb.h" @@ -10,7 +10,7 @@ -const pb_field_t ledger_swap_NewTransactionResponse_fields[12] = { +const pb_field_t ledger_swap_NewTransactionResponse_fields[13] = { PB_FIELD( 1, STRING , SINGULAR, STATIC , FIRST, ledger_swap_NewTransactionResponse, payin_address, payin_address, 0), PB_FIELD( 2, STRING , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, payin_extra_id, payin_address, 0), PB_FIELD( 3, STRING , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, refund_address, payin_extra_id, 0), @@ -22,6 +22,7 @@ const pb_field_t ledger_swap_NewTransactionResponse_fields[12] = { PB_FIELD( 9, BYTES , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, amount_to_provider, currency_to, 0), PB_FIELD( 10, BYTES , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, amount_to_wallet, amount_to_provider, 0), PB_FIELD( 11, STRING , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, device_transaction_id, amount_to_wallet, 0), + PB_FIELD( 12, BYTES , SINGULAR, STATIC , OTHER, ledger_swap_NewTransactionResponse, device_transaction_id_ng, device_transaction_id, 0), PB_LAST_FIELD }; diff --git a/src/proto/protocol.pb.h b/src/proto/protocol.pb.h index 2482f2ac..958bcf84 100644 --- a/src/proto/protocol.pb.h +++ b/src/proto/protocol.pb.h @@ -1,5 +1,5 @@ /* Automatically generated nanopb header */ -/* Generated by nanopb-0.3.9 at Mon Jan 16 15:05:55 2023. */ +/* Generated by nanopb-0.3.9 at Mon Jul 31 14:13:55 2023. */ #ifndef PB_LEDGER_SWAP_PROTOCOL_PB_H_INCLUDED #define PB_LEDGER_SWAP_PROTOCOL_PB_H_INCLUDED @@ -29,6 +29,7 @@ typedef struct _ledger_swap_NewFundResponse { typedef PB_BYTES_ARRAY_T(16) ledger_swap_NewTransactionResponse_amount_to_provider_t; typedef PB_BYTES_ARRAY_T(16) ledger_swap_NewTransactionResponse_amount_to_wallet_t; +typedef PB_BYTES_ARRAY_T(32) ledger_swap_NewTransactionResponse_device_transaction_id_ng_t; typedef struct _ledger_swap_NewTransactionResponse { char payin_address[63]; char payin_extra_id[20]; @@ -41,6 +42,7 @@ typedef struct _ledger_swap_NewTransactionResponse { ledger_swap_NewTransactionResponse_amount_to_provider_t amount_to_provider; ledger_swap_NewTransactionResponse_amount_to_wallet_t amount_to_wallet; char device_transaction_id[11]; + ledger_swap_NewTransactionResponse_device_transaction_id_ng_t device_transaction_id_ng; /* @@protoc_insertion_point(struct:ledger_swap_NewTransactionResponse) */ } ledger_swap_NewTransactionResponse; @@ -67,11 +69,11 @@ typedef struct _ledger_swap_NewSellResponse { /* Default values for struct fields */ /* Initializer values for message structs */ -#define ledger_swap_NewTransactionResponse_init_default {"", "", "", "", "", "", "", "", {0, {0}}, {0, {0}}, ""} +#define ledger_swap_NewTransactionResponse_init_default {"", "", "", "", "", "", "", "", {0, {0}}, {0, {0}}, "", {0, {0}}} #define ledger_swap_UDecimal_init_default {{0, {0}}, 0} #define ledger_swap_NewSellResponse_init_default {"", "", {0, {0}}, "", "", ledger_swap_UDecimal_init_default, {0, {0}}} #define ledger_swap_NewFundResponse_init_default {"", "", "", {0, {0}}, "", {0, {0}}} -#define ledger_swap_NewTransactionResponse_init_zero {"", "", "", "", "", "", "", "", {0, {0}}, {0, {0}}, ""} +#define ledger_swap_NewTransactionResponse_init_zero {"", "", "", "", "", "", "", "", {0, {0}}, {0, {0}}, "", {0, {0}}} #define ledger_swap_UDecimal_init_zero {{0, {0}}, 0} #define ledger_swap_NewSellResponse_init_zero {"", "", {0, {0}}, "", "", ledger_swap_UDecimal_init_zero, {0, {0}}} #define ledger_swap_NewFundResponse_init_zero {"", "", "", {0, {0}}, "", {0, {0}}} @@ -94,6 +96,7 @@ typedef struct _ledger_swap_NewSellResponse { #define ledger_swap_NewTransactionResponse_amount_to_provider_tag 9 #define ledger_swap_NewTransactionResponse_amount_to_wallet_tag 10 #define ledger_swap_NewTransactionResponse_device_transaction_id_tag 11 +#define ledger_swap_NewTransactionResponse_device_transaction_id_ng_tag 12 #define ledger_swap_UDecimal_coefficient_tag 1 #define ledger_swap_UDecimal_exponent_tag 2 #define ledger_swap_NewSellResponse_trader_email_tag 1 @@ -105,13 +108,13 @@ typedef struct _ledger_swap_NewSellResponse { #define ledger_swap_NewSellResponse_device_transaction_id_tag 7 /* Struct field encoding specification for nanopb */ -extern const pb_field_t ledger_swap_NewTransactionResponse_fields[12]; +extern const pb_field_t ledger_swap_NewTransactionResponse_fields[13]; extern const pb_field_t ledger_swap_UDecimal_fields[3]; extern const pb_field_t ledger_swap_NewSellResponse_fields[8]; extern const pb_field_t ledger_swap_NewFundResponse_fields[7]; /* Maximum encoded size of messages (where known) */ -#define ledger_swap_NewTransactionResponse_size 334 +#define ledger_swap_NewTransactionResponse_size 368 #define ledger_swap_UDecimal_size 24 #define ledger_swap_NewSellResponse_size 219 #define ledger_swap_NewFundResponse_size 233 diff --git a/src/proto/protocol.proto b/src/proto/protocol.proto index 9b448dc4..8e653ec5 100644 --- a/src/proto/protocol.proto +++ b/src/proto/protocol.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package ledger_swap; - + message NewTransactionResponse { string payin_address = 1; string payin_extra_id = 2; @@ -13,6 +13,7 @@ message NewTransactionResponse { bytes amount_to_provider = 9; bytes amount_to_wallet = 10; string device_transaction_id = 11; + bytes device_transaction_id_ng = 12; } // (coefficient) * 10^(- exponent) diff --git a/src/set_partner_key.c b/src/set_partner_key.c index 4381f697..4a28b15c 100644 --- a/src/set_partner_key.c +++ b/src/set_partner_key.c @@ -5,69 +5,81 @@ #include "set_partner_key.h" #include "swap_errors.h" #include "globals.h" +#include "buffer.h" #include "io.h" +#include "cx.h" -int set_partner_key(const command_t *cmd) { +static uint16_t parse_set_partner_key_command(const command_t *cmd, + buf_t *partner_name, + uint8_t **raw_pubkey) { // data is serialized as // 1 byte - partner name length L // L bytes - partner name // 65 bytes - uncompressed partner public key - if (cmd->data.size < 1) { - PRINTF("Error: Input buffer is too small\n"); - - return reply_error(INCORRECT_COMMAND_DATA); + uint16_t offset = 0; + if (!parse_to_sized_buffer(cmd->data.bytes, cmd->data.size, 1, partner_name, &offset)) { + PRINTF("Failed to read partner name from partner credentials\n"); + return INCORRECT_COMMAND_DATA; } - G_swap_ctx.partner.name_length = cmd->data.bytes[0]; - - if ((G_swap_ctx.partner.name_length < MIN_PARTNER_NAME_LENGHT) || - (G_swap_ctx.partner.name_length > MAX_PARTNER_NAME_LENGHT)) { + if ((partner_name->size < MIN_PARTNER_NAME_LENGHT) || + (partner_name->size > MAX_PARTNER_NAME_LENGHT)) { PRINTF("Error: Partner name length should be in [%u, %u]\n", MIN_PARTNER_NAME_LENGHT, MAX_PARTNER_NAME_LENGHT); - - return reply_error(INCORRECT_COMMAND_DATA); + return INCORRECT_COMMAND_DATA; } - if (1 + G_swap_ctx.partner.name_length + UNCOMPRESSED_KEY_LENGTH != cmd->data.size) { + if (offset + UNCOMPRESSED_KEY_LENGTH != cmd->data.size) { PRINTF("Error: Input buffer length doesn't match correct SET_PARTNER_KEY message\n"); + return INCORRECT_COMMAND_DATA; + } + *raw_pubkey = cmd->data.bytes + offset; + + return 0; +} - return reply_error(INCORRECT_COMMAND_DATA); +int set_partner_key(const command_t *cmd) { + // Use dedicated function to extract the partner name and public key + buf_t partner; + uint8_t *raw_pubkey; + uint16_t err = parse_set_partner_key_command(cmd, &partner, &raw_pubkey); + if (err != 0) { + return reply_error(err); } // The incoming partner name is NOT NULL terminated, so we use memcpy and // manually NULL terminate the buffer memset(G_swap_ctx.partner.name, 0, sizeof(G_swap_ctx.partner.name)); - memcpy(G_swap_ctx.partner.name, cmd->data.bytes + 1, G_swap_ctx.partner.name_length); + memcpy(G_swap_ctx.partner.name, partner.bytes, partner.size); + G_swap_ctx.partner.name_length = partner.size; + // Create the verifying key from the raw public key. Curve used depends of the flow + cx_curve_t curve; if (cmd->subcommand == SWAP) { - if (cx_ecfp_init_public_key_no_throw(CX_CURVE_SECP256K1, - cmd->data.bytes + 1 + G_swap_ctx.partner.name_length, - UNCOMPRESSED_KEY_LENGTH, - &(G_swap_ctx.partner.public_key)) != CX_OK) { - PRINTF("Error: cx_ecfp_init_public_key_no_throw\n"); - return reply_error(INTERNAL_ERROR); - } + curve = CX_CURVE_SECP256K1; + } else { + curve = CX_CURVE_256R1; } - - if (cmd->subcommand == SELL || cmd->subcommand == FUND) { - if (cx_ecfp_init_public_key_no_throw(CX_CURVE_256R1, - cmd->data.bytes + 1 + G_swap_ctx.partner.name_length, - UNCOMPRESSED_KEY_LENGTH, - &(G_swap_ctx.partner.public_key)) != CX_OK) { - PRINTF("Error: cx_ecfp_init_public_key_no_throw\n"); - return reply_error(INTERNAL_ERROR); - } + if (cx_ecfp_init_public_key_no_throw(curve, + raw_pubkey, + UNCOMPRESSED_KEY_LENGTH, + &(G_swap_ctx.partner.public_key)) != CX_OK) { + PRINTF("Error: cx_ecfp_init_public_key_no_throw\n"); + return reply_error(INTERNAL_ERROR); } - cx_hash_sha256(cmd->data.bytes, - cmd->data.size, - G_swap_ctx.sha256_digest, - sizeof(G_swap_ctx.sha256_digest)); + // Save the hash of the entire credentials to check that the Ledger key signed it later + if (cx_hash_sha256(cmd->data.bytes, + cmd->data.size, + G_swap_ctx.sha256_digest, + sizeof(G_swap_ctx.sha256_digest)) == 0) { + PRINTF("cx_hash_sha256 internal error\n"); + return reply_error(INTERNAL_ERROR); + } if (reply_success() < 0) { PRINTF("Error: failed to send\n"); - return -1; } diff --git a/src/start_new_transaction.c b/src/start_new_transaction.c index 37b72d1f..12465a63 100644 --- a/src/start_new_transaction.c +++ b/src/start_new_transaction.c @@ -15,6 +15,7 @@ int start_new_transaction(const command_t *cmd) { return reply_error(INTERNAL_ERROR); } + // Legacy swap flow : 10 char 'string' (not '\0' terminated) if (cmd->subcommand == SWAP) { output_buffer_size = sizeof(G_swap_ctx.device_transaction_id.swap); @@ -25,10 +26,9 @@ int start_new_transaction(const command_t *cmd) { G_swap_ctx.device_transaction_id.swap[i] = (char) ((int) 'A' + cx_rng_u8() % 26); #endif } - } - - if (cmd->subcommand == SELL || cmd->subcommand == FUND) { - output_buffer_size = sizeof(G_swap_ctx.device_transaction_id.sell_fund); + } else { + // All other flows : 32 bytes + output_buffer_size = sizeof(G_swap_ctx.device_transaction_id.unified); #ifdef TESTING unsigned char tx_id[32] = { @@ -37,9 +37,9 @@ int start_new_transaction(const command_t *cmd) { 0x23, 0x80, 0x1b, 0x1a, 0xeb, 0x7d, 0x0b, 0xcb, // 0xba, 0xa2, 0xa4, 0xf4, 0x6b, 0xf8, 0x18, 0x4b // }; - memcpy(G_swap_ctx.device_transaction_id.sell_fund, tx_id, sizeof(tx_id)); + memcpy(G_swap_ctx.device_transaction_id.unified, tx_id, sizeof(tx_id)); #else - cx_rng(G_swap_ctx.device_transaction_id.sell_fund, output_buffer_size); + cx_rng(G_swap_ctx.device_transaction_id.unified, output_buffer_size); #endif } diff --git a/src/start_signing_transaction.c b/src/start_signing_transaction.c index 44c755d6..fb21c2ef 100644 --- a/src/start_signing_transaction.c +++ b/src/start_signing_transaction.c @@ -5,26 +5,37 @@ int start_signing_transaction(const command_t *cmd) { int ret = 0; - G_io_apdu_buffer[0] = 0x90; - G_io_apdu_buffer[1] = 0x00; - io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); - G_swap_ctx.state = INITIAL_STATE; + + // Inform the caller that we will call the lib app + if (instant_reply_success() < 0) { + PRINTF("Error: failed to send\n"); + return -1; + } + + // Create the variable given to the signing app. It's placed in BSS section to save RAM + // (heap is shared, stack is not) static create_transaction_parameters_t lib_in_out_params; lib_in_out_params.fee_amount = G_swap_ctx.transaction_fee; lib_in_out_params.fee_amount_length = G_swap_ctx.transaction_fee_length; - lib_in_out_params.coin_configuration = G_swap_ctx.payin_coin_config.bytes; - lib_in_out_params.coin_configuration_length = G_swap_ctx.payin_coin_config.size; - - if (cmd->subcommand == SWAP) { - lib_in_out_params.amount = G_swap_ctx.received_transaction.amount_to_provider.bytes; - lib_in_out_params.amount_length = G_swap_ctx.received_transaction.amount_to_provider.size; - lib_in_out_params.destination_address = G_swap_ctx.received_transaction.payin_address; - lib_in_out_params.destination_address_extra_id = - G_swap_ctx.received_transaction.payin_extra_id; + lib_in_out_params.coin_configuration_length = G_swap_ctx.paying_sub_coin_config_size; + + // Small patch to give the app NULL pointer if there is no sub coin conf (Solana checks it) + // Solana should be changed to remove this unnecessary check, in the meantime we workaround here + if (G_swap_ctx.paying_sub_coin_config_size == 0) { + lib_in_out_params.coin_configuration = NULL; + } else { + lib_in_out_params.coin_configuration = G_swap_ctx.paying_sub_coin_config; + } + + if (cmd->subcommand == SWAP || cmd->subcommand == SWAP_NG) { + lib_in_out_params.amount = G_swap_ctx.swap_transaction.amount_to_provider.bytes; + lib_in_out_params.amount_length = G_swap_ctx.swap_transaction.amount_to_provider.size; + lib_in_out_params.destination_address = G_swap_ctx.swap_transaction.payin_address; + lib_in_out_params.destination_address_extra_id = G_swap_ctx.swap_transaction.payin_extra_id; } - if (cmd->subcommand == SELL) { + if (cmd->subcommand == SELL || cmd->subcommand == SELL_NG) { lib_in_out_params.amount = G_swap_ctx.sell_transaction.in_amount.bytes; lib_in_out_params.amount_length = G_swap_ctx.sell_transaction.in_amount.size; lib_in_out_params.destination_address = G_swap_ctx.sell_transaction.in_address; @@ -32,7 +43,7 @@ int start_signing_transaction(const command_t *cmd) { lib_in_out_params.destination_address_extra_id = G_swap_ctx.sell_transaction_extra_id; } - if (cmd->subcommand == FUND) { + if (cmd->subcommand == FUND || cmd->subcommand == FUND_NG) { lib_in_out_params.amount = G_swap_ctx.fund_transaction.in_amount.bytes; lib_in_out_params.amount_length = G_swap_ctx.fund_transaction.in_amount.size; lib_in_out_params.destination_address = G_swap_ctx.fund_transaction.in_address; diff --git a/src/states.h b/src/states.h index a1b2a140..5a0d48a2 100644 --- a/src/states.h +++ b/src/states.h @@ -11,5 +11,5 @@ typedef enum { WAITING_USER_VALIDATION = 7, WAITING_SIGNING = 8, SIGN_FINISHED = 9, - STATE_UPPER_BOUND + STATE_UPPER_BOUND, } state_e; diff --git a/src/swap_errors.h b/src/swap_errors.h index 880986d1..a487b019 100644 --- a/src/swap_errors.h +++ b/src/swap_errors.h @@ -1,5 +1,4 @@ -#ifndef _SWAP_ERRORS_H_ -#define _SWAP_ERRORS_H_ +#pragma once // This value is the part of the host <-> device protocol // they will be reported to host in APDU @@ -15,10 +14,14 @@ typedef enum { USER_REFUSED = 0x6A84, INTERNAL_ERROR = 0x6A85, WRONG_P1 = 0x6A86, - WRONG_P2 = 0x6A87, + WRONG_P2_SUBCOMMAND = 0x6A87, + WRONG_P2_EXTENSION = 0x6A88, + INVALID_P2_EXTENSION = 0x6A89, CLASS_NOT_SUPPORTED = 0x6E00, + MALFORMED_APDU = 0x6E01, + INVALID_DATA_LENGTH = 0x6E02, INVALID_INSTRUCTION = 0x6D00, - SIGN_VERIFICATION_FAIL = 0x9D1A + UNEXPECTED_INSTRUCTION = 0x6D01, + SIGN_VERIFICATION_FAIL = 0x9D1A, + SUCCESS = 0x9000, } swap_error_e; - -#endif //_SWAP_ERRORS_H_ \ No newline at end of file diff --git a/src/ticker_normalization.c b/src/ticker_normalization.c index c13c6fe3..95f8efe2 100644 --- a/src/ticker_normalization.c +++ b/src/ticker_normalization.c @@ -34,24 +34,24 @@ void set_ledger_currency_name(char *currency, size_t currency_size) { } // Check if a given ticker matches the current swap context -bool check_matching_ticker(const buf_t *ticker, const char *reference_ticker) { +bool check_matching_ticker(buf_t ticker, const char *reference_ticker) { char normalized_ticker_name[TICKER_MAX_SIZE_B]; uint8_t normalized_ticker_len; // Normalize the ticker name first - memcpy(normalized_ticker_name, ticker->bytes, sizeof(normalized_ticker_name)); - normalized_ticker_name[ticker->size] = '\0'; - to_uppercase(normalized_ticker_name, ticker->size); + memcpy(normalized_ticker_name, ticker.bytes, sizeof(normalized_ticker_name)); + normalized_ticker_name[ticker.size] = '\0'; + to_uppercase(normalized_ticker_name, ticker.size); set_ledger_currency_name(normalized_ticker_name, sizeof(normalized_ticker_name)); // Recalculate length in case it changed normalized_ticker_len = strlen(normalized_ticker_name); if (strncmp(normalized_ticker_name, - (const char *) ticker->bytes, - MAX(normalized_ticker_len, ticker->size)) != 0) { + (const char *) ticker.bytes, + MAX(normalized_ticker_len, ticker.size)) != 0) { PRINTF("Normalized ticker, from '%.*s' to '%s'\n", - ticker->size, - ticker->bytes, + ticker.size, + ticker.bytes, normalized_ticker_name); } diff --git a/src/ticker_normalization.h b/src/ticker_normalization.h index 88fbf3ac..1b8da078 100644 --- a/src/ticker_normalization.h +++ b/src/ticker_normalization.h @@ -4,4 +4,4 @@ void to_uppercase(char *str, unsigned char size); void set_ledger_currency_name(char *currency, size_t currency_size); -bool check_matching_ticker(const buf_t *ticker, const char *reference_ticker); +bool check_matching_ticker(buf_t ticker, const char *reference_ticker); diff --git a/src/ui/validate_transaction_bagl.c b/src/ui/validate_transaction_bagl.c index 23bf9aac..fd15add9 100644 --- a/src/ui/validate_transaction_bagl.c +++ b/src/ui/validate_transaction_bagl.c @@ -56,7 +56,7 @@ UX_STEP_NOCB(ux_confirm_flow_3_floating_step, bnnn_paging, UX_STEP_NOCB(ux_confirm_flow_3_2_step, bnnn_paging, { .title = G_swap_ctx.partner.prefixed_name, - .text = G_swap_ctx.printable_get_amount, + .text = G_swap_ctx.account_name, }); UX_STEP_NOCB(ux_confirm_flow_4_step, bnnn_paging, { @@ -83,15 +83,15 @@ void ui_validate_amounts(void) { ux_confirm_flow[step++] = &ux_confirm_flow_1_step; - if (G_swap_ctx.subcommand == SELL) { + if (G_swap_ctx.subcommand == SELL || G_swap_ctx.subcommand == SELL_NG) { ux_confirm_flow[step++] = &ux_confirm_flow_1_2_step; - } else if (G_swap_ctx.subcommand == FUND) { + } else if (G_swap_ctx.subcommand == FUND || G_swap_ctx.subcommand == FUND_NG) { ux_confirm_flow[step++] = &ux_confirm_flow_1_3_step; } ux_confirm_flow[step++] = &ux_confirm_flow_2_step; - if (G_swap_ctx.subcommand == FUND) { + if (G_swap_ctx.subcommand == FUND || G_swap_ctx.subcommand == FUND_NG) { ux_confirm_flow[step++] = &ux_confirm_flow_3_2_step; } else if (G_swap_ctx.rate == FLOATING) { ux_confirm_flow[step++] = &ux_confirm_flow_3_floating_step; diff --git a/test/python/apps/exchange.py b/test/python/apps/exchange.py index abec3a0c..50ffa21e 100644 --- a/test/python/apps/exchange.py +++ b/test/python/apps/exchange.py @@ -8,6 +8,11 @@ from ..utils import handle_lib_call_start_or_stop, int_to_minimally_sized_bytes from .exchange_transaction_builder import SubCommand +MAX_CHUNK_SIZE = 255 + +P2_EXTEND = 0x01 << 4 +P2_MORE = 0x02 << 4 + class Command(IntEnum): GET_VERSION = 0x02 @@ -40,6 +45,14 @@ class Errors(IntEnum): SIGN_VERIFICATION_FAIL = 0x9D1A +def prefix_with_len_custom(to_prefix: bytes, prefix_length: int = 1) -> bytes: + prefix = len(to_prefix).to_bytes(prefix_length, byteorder="big") + print(prefix.hex()) + b = prefix + to_prefix + # b = b'\0' + b + return b + + class ExchangeClient: CLA = 0xE0 def __init__(self, @@ -90,8 +103,27 @@ def check_partner_key(self, signed_credentials: bytes) -> RAPDU: def process_transaction(self, transaction: bytes, fees: int) -> RAPDU: fees_bytes = int_to_minimally_sized_bytes(fees) - payload = prefix_with_len(transaction) + prefix_with_len(fees_bytes) - return self._exchange(Command.PROCESS_TRANSACTION_RESPONSE, payload=payload) + prefix_length = 2 if (self.subcommand == SubCommand.SWAP_NG or self.subcommand == SubCommand.FUND_NG or self.subcommand == SubCommand.SELL_NG) else 1 + payload = prefix_with_len_custom(transaction, prefix_length) + prefix_with_len(fees_bytes) + + if self.subcommand == SubCommand.SWAP or self.subcommand == SubCommand.FUND or self.subcommand == SubCommand.SELL: + return self._exchange(Command.PROCESS_TRANSACTION_RESPONSE, payload=payload) + + else: + print(len(payload)) + print(payload.hex()) + payload_splited = [payload[x:x + MAX_CHUNK_SIZE] for x in range(0, len(payload), MAX_CHUNK_SIZE)] + for i, p in enumerate(payload_splited): + p2 = self.subcommand + # Send all chunks with P2_MORE except for the last chunk + if i != len(payload_splited) - 1: + p2 |= P2_MORE + # Send all chunks with P2_EXTEND except for the first chunk + if i != 0: + p2 |= P2_EXTEND + rapdu = self._client.exchange(self.CLA, Command.PROCESS_TRANSACTION_RESPONSE, p1=self.rate, p2=p2, data=p) + + return rapdu def check_transaction_signature(self, encoded_transaction: bytes) -> RAPDU: return self._exchange(Command.CHECK_TRANSACTION_SIGNATURE, payload=encoded_transaction) @@ -102,7 +134,7 @@ def check_address(self, refund_configuration: Optional[bytes] = None) -> Generator[None, None, None]: self._premature_error=False - if self._subcommand == SubCommand.SWAP: + if self._subcommand == SubCommand.SWAP or self._subcommand == SubCommand.SWAP_NG: assert refund_configuration != None, f'A refund currency is needed but no conf as been given' # If refund adress has to be checked, send sync CHECk_PAYOUT_ADDRESS first rapdu = self._exchange(Command.CHECK_PAYOUT_ADDRESS, payload=payout_configuration) diff --git a/test/python/apps/exchange_test_runner.py b/test/python/apps/exchange_test_runner.py index 8e0d20c6..e2ea9b5d 100644 --- a/test/python/apps/exchange_test_runner.py +++ b/test/python/apps/exchange_test_runner.py @@ -14,6 +14,8 @@ # When adding a new test, have it prefixed by this string in order to have it automatically parametrized for currencies tests TEST_METHOD_PREFIX="perform_test_" +TEST_LEGACY_SUFFIX="_legacy_flow" +TEST_UNIFIED_SUFFIX="_ng_flow" # Exchange tests helpers, create a child of this class that define coin-specific elements and call its tests entry points class ExchangeTestRunner: @@ -49,8 +51,15 @@ def __init__(self, backend, exchange_navigation_helper): self.exchange_navigation_helper = exchange_navigation_helper def run_test(self, function_to_test: str): + # Remove the flow suffix as the function is the same and the snapshot path is the same too + if function_to_test.endswith(TEST_LEGACY_SUFFIX): + use_legacy_flow = True + function_to_test = function_to_test.removesuffix(TEST_LEGACY_SUFFIX) + if function_to_test.endswith(TEST_UNIFIED_SUFFIX): + use_legacy_flow = False + function_to_test = function_to_test.removesuffix(TEST_UNIFIED_SUFFIX) self.exchange_navigation_helper.set_test_name_suffix("_" + function_to_test) - getattr(self, TEST_METHOD_PREFIX + function_to_test)() + getattr(self, TEST_METHOD_PREFIX + function_to_test)(use_legacy_flow) def _perform_valid_exchange(self, subcommand, tx_infos, fees, ui_validation): # Initialize the exchange client plugin that will format and send the APDUs to the device @@ -68,11 +77,11 @@ def _perform_valid_exchange(self, subcommand, tx_infos, fees, ui_validation): # Craft the exchange transaction proposal and have it signed by the enrolled partner tx = craft_tx(subcommand, tx_infos, transaction_id) - encoded_tx = encode_tx(subcommand, partner, tx) + signed_tx = encode_tx(subcommand, partner, tx) # Send the exchange transaction proposal and it's signature ex.process_transaction(tx, fees) - ex.check_transaction_signature(encoded_tx) + ex.check_transaction_signature(signed_tx) # Ask our fake CAL the coin configuration for both payout and refund tickers (None for refund in case of FUND or SELL) payout_ticker = extract_payout_ticker(subcommand, tx_infos) @@ -93,7 +102,7 @@ def _perform_valid_exchange(self, subcommand, tx_infos, fees, ui_validation): # Ask exchange to start the library application to sign the actual outgoing transaction ex.start_signing_transaction() - def perform_valid_swap_from_custom(self, destination, send_amount, fees, memo, refund_address=None, refund_memo=None, ui_validation=True): + def perform_valid_swap_from_custom(self, destination, send_amount, fees, memo, refund_address=None, refund_memo=None, ui_validation=True, legacy=False): refund_address = self.valid_refund if refund_address is None else refund_address refund_memo = self.valid_refund_memo if refund_memo is None else refund_memo tx_infos = { @@ -108,9 +117,10 @@ def perform_valid_swap_from_custom(self, destination, send_amount, fees, memo, r "amount_to_provider": int_to_minimally_sized_bytes(send_amount), "amount_to_wallet": b"\246\333t\233+\330\000", # Default } - self._perform_valid_exchange(SubCommand.SWAP, tx_infos, fees, ui_validation=ui_validation) + subcommand = SubCommand.SWAP if legacy else SubCommand.SWAP_NG + self._perform_valid_exchange(subcommand, tx_infos, fees, ui_validation=ui_validation) - def perform_valid_swap_to_custom(self, destination, send_amount, fees, memo, ui_validation=True): + def perform_valid_swap_to_custom(self, destination, send_amount, fees, memo, ui_validation=True, legacy=False): tx_infos = { "payin_address": "0xDad77910DbDFdE764fC21FCD4E74D71bBACA6D8D", # Default "payin_extra_id": "", # Default @@ -123,9 +133,10 @@ def perform_valid_swap_to_custom(self, destination, send_amount, fees, memo, ui_ "amount_to_provider": int_to_minimally_sized_bytes(send_amount), "amount_to_wallet": b"\246\333t\233+\330\000", # Default } - self._perform_valid_exchange(SubCommand.SWAP, tx_infos, fees, ui_validation=ui_validation) + subcommand = SubCommand.SWAP if legacy else SubCommand.SWAP_NG + self._perform_valid_exchange(subcommand, tx_infos, fees, ui_validation=ui_validation) - def perform_valid_fund_from_custom(self, destination, send_amount, fees): + def perform_valid_fund_from_custom(self, destination, send_amount, fees, legacy=False): tx_infos = { "user_id": self.fund_user_id, "account_name": self.fund_account_name, @@ -133,9 +144,10 @@ def perform_valid_fund_from_custom(self, destination, send_amount, fees): "in_amount": int_to_minimally_sized_bytes(send_amount), "in_address": destination, } - self._perform_valid_exchange(SubCommand.FUND, tx_infos, fees, ui_validation=True) + subcommand = SubCommand.FUND if legacy else SubCommand.FUND_NG + self._perform_valid_exchange(subcommand, tx_infos, fees, ui_validation=True) - def perform_valid_sell_from_custom(self, destination, send_amount, fees): + def perform_valid_sell_from_custom(self, destination, send_amount, fees, legacy=False): tx_infos = { "trader_email": self.sell_trader_email, "out_currency": self.sell_out_currency, @@ -144,7 +156,8 @@ def perform_valid_sell_from_custom(self, destination, send_amount, fees): "in_amount": int_to_minimally_sized_bytes(send_amount), "in_address": destination, } - self._perform_valid_exchange(SubCommand.SELL, tx_infos, fees, ui_validation=True) + subcommand = SubCommand.SELL if legacy else SubCommand.SELL_NG + self._perform_valid_exchange(subcommand, tx_infos, fees, ui_validation=True) # Implement this function for each tested coin def perform_final_tx(self, destination, send_amount, fees, memo): @@ -160,7 +173,7 @@ def perform_coin_specific_final_tx(self, destination, send_amount, fees, memo): ######################################################### # We test that the currency app returns a fail when checking an incorrect refund address - def perform_test_swap_wrong_refund(self): + def perform_test_swap_wrong_refund(self, legacy): with pytest.raises(ExceptionRAPDU) as e: self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, @@ -168,61 +181,62 @@ def perform_test_swap_wrong_refund(self): self.valid_destination_memo_1, refund_address=self.fake_refund, refund_memo=self.fake_refund_memo, - ui_validation=False) + ui_validation=False, + legacy=legacy) assert e.value.status == Errors.INVALID_ADDRESS # We test that the currency app returns a fail when checking an incorrect payout address - def perform_test_swap_wrong_payout(self): + def perform_test_swap_wrong_payout(self, legacy): with pytest.raises(ExceptionRAPDU) as e: - self.perform_valid_swap_to_custom(self.fake_payout, self.valid_send_amount_1, self.valid_fees_1, self.fake_payout_memo, ui_validation=False) + self.perform_valid_swap_to_custom(self.fake_payout, self.valid_send_amount_1, self.valid_fees_1, self.fake_payout_memo, ui_validation=False, legacy=legacy) assert e.value.status == Errors.INVALID_ADDRESS # The absolute standard swap, using default values, user accepts on UI - def perform_test_swap_valid_1(self): - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + def perform_test_swap_valid_1(self, legacy): + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) # The second standard swap, using alternate default values, user accepts on UI - def perform_test_swap_valid_2(self): - self.perform_valid_swap_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, self.valid_destination_memo_2) + def perform_test_swap_valid_2(self, legacy): + self.perform_valid_swap_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, self.valid_destination_memo_2, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, self.valid_destination_memo_2) # Make a valid swap and then ask a second signature - def perform_test_swap_refuse_double_sign(self): - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + def perform_test_swap_refuse_double_sign(self, legacy): + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) - assert e.value.status == Errors.INVALID_INSTRUCTION or e.value.status == Errors.WRONG_P2 + assert e.value.status != 0x9000 # Test swap with a malicious TX with tampered fees - def perform_test_swap_wrong_fees(self): + def perform_test_swap_wrong_fees(self, legacy): assert self.valid_fees_1 != self.valid_fees_2, "This test won't work if the values are the same" - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_2, self.valid_destination_memo_1) assert e.value.status == self.signature_refusal_error_code # Test swap with a malicious TX with tampered memo - def perform_test_swap_wrong_memo(self): + def perform_test_swap_wrong_memo(self, legacy): assert self.valid_destination_memo_1 != self.valid_destination_memo_2, "This test won't work if the values are the same" - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_2) assert e.value.status == self.signature_refusal_error_code # Test swap with a malicious TX with tampered destination - def perform_test_swap_wrong_destination(self): + def perform_test_swap_wrong_destination(self, legacy): assert self.valid_destination_1 != self.valid_destination_2, "This test won't work if the values are the same" - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) assert e.value.status == self.signature_refusal_error_code # Test swap with a malicious TX with tampered amount - def perform_test_swap_wrong_amount(self): + def perform_test_swap_wrong_amount(self, legacy): assert self.valid_send_amount_1 != self.valid_send_amount_2, "This test won't work if the values are the same" - self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1) + self.perform_valid_swap_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, self.valid_destination_memo_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_2, self.valid_fees_1, self.valid_destination_memo_1) assert e.value.status == self.signature_refusal_error_code @@ -232,50 +246,50 @@ def perform_test_swap_wrong_amount(self): ######################################################### # The absolute standard fund, using default values, user accepts on UI - def perform_test_fund_valid_1(self): - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_fund_valid_1(self, legacy): + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") # The second standard fund, using alternate default values, user accepts on UI - def perform_test_fund_valid_2(self): - self.perform_valid_fund_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2) + def perform_test_fund_valid_2(self, legacy): + self.perform_valid_fund_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, "") # Make a valid fund and then ask a second signature - def perform_test_fund_refuse_double_sign(self): - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_fund_refuse_double_sign(self, legacy): + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") - assert e.value.status == Errors.INVALID_INSTRUCTION + assert e.value.status != 0x9000 # Test fund with a malicious TX with tampered fees - def perform_test_fund_wrong_fees(self): + def perform_test_fund_wrong_fees(self, legacy): assert self.valid_fees_1 != self.valid_fees_2, "This test won't work if the values are the same" - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_2, "") assert e.value.status == self.signature_refusal_error_code # Test fund with a malicious TX with tampered memo - def perform_test_fund_wrong_memo(self): - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_fund_wrong_memo(self, legacy): + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "no memo expected") assert e.value.status == self.signature_refusal_error_code # Test fund with a malicious TX with tampered destination - def perform_test_fund_wrong_destination(self): + def perform_test_fund_wrong_destination(self, legacy): assert self.valid_destination_1 != self.valid_destination_2, "This test won't work if the values are the same" - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_1, self.valid_fees_1, "") assert e.value.status == self.signature_refusal_error_code # Test fund with a malicious TX with tampered amount - def perform_test_fund_wrong_amount(self): + def perform_test_fund_wrong_amount(self, legacy): assert self.valid_send_amount_1 != self.valid_send_amount_2, "This test won't work if the values are the same" - self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_fund_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_2, self.valid_fees_1, "") assert e.value.status == self.signature_refusal_error_code @@ -285,50 +299,50 @@ def perform_test_fund_wrong_amount(self): ######################################################### # The absolute standard sell, using default values, user accepts on UI - def perform_test_sell_valid_1(self): - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_sell_valid_1(self, legacy): + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") # The second standard sell, using alternate default values, user accepts on UI - def perform_test_sell_valid_2(self): - self.perform_valid_sell_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2) + def perform_test_sell_valid_2(self, legacy): + self.perform_valid_sell_from_custom(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_2, self.valid_fees_2, "") # Make a valid sell and then ask a second signature - def perform_test_sell_refuse_double_sign(self): - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_sell_refuse_double_sign(self, legacy): + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "") - assert e.value.status == Errors.INVALID_INSTRUCTION + assert e.value.status != 0x9000 # Test sell with a malicious TX with tampered fees - def perform_test_sell_wrong_fees(self): + def perform_test_sell_wrong_fees(self, legacy): assert self.valid_fees_1 != self.valid_fees_2, "This test won't work if the values are the same" - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_2, "") assert e.value.status == self.signature_refusal_error_code # Test sell with a malicious TX with tampered memo - def perform_test_sell_wrong_memo(self): - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + def perform_test_sell_wrong_memo(self, legacy): + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, "no memo expected") assert e.value.status == self.signature_refusal_error_code # Test sell with a malicious TX with tampered destination - def perform_test_sell_wrong_destination(self): + def perform_test_sell_wrong_destination(self, legacy): assert self.valid_destination_1 != self.valid_destination_2, "This test won't work if the values are the same" - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_2, self.valid_send_amount_1, self.valid_fees_1, "") assert e.value.status == self.signature_refusal_error_code # Test sell with a malicious TX with tampered amount - def perform_test_sell_wrong_amount(self): + def perform_test_sell_wrong_amount(self, legacy): assert self.valid_send_amount_1 != self.valid_send_amount_2, "This test won't work if the values are the same" - self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1) + self.perform_valid_sell_from_custom(self.valid_destination_1, self.valid_send_amount_1, self.valid_fees_1, legacy=legacy) with pytest.raises(ExceptionRAPDU) as e: self.perform_coin_specific_final_tx(self.valid_destination_1, self.valid_send_amount_2, self.valid_fees_1, "") assert e.value.status == self.signature_refusal_error_code @@ -336,7 +350,11 @@ def perform_test_sell_wrong_amount(self): # Automatically collect all tests functions and export their name in ready-to-be-parametrized lists _all_test_methods_prefixed = [method for method in dir(ExchangeTestRunner) if method.startswith(TEST_METHOD_PREFIX)] # Remove prefix to have nice snapshots directories -ALL_TESTS = [str(i).replace(TEST_METHOD_PREFIX, '') for i in _all_test_methods_prefixed] +ALL_TESTS_NAME = [str(i).replace(TEST_METHOD_PREFIX, '') for i in _all_test_methods_prefixed] + +# Parametrize with NG too +ALL_TESTS = [x + suffix for x in ALL_TESTS_NAME for suffix in (TEST_LEGACY_SUFFIX, TEST_UNIFIED_SUFFIX)] + ALL_TESTS_EXCEPT_MEMO = [test for test in ALL_TESTS if not "memo" in test] ALL_TESTS_EXCEPT_MEMO_AND_FEES = [test for test in ALL_TESTS if (not "memo" in test and not "fees" in test)] SWAP_TESTS = [test for test in ALL_TESTS if "swap" in test] diff --git a/test/python/apps/exchange_transaction_builder.py b/test/python/apps/exchange_transaction_builder.py index 9ce7868a..916127a4 100644 --- a/test/python/apps/exchange_transaction_builder.py +++ b/test/python/apps/exchange_transaction_builder.py @@ -30,25 +30,20 @@ class SubCommand(IntEnum): SWAP = 0x00 SELL = 0x01 FUND = 0x02 + SWAP_NG = 0x03 + SELL_NG = 0x04 + FUND_NG = 0x05 -SUBCOMMAND_TO_CURVE = { - SubCommand.SWAP: ec.SECP256K1(), - SubCommand.SELL: ec.SECP256R1(), - SubCommand.FUND: ec.SECP256R1(), -} - - -def get_partner_curve(sub_command: SubCommand) -> ec.EllipticCurve: - return SUBCOMMAND_TO_CURVE[sub_command] - @dataclass(frozen=True) class SubCommandSpecs: + partner_curve: ec.EllipticCurve signature_computation: SignatureComputation signature_encoding: SignatureEncoding payload_encoding: PayloadEncoding transaction_type: Callable required_fields: Iterable[str] + transaction_id_field: str payout_field: str refund_field: Optional[str] @@ -74,46 +69,87 @@ def encode_signature(self, signature_to_encode: bytes) -> bytes: return signature_to_encode def create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes: - raw_transaction = self.transaction_type(**conf, device_transaction_id=transaction_id).SerializeToString() + # Alter a copy of conf to not modify the actual conf + c = conf.copy() + c[self.transaction_id_field] = transaction_id + raw_transaction = self.transaction_type(**c).SerializeToString() return self.encode_payload(raw_transaction) +SWAP_NG_SPECS: SubCommandSpecs = SubCommandSpecs( + partner_curve = ec.SECP256R1(), + signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL, + signature_encoding = SignatureEncoding.PLAIN_R_S, + payload_encoding = PayloadEncoding.BASE_64_URL, + transaction_type = NewTransactionResponse, + required_fields = ["payin_address", "payin_extra_id", "refund_address", "refund_extra_id", + "payout_address", "payout_extra_id", "currency_from", "currency_to", + "amount_to_provider", "amount_to_wallet"], + transaction_id_field = "device_transaction_id_ng", + payout_field = "currency_to", + refund_field = "currency_from", +) + SWAP_SPECS: SubCommandSpecs = SubCommandSpecs( + partner_curve = ec.SECP256K1(), signature_computation = SignatureComputation.BINARY_ENCODED_PAYLOAD, signature_encoding = SignatureEncoding.DER, payload_encoding = PayloadEncoding.BYTES_ARRAY, transaction_type = NewTransactionResponse, required_fields = ["payin_address", "payin_extra_id", "refund_address", "refund_extra_id", - "payout_address", "payout_extra_id", "currency_from", "currency_to", - "amount_to_provider", "amount_to_wallet"], - payout_field= "currency_to", - refund_field= "currency_from" + "payout_address", "payout_extra_id", "currency_from", "currency_to", + "amount_to_provider", "amount_to_wallet"], + transaction_id_field = "device_transaction_id", + payout_field = "currency_to", + refund_field = "currency_from", ) -SELL_SPECS: SubCommandSpecs = SubCommandSpecs( +SELL_NG_SPECS: SubCommandSpecs = SubCommandSpecs( + partner_curve = ec.SECP256R1(), signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL, signature_encoding = SignatureEncoding.PLAIN_R_S, payload_encoding = PayloadEncoding.BASE_64_URL, transaction_type = NewSellResponse, + transaction_id_field = "device_transaction_id", required_fields = ["trader_email", "in_currency", "in_amount", "in_address", "out_currency", "out_amount"], - payout_field="in_currency", - refund_field= None + payout_field = "in_currency", + refund_field = None, +) + +# Legacy SELL specs happen to be the same as the unified specs +SELL_SPECS: SubCommandSpecs = SELL_NG_SPECS + +FUND_NG_SPECS: SubCommandSpecs = SubCommandSpecs( + partner_curve = ec.SECP256R1(), + signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL, + signature_encoding = SignatureEncoding.PLAIN_R_S, + payload_encoding = PayloadEncoding.BASE_64_URL, + transaction_type = NewFundResponse, + required_fields = ["user_id", "account_name", "in_currency", "in_amount", "in_address"], + transaction_id_field = "device_transaction_id", + payout_field = "in_currency", + refund_field = None, ) FUND_SPECS: SubCommandSpecs = SubCommandSpecs( + partner_curve = ec.SECP256R1(), signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL, signature_encoding = SignatureEncoding.DER, payload_encoding = PayloadEncoding.BASE_64_URL, transaction_type = NewFundResponse, required_fields = ["user_id", "account_name", "in_currency", "in_amount", "in_address"], - payout_field="in_currency", - refund_field= None + transaction_id_field = "device_transaction_id", + payout_field = "in_currency", + refund_field = None, ) SUBCOMMAND_TO_SPECS = { SubCommand.SWAP: SWAP_SPECS, SubCommand.SELL: SELL_SPECS, SubCommand.FUND: FUND_SPECS, + SubCommand.SWAP_NG: SWAP_NG_SPECS, + SubCommand.SELL_NG: SELL_NG_SPECS, + SubCommand.FUND_NG: FUND_NG_SPECS, } def craft_tx(subcommand: SubCommand, conf: Dict, transaction_id: bytes) -> bytes: @@ -137,3 +173,6 @@ def extract_refund_ticker(subcommand: SubCommand, tx_infos: Dict) -> Optional[st return tx_infos[subcommand_specs.refund_field] else: return None + +def get_partner_curve(sub_command: SubCommand) -> ec.EllipticCurve: + return SUBCOMMAND_TO_SPECS[sub_command].partner_curve diff --git a/test/python/apps/pb/exchange_pb2.py b/test/python/apps/pb/exchange_pb2.py index b130cd43..9313ae5f 100644 --- a/test/python/apps/pb/exchange_pb2.py +++ b/test/python/apps/pb/exchange_pb2.py @@ -19,7 +19,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x18src/proto/protocol.proto\x12\x0bledger_swap\"\xaa\x02\n\x16NewTransactionResponse\x12\x15\n\rpayin_address\x18\x01 \x01(\t\x12\x16\n\x0epayin_extra_id\x18\x02 \x01(\t\x12\x16\n\x0erefund_address\x18\x03 \x01(\t\x12\x17\n\x0frefund_extra_id\x18\x04 \x01(\t\x12\x16\n\x0epayout_address\x18\x05 \x01(\t\x12\x17\n\x0fpayout_extra_id\x18\x06 \x01(\t\x12\x15\n\rcurrency_from\x18\x07 \x01(\t\x12\x13\n\x0b\x63urrency_to\x18\x08 \x01(\t\x12\x1a\n\x12\x61mount_to_provider\x18\t \x01(\x0c\x12\x18\n\x10\x61mount_to_wallet\x18\n \x01(\x0c\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x0b \x01(\t\"1\n\x08UDecimal\x12\x13\n\x0b\x63oefficient\x18\x01 \x01(\x0c\x12\x10\n\x08\x65xponent\x18\x02 \x01(\r\"\xc3\x01\n\x0fNewSellResponse\x12\x14\n\x0ctrader_email\x18\x01 \x01(\t\x12\x13\n\x0bin_currency\x18\x02 \x01(\t\x12\x11\n\tin_amount\x18\x03 \x01(\x0c\x12\x12\n\nin_address\x18\x04 \x01(\t\x12\x14\n\x0cout_currency\x18\x05 \x01(\t\x12)\n\nout_amount\x18\x06 \x01(\x0b\x32\x15.ledger_swap.UDecimal\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x07 \x01(\x0c\"\x93\x01\n\x0fNewFundResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x14\n\x0c\x61\x63\x63ount_name\x18\x02 \x01(\t\x12\x13\n\x0bin_currency\x18\x03 \x01(\t\x12\x11\n\tin_amount\x18\x04 \x01(\x0c\x12\x12\n\nin_address\x18\x05 \x01(\t\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x06 \x01(\x0c\x62\x06proto3' + serialized_pb=b'\n\x18src/proto/protocol.proto\x12\x0bledger_swap\"\xcc\x02\n\x16NewTransactionResponse\x12\x15\n\rpayin_address\x18\x01 \x01(\t\x12\x16\n\x0epayin_extra_id\x18\x02 \x01(\t\x12\x16\n\x0erefund_address\x18\x03 \x01(\t\x12\x17\n\x0frefund_extra_id\x18\x04 \x01(\t\x12\x16\n\x0epayout_address\x18\x05 \x01(\t\x12\x17\n\x0fpayout_extra_id\x18\x06 \x01(\t\x12\x15\n\rcurrency_from\x18\x07 \x01(\t\x12\x13\n\x0b\x63urrency_to\x18\x08 \x01(\t\x12\x1a\n\x12\x61mount_to_provider\x18\t \x01(\x0c\x12\x18\n\x10\x61mount_to_wallet\x18\n \x01(\x0c\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x0b \x01(\t\x12 \n\x18\x64\x65vice_transaction_id_ng\x18\x0c \x01(\x0c\"1\n\x08UDecimal\x12\x13\n\x0b\x63oefficient\x18\x01 \x01(\x0c\x12\x10\n\x08\x65xponent\x18\x02 \x01(\r\"\xc3\x01\n\x0fNewSellResponse\x12\x14\n\x0ctrader_email\x18\x01 \x01(\t\x12\x13\n\x0bin_currency\x18\x02 \x01(\t\x12\x11\n\tin_amount\x18\x03 \x01(\x0c\x12\x12\n\nin_address\x18\x04 \x01(\t\x12\x14\n\x0cout_currency\x18\x05 \x01(\t\x12)\n\nout_amount\x18\x06 \x01(\x0b\x32\x15.ledger_swap.UDecimal\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x07 \x01(\x0c\"\x93\x01\n\x0fNewFundResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x14\n\x0c\x61\x63\x63ount_name\x18\x02 \x01(\t\x12\x13\n\x0bin_currency\x18\x03 \x01(\t\x12\x11\n\tin_amount\x18\x04 \x01(\x0c\x12\x12\n\nin_address\x18\x05 \x01(\t\x12\x1d\n\x15\x64\x65vice_transaction_id\x18\x06 \x01(\x0c\x62\x06proto3' ) @@ -110,6 +110,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='device_transaction_id_ng', full_name='ledger_swap.NewTransactionResponse.device_transaction_id_ng', index=11, + number=12, 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, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -123,7 +130,7 @@ oneofs=[ ], serialized_start=42, - serialized_end=340, + serialized_end=374, ) @@ -161,8 +168,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=342, - serialized_end=391, + serialized_start=376, + serialized_end=425, ) @@ -235,8 +242,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=394, - serialized_end=589, + serialized_start=428, + serialized_end=623, ) @@ -302,8 +309,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=592, - serialized_end=739, + serialized_start=626, + serialized_end=773, ) _NEWSELLRESPONSE.fields_by_name['out_amount'].message_type = _UDECIMAL