From b9d7c7f9a63e9c1663a4995b9c2022077d1155d3 Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Fri, 13 Dec 2024 20:07:26 +0100 Subject: [PATCH] wip --- Makefile | 7 + ledger_app.toml | 7 +- src/apdu_parser.c | 16 +- src/buf.h | 5 + src/get_challenge_handler.c | 6 +- src/pki.c | 43 + src/pki.h | 14 + src/states.h | 2 - src/swap_errors.h | 6 + src/tlv.h | 41 + src/tlv_utils.c | 252 ++++++ src/tlv_utils.h | 147 ++++ src/trusted_name_descriptor_handler.c | 947 ++++----------------- src/trusted_name_descriptor_handler.h | 5 - test/python/apps/cal.py | 3 +- test/python/apps/exchange.py | 128 ++- test/python/apps/keychain/trusted_name.pem | 8 + test/python/apps/solana.py | 7 +- test/python/apps/solana_keychain.py | 30 + test/python/apps/solana_tlv.py | 42 + test/python/apps/solana_utils.py | 25 +- test/python/test_flow_order.py | 75 +- test/python/test_solana.py | 180 ++-- test/python/test_trusted_name.py | 217 +++++ 24 files changed, 1260 insertions(+), 953 deletions(-) create mode 100644 src/pki.c create mode 100644 src/pki.h create mode 100644 src/tlv.h create mode 100644 src/tlv_utils.c create mode 100644 src/tlv_utils.h create mode 100644 test/python/apps/keychain/trusted_name.pem create mode 100644 test/python/apps/solana_keychain.py create mode 100644 test/python/apps/solana_tlv.py create mode 100644 test/python/test_trusted_name.py diff --git a/Makefile b/Makefile index f9089d1a..27774eca 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,13 @@ ifdef TEST_PUBLIC_KEY DEFINES += TEST_PUBLIC_KEY endif +ifdef TRUSTED_NAME_TEST_KEY + $(info [INFO] TRUSTED_NAME_TEST_KEY enabled) + DEFINES += TRUSTED_NAME_TEST_KEY +endif + +DEFINES += SDK_TLV_PARSER + ######################################## # Protobuf files regeneration # diff --git a/ledger_app.toml b/ledger_app.toml index 55c76f90..02c30056 100644 --- a/ledger_app.toml +++ b/ledger_app.toml @@ -4,9 +4,10 @@ sdk = "C" devices = ["nanos", "nanox", "nanos+", "stax", "flex"] [use_cases] -use_test_keys = "TEST_PUBLIC_KEY=1" -dbg_use_test_keys = "DEBUG=1 TEST_PUBLIC_KEY=1" -testing_dbg_use_test_keys = "DEBUG=1 TESTING=1 TEST_PUBLIC_KEY=1" +use_test_keys = "TEST_PUBLIC_KEY=1 TRUSTED_NAME_TEST_KEY=1" +dbg_use_test_keys = "DEBUG=1 TEST_PUBLIC_KEY=1 TRUSTED_NAME_TEST_KEY=1" +full_replay = "DEBUG=1 TESTING=1 TEST_PUBLIC_KEY=1 TRUSTED_NAME_TEST_KEY=1 FIXED_TLV_CHALLENGE=1" +dbg_full_replay = "DEBUG=1 TESTING=1 TEST_PUBLIC_KEY=1 TRUSTED_NAME_TEST_KEY=1 FIXED_TLV_CHALLENGE=1" [tests] pytest_directory = "./test/python/" diff --git a/src/apdu_parser.c b/src/apdu_parser.c index dc1366aa..7a53efcc 100644 --- a/src/apdu_parser.c +++ b/src/apdu_parser.c @@ -66,6 +66,16 @@ static uint16_t check_instruction(uint8_t instruction, uint8_t subcommand) { return INVALID_INSTRUCTION; } + if (instruction == GET_CHALLENGE && (subcommand != SWAP && subcommand != SWAP_NG)) { + PRINTF("Instruction GET_CHALLENGE is only for SWAP based flows\n"); + return INVALID_INSTRUCTION; + } + + if (instruction == SEND_TRUSTED_NAME_DESCRIPTOR && (subcommand != SWAP && subcommand != SWAP_NG)) { + PRINTF("Instruction SEND_TRUSTED_NAME_DESCRIPTOR is only for SWAP based flows\n"); + return INVALID_INSTRUCTION; + } + if ((instruction == CHECK_REFUND_ADDRESS_AND_DISPLAY || instruction == CHECK_REFUND_ADDRESS_NO_DISPLAY) && (subcommand != SWAP && subcommand != SWAP_NG)) { @@ -108,7 +118,7 @@ static uint16_t check_instruction(uint8_t instruction, uint8_t subcommand) { check_subcommand_context = true; break; case SEND_TRUSTED_NAME_DESCRIPTOR: - check_current_state = CHALLENGE_SENT; + check_current_state = SIGNATURE_CHECKED; check_subcommand_context = true; break; case CHECK_PAYOUT_ADDRESS: @@ -229,8 +239,8 @@ uint16_t check_apdu_validity(uint8_t *apdu, size_t apdu_length, command_t *comma 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", + if (instruction != PROCESS_TRANSACTION_RESPONSE_COMMAND && instruction != SEND_TRUSTED_NAME_DESCRIPTOR && !is_whole_apdu) { + PRINTF("Extension %d refused, only allowed for PROCESS_TRANSACTION_RESPONSE and SEND_TRUSTED_NAME_DESCRIPTOR instruction\n", extension); return WRONG_P2_EXTENSION; } diff --git a/src/buf.h b/src/buf.h index a93dc91a..45804688 100644 --- a/src/buf.h +++ b/src/buf.h @@ -9,6 +9,11 @@ typedef struct buf_s { uint16_t size; } buf_t; +typedef struct cbuf_s { + const uint8_t *bytes; + uint16_t size; +} cbuf_t; + bool parse_to_sized_buffer(uint8_t *in_buffer, uint16_t in_size, uint8_t size_of_length_field, diff --git a/src/get_challenge_handler.c b/src/get_challenge_handler.c index 55b6114c..286967c1 100644 --- a/src/get_challenge_handler.c +++ b/src/get_challenge_handler.c @@ -13,7 +13,7 @@ static uint32_t challenge; * Generate a new challenge from the Random Number Generator */ void roll_challenge(void) { -#ifdef HAVE_TRUSTED_NAME_TEST +#ifdef FIXED_TLV_CHALLENGE challenge = 0xdeadbeef; #else challenge = cx_rng_u32(); @@ -49,7 +49,5 @@ int get_challenge_handler(void) { return -1; } - G_swap_ctx.state = CHALLENGE_SENT; - return 0; -} \ No newline at end of file +} diff --git a/src/pki.c b/src/pki.c new file mode 100644 index 00000000..1cb673cf --- /dev/null +++ b/src/pki.c @@ -0,0 +1,43 @@ +#include "os.h" +#include "cx.h" +#include "pki.h" +#include "os_pki.h" + +int check_signature_with_pki(uint8_t *buffer, + uint8_t buffer_length, + uint8_t expected_key_usage, + const cbuf_t *signature) { + uint8_t key_usage = 0; + size_t certificate_name_len = 0; + uint8_t certificate_name[CERTIFICATE_TRUSTED_NAME_MAXLEN] = {0}; + cx_ecfp_384_public_key_t public_key = {0}; + + bolos_err_t bolos_err; + bolos_err = os_pki_get_info(&key_usage, certificate_name, &certificate_name_len, &public_key); + if (bolos_err != 0x0000) { + PRINTF("Error %x while getting PKI certificate info\n", bolos_err); + return -1; + } + + if (key_usage != expected_key_usage) { + PRINTF("Wrong usage certificate %d, expected %d\n", key_usage, expected_key_usage); + return -1; + } + + PRINTF("Certificate '%s' loaded with success\n", certificate_name); + + // Checking the signature with PKI + if (!os_pki_verify(buffer, buffer_length, (uint8_t *)signature->bytes, signature->size)) { + PRINTF("Error, '%.*H' is not a signature of buffer '%.*H' by the PKI key '%.*H'\n", + signature->size, + signature->bytes, + buffer_length, + buffer, + sizeof(public_key), + &public_key); + return -1; + } + + PRINTF("Signature verified successfully\n"); + return 0; +} diff --git a/src/pki.h b/src/pki.h new file mode 100644 index 00000000..0ab71bf3 --- /dev/null +++ b/src/pki.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "buf.h" + +int check_signature_with_pki(uint8_t *buffer, + uint8_t buffer_length, + uint8_t expected_key_usage, + const cbuf_t *signature); diff --git a/src/states.h b/src/states.h index 42d2ddef..f9fed247 100644 --- a/src/states.h +++ b/src/states.h @@ -7,8 +7,6 @@ typedef enum { PROVIDER_CHECKED, TRANSACTION_RECEIVED, SIGNATURE_CHECKED, - CHALLENGE_SENT, - TRUSTED_NAME_DESCRIPTOR_RECEIVED, PAYOUT_ADDRESS_CHECKED, ALL_ADDRESSES_CHECKED, WAITING_USER_VALIDATION, diff --git a/src/swap_errors.h b/src/swap_errors.h index adb37cf8..0aa9ba2f 100644 --- a/src/swap_errors.h +++ b/src/swap_errors.h @@ -21,6 +21,12 @@ typedef enum { AMOUNT_FORMATTING_FAILED = 0x6A8B, APPLICATION_NOT_INSTALLED = 0x6A8C, WRONG_EXTRA_ID_OR_EXTRA_DATA = 0x6A8D, + WRONG_CHALLENGE = 0x6A8E, + WRONG_TLV_CONTENT = 0x6A8F, + MISSING_TLV_CONTENT = 0x6A90, + WRONG_TLV_FORMAT = 0x6A91, + WRONG_TLV_KEY_ID = 0x6A92, + WRONG_TLV_SIGNATURE = 0x6A93, CLASS_NOT_SUPPORTED = 0x6E00, MALFORMED_APDU = 0x6E01, INVALID_DATA_LENGTH = 0x6E02, diff --git a/src/tlv.h b/src/tlv.h new file mode 100644 index 00000000..727f4521 --- /dev/null +++ b/src/tlv.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "buf.h" + +// List of TLV tags recognized by the Exchange application +#define TLV_TAGS \ + X(STRUCT_TYPE, 0x01) \ + X(STRUCT_VERSION, 0x02) \ + X(TRUSTED_NAME_TYPE, 0x70) \ + X(TRUSTED_NAME_SOURCE, 0x71) \ + X(TRUSTED_NAME, 0x20) \ + X(CHAIN_ID, 0x23) \ + X(ADDRESS, 0x22) \ + X(SOURCE_CONTRACT, 0x73) \ + X(CHALLENGE, 0x12) \ + X(SIGNER_KEY_ID, 0x13) \ + X(SIGNER_ALGO, 0x14) \ + X(SIGNATURE, 0x15) + +// Parsed TLV data +typedef struct tlv_out_s { + uint8_t struct_version; + uint8_t struct_type; + cbuf_t token_account; + cbuf_t owner; + cbuf_t spl_token; + uint64_t chain_id; + uint32_t challenge; + uint8_t name_type; + uint8_t name_source; + + uint8_t key_id; + uint8_t sig_algorithm; + cbuf_t input_sig; +} tlv_out_t; diff --git a/src/tlv_utils.c b/src/tlv_utils.c new file mode 100644 index 00000000..5a568338 --- /dev/null +++ b/src/tlv_utils.c @@ -0,0 +1,252 @@ +#include "tlv_utils.h" + +#define DER_LONG_FORM_FLAG 0x80 // 8th bit set +#define DER_FIRST_BYTE_VALUE_MASK 0x7f + +bool get_uint_from_tlv_data(const tlv_data_t *data, uint32_t *value) { + uint8_t size_diff; + uint8_t buffer[sizeof(uint32_t)]; + + if (data->length > sizeof(buffer)) { + PRINTF("Unexpectedly long value (%u bytes) for tag 0x%x\n", data->length, data->tag); + return false; + } + size_diff = sizeof(buffer) - data->length; + memset(buffer, 0, size_diff); + memcpy(buffer + size_diff, data->value, data->length); + *value = U4BE(buffer, 0); + return true; +} + +bool get_uint8_t_from_tlv_data(const tlv_data_t *data, uint8_t *value) { + uint32_t tmp_value; + if (!get_uint_from_tlv_data(data, &tmp_value) || (tmp_value > UINT8_MAX)) { + return false; + } + *value = tmp_value; + return true; +} + +bool get_cbuf_from_tlv_data(const tlv_data_t *data, cbuf_t *out, uint16_t min_size, uint16_t max_size) { + if (data->length < min_size) { + PRINTF("Expected at least %d bytes, found %D\n", min_size, data->length); + return false; + } + if (max_size != 0 && data->length > max_size) { + PRINTF("Expected at most %d bytes, found %D\n", max_size, data->length); + return false; + } + out->size = data->length; + out->bytes = data->value; + return true; +} + +/** Parse DER-encoded value + * + * Parses a DER-encoded value (up to 4 bytes long) + * https://en.wikipedia.org/wiki/X.690 + * + * @param[in] payload the TLV payload + * @param[in,out] offset the payload offset + * @param[out] value the parsed value + * @return whether it was successful + */ +bool parse_der_value(const buf_t *payload, size_t *offset, uint32_t *value) { + bool ret = false; + uint8_t byte_length; + uint8_t buf[sizeof(*value)]; + + if (value != NULL) { + if (payload->bytes[*offset] & DER_LONG_FORM_FLAG) { // long form + byte_length = payload->bytes[*offset] & DER_FIRST_BYTE_VALUE_MASK; + *offset += 1; + if ((*offset + byte_length) > payload->size) { + PRINTF("TLV payload too small for DER encoded value\n"); + } else { + if (byte_length > sizeof(buf) || byte_length == 0) { + PRINTF("Unexpectedly long DER-encoded value (%u bytes)\n", byte_length); + } else { + memset(buf, 0, (sizeof(buf) - byte_length)); + memcpy(buf + (sizeof(buf) - byte_length), &payload->bytes[*offset], byte_length); + *value = U4BE(buf, 0); + *offset += byte_length; + ret = true; + } + } + } else { // short form + *value = payload->bytes[*offset]; + *offset += 1; + ret = true; + } + } + return ret; +} + +/** + * Get DER-encoded value as an uint8 + * + * Parses the value and checks if it fits in the given \ref uint8_t value + * + * @param[in] payload the TLV payload + * @param[in,out] offset + * @param[out] value the parsed value + * @return whether it was successful + */ +bool get_der_value_as_uint8(const buf_t *payload, size_t *offset, uint8_t *value) { + uint32_t tmp_value; + if (!parse_der_value(payload, offset, &tmp_value)) { + return false; + } + + if (tmp_value > UINT8_MAX) { + PRINTF("TLV DER-encoded value %d does not fit in uint8_t\n", tmp_value); + return false; + } + + *value = (uint8_t) tmp_value; + return true; +} + +// Some Xmacro dark magic to assign to each tag_FLAG its flag value +// Used by the function below that maps TAGS to FLAGS +typedef enum tlv_rcv_bit_e { + #define X(name, value) name##_FLAG = (1 << __COUNTER__), + TLV_TAGS + #undef X +} tlv_rcv_bit_t; + +// Some Xmacro dark magic to get the flag value from the tag +// 0(n) complexity but we don't care +uint32_t get_tag_flag(tlv_tag_t tag) { + switch(tag) { + #define X(name, value) case name: return name##_FLAG; + TLV_TAGS + #undef X + default: + return (uint32_t) -1; + } +} + +/** + * Calls the proper handler for the given TLV data + * + * Checks if there is a proper handler function for the given TLV tag and then calls it + * + * @param[in] handlers list of tag / handler function pairs + * @param[in] data the TLV data + * @param[in/out] received_tags_flags the tags flagged as received + * @param[out] tlv_out the parsed tlv + * @return whether it was successful + */ +static bool handle_tlv_data(const tlv_handler_t handlers[TLV_COUNT], + const tlv_data_t *data, + uint32_t *received_tags_flags, + tlv_out_t *tlv_out) { + // check if a handler exists for this tag + for (uint8_t idx = 0; idx < TLV_COUNT; ++idx) { + if (handlers[idx].tag == data->tag) { + // Refuse if this tag was already received + if (*received_tags_flags & get_tag_flag(handlers[idx].tag)) { + PRINTF("Tag 0x%x already received, rejecting duplicate\n", handlers[idx].tag); + return false; + } + + // Mark this tag as received + *received_tags_flags |= get_tag_flag(handlers[idx].tag); + + // Call the handler function + tlv_handler_cb_t *fptr = PIC(handlers[idx].func); + if (!(*fptr)(data, tlv_out)) { + PRINTF("Error while handling tag 0x%x\n", handlers[idx].tag); + return false; + } + + // Success + return true; + } + } + + PRINTF("No handler found for tag 0x%x\n", data->tag); + return false; +} + +typedef enum tlv_step_e { + TLV_TAG, + TLV_LENGTH, + TLV_VALUE, +} tlv_step_t; + +bool parse_tlv(const tlv_handler_t handlers[TLV_COUNT], + const buf_t *payload, + tlv_out_t *tlv_out, + tlv_tag_t signature_tag, + uint8_t tlv_hash[INT256_LENGTH], + uint32_t *received_tags_flags) { + tlv_step_t step = TLV_TAG; + tlv_data_t data; + size_t offset = 0; + size_t tag_start_offset; + cx_sha256_t hash_ctx; + + cx_sha256_init(&hash_ctx); + + // handle TLV payload + while (offset < payload->size) { + switch (step) { + case TLV_TAG: + tag_start_offset = offset; + if (!get_der_value_as_uint8(payload, &offset, &data.tag)) { + return false; + } + step = TLV_LENGTH; + break; + + case TLV_LENGTH: + if (!get_der_value_as_uint8(payload, &offset, &data.length)) { + return false; + } + step = TLV_VALUE; + break; + + case TLV_VALUE: + if ((offset + data.length) > payload->size) { + PRINTF("Error: value would go beyond the TLV payload!\n"); + return false; + } + data.value = &payload->bytes[offset]; + PRINTF("Handling tag 0x%02x length %d value '%.*H'\n", data.tag, data.length, data.length, data.value); + if (!handle_tlv_data(handlers, &data, received_tags_flags, tlv_out)) { + PRINTF("Handler reported an error\n"); + return false; + } + offset += data.length; + // Feed this TLV into the hash (except the signature itself) + if (data.tag != signature_tag) { + CX_ASSERT(cx_hash_no_throw((cx_hash_t *) &hash_ctx, + 0, + &payload->bytes[tag_start_offset], + (offset - tag_start_offset), + NULL, + 0)); + } + step = TLV_TAG; + break; + + default: + return false; + } + } + if (step != TLV_TAG) { + PRINTF("Error: unexpected end step %d\n", step); + return false; + } + if (offset != payload->size) { + PRINTF("Error: unexpected data at the end of the TLV payload!\n"); + return false; + } + + CX_ASSERT( + cx_hash_no_throw((cx_hash_t *) &hash_ctx, CX_LAST, NULL, 0, tlv_hash, INT256_LENGTH)); + + return true; +} diff --git a/src/tlv_utils.h b/src/tlv_utils.h new file mode 100644 index 00000000..206c3f11 --- /dev/null +++ b/src/tlv_utils.h @@ -0,0 +1,147 @@ +#pragma once + +#ifdef SDK_TLV_PARSER + +#include +#include +#include +#include + +#include "tlv.h" +#include "buf.h" + +/** Common TLV parser for Ledger embedded applications + * To use it in your application: + * - enable the SDK_TLV_PARSER flag in your Makefile + * + * - write a "tlv.h" file containing: + * - a 'tlv_out_t' structure that will be given to your handlers. Its content is up to you + * + * - a 'TLV_TAGS' X-Macro of your tags, example: + #define TLV_TAGS \ + X(MY_TAG_1, 0x01) \ + X(MY_TAG_2, 0x09) \ + X(ANOTHER_TAG, 0xcafe) + + * - include "tlv_utils.h" + * - to have the enum of the X-Macro tags expanded. + * - In this example MY_TAG_1 = 0x01, MY_TAG_2 = 0x09 and so on + * - to have the TAGS_COUNT automatically defined + * - to access the parse_tlv() top function + * - to call get_tag_flag() to know which tags were received + * - to use the get_X_from_tlv_data() helpers + */ + +#define INT256_LENGTH 32 + +// Some Xmacro dark magic to assign to each tag its value +typedef enum tlv_tag_e { + #define X(name, value) name = value, + TLV_TAGS + #undef X +} tlv_tag_t; + +// Some Xmacro dark magic to calculate TLV_COUNT +typedef enum tlv_tag_count_e { + // Define a garbage enum member for each TAG, starting at 0 + #define X(name, value) name##_AUTOCOUNT, + TLV_TAGS + #undef X + // Automatically defines TLV_COUNT as the total number of tags + TLV_COUNT +} tlv_tag_count_t; + +// The received TLV data that will be fed to each handler +typedef struct tlv_data_s { + tlv_tag_t tag; + uint8_t length; + const uint8_t *value; +} tlv_data_t; + +// The handlers prototype to give to parse_tlv() +typedef bool(tlv_handler_cb_t)(const tlv_data_t *data, tlv_out_t *tlv_extracted); + +typedef struct { + tlv_tag_t tag; + tlv_handler_cb_t *func; +} tlv_handler_t; + +/** + * Get uint from tlv data + * + * This function extracts an unsigned 32-bit integer (up to 4 bytes) from the TLV data. + * The length of the data must not exceed 4 bytes. + * + * The data is padded with leading zeros if it is less than 4 bytes, and the resulting value is converted + * to a 32-bit unsigned integer in big-endian byte order. + * + * @param[in] data The TLV data containing the value to be extracted + * @param[out] value Pointer to a uint32_t where the result will be stored + * @return True if the extraction was successful, false otherwise (invalid length or data) + */ +bool get_uint_from_tlv_data(const tlv_data_t *data, uint32_t *value); + +/** + * Get uint8_t from tlv data + * + * This function extracts a uint8_t value from the TLV data by calling `get_uint_from_tlv_data`. + * The extracted value is then checked to ensure it fits within the uint8_t range (0-255). + * + * If the value is too large (greater than `UINT8_MAX`), the extraction fails. + * + * @param[in] data The TLV data containing the value to be extracted + * @param[out] value Pointer to a uint8_t where the result will be stored + * @return True if the extraction was successful, false otherwise (either failed extraction or out-of-range value) + */ +bool get_uint8_t_from_tlv_data(const tlv_data_t *data, uint8_t *value); + +/** + * Get a cbuf_t from tlv data + * + * This function extracts a `cbuf_t` (circular buffer) from the TLV data, ensuring that the extracted + * data's length is within the specified bounds (`min_size` and `max_size`). + * + * The `cbuf_t` structure will be populated with the data's size and pointer to the actual bytes. + * + * @param[in] data The TLV data containing the value to be extracted + * @param[out] out The `cbuf_t` where the extracted value will be stored + * @param[in] min_size The minimum acceptable size for the extracted data + * @param[in] max_size The maximum acceptable size for the extracted data (0 if no upper limit) + * @return True if the extraction was successful, false otherwise (data length is outside the allowed range) + */ +bool get_cbuf_from_tlv_data(const tlv_data_t *data, cbuf_t *out, uint16_t min_size, uint16_t max_size); + +/** + * Get the receive bit flag associated with a TLV tag. + * + * Maps a TLV tag to its corresponding receive bit flag + * The flag is represented as a single bit in a 32-bit + * integer, allowing for efficient bitwise operations. + * + * @param[in] tag The TLV tag for which the flag is required. + * @return The corresponding flag value for the given TLV tag. + * If the tag is invalid, returns (uint32_t)-1. + */ +uint32_t get_tag_flag(tlv_tag_t tag); + +/** + * Parse the TLV payload + * + * Does the TLV parsing but also the SHA-256 hash of the payload. + * + * @param[in] handlers the handlers to use to parse the TLV + * @param[in] payload the raw TLV payload + * @param[out] tlv_out the parsed TLV data + * @param[in] signature_tag a tag to ignore while computing the hash + * @param[out] tlv_hash the computed hash of the entire TLV except the signature tag + * @param[out] received_tags_flags the flags of all received tags + * @return whether it was successful + */ +bool parse_tlv(const tlv_handler_t handlers[TLV_COUNT], + const buf_t *payload, + tlv_out_t *tlv_out, + tlv_tag_t signature_tag, + uint8_t tlv_hash[INT256_LENGTH], + uint32_t *received_tags_flags); + +#endif // SDK_TLV_PARSER diff --git a/src/trusted_name_descriptor_handler.c b/src/trusted_name_descriptor_handler.c index 29551d0c..9099f745 100644 --- a/src/trusted_name_descriptor_handler.c +++ b/src/trusted_name_descriptor_handler.c @@ -9,7 +9,10 @@ #include "io_helpers.h" #include "os_pki.h" +#include "pki.h" #include "cx.h" +#include "tlv.h" +#include "tlv_utils.h" #define TYPE_ADDRESS 0x06 #define TYPE_DYN_RESOLVER 0x06 @@ -17,706 +20,187 @@ #define STRUCT_TYPE_TRUSTED_NAME 0x03 #define ALGO_SECP256K1 1 -#define DER_LONG_FORM_FLAG 0x80 // 8th bit set -#define DER_FIRST_BYTE_VALUE_MASK 0x7f - -#define INT256_LENGTH 32 - -typedef enum { TLV_TAG, TLV_LENGTH, TLV_VALUE } e_tlv_step; - -// This enum needs to be ordered the same way as the e_tlv_tag one ! -typedef enum { - STRUCT_TYPE_RCV_BIT = 0, - STRUCT_VERSION_RCV_BIT, - TRUSTED_NAME_TYPE_RCV_BIT, - TRUSTED_NAME_SOURCE_RCV_BIT, - TRUSTED_NAME_RCV_BIT, - CHAIN_ID_RCV_BIT, - ADDRESS_RCV_BIT, - SOURCE_CONTRACT_RCV_BIT, - CHALLENGE_RCV_BIT, - SIGNER_KEY_ID_RCV_BIT, - SIGNER_ALGO_RCV_BIT, - SIGNATURE_RCV_BIT, -} e_tlv_rcv_bit; - -#define RCV_FLAG(a) (1 << a) - -/* -trusted_name_descriptor = tlv(TAG_STRUCTURE_TYPE, u8(TYPE_TRUSTED_NAME)) 3 bytes + - & tlv(TAG_VERSION, u8(0x02)) 3 bytes + - & tlv(TAG_TRUSTED_NAME_TYPE, 0x06) 3 bytes + - & tlv(TAG_TRUSTED_NAME_SOURCE, 0x06) 3 bytes + - & tlv(TAG_TRUSTED_NAME, trusted_name) 2 + 44 bytes + - & tlv(TAG_CHAIN_ID, chain_id) 2 + 8 bytes + - & tlv(TAG_ADDRESS, address) 2 + 44 bytes + - & tlv(TAG_SOURCE_CONTRACT, source_contract)* 2 + 44 bytes + - & tlv(TAG_CHALLENGE, challenge) 2 + 4 bytes + - & tlv(TAG_SIGNER_KEY_ID, key_id) 2 + 2 bytes + - & tlv(TAG_SIGNER_ALGORITHM, signature_algorithm) 2 + 1 byte + - & tlv(TAG_SIGNATURE, signature(~,~)) 2 + 72 bytes => 247 - -T L V -01 01 03 TAG_STRUCTURE_TYPE -02 01 02 TAG_VERSION -70 01 06 TAG_TRUSTED_NAME_TYPE -71 01 06 TAG_TRUSTED_NAME_SOURCE -20 20 276497ba0bb8659172b72edd8c66e18f561764d9c86a610a3a7e0f79c0baf9db TAG_TRUSTED_NAME -23 01 65 TAG_CHAIN_ID -22 20 606501b302e1801892f80a2979f585f8855d0f2034790a2455f744fac503d7b5 TAG_ADDRESS -73 20 c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61 TAG_SOURCE_CONTRACT -12 04 deadbeef TAG_CHALLENGE -13 01 03 TAG_SIGNER_KEY_ID -14 01 01 TAG_SIGNER_ALGORITHM -15 47 30..2e - - -01 01 03 -02 01 02 -70 01 06 -71 01 06 -20 20 276497BA0BB8659172B72EDD8C66E18F561764D9C86A610A3A7E0F79C0BAF9DB -23 01 65 -22 20 606501B302E1801892F80A2979F585F8855D0F2034790A2455F744FAC503D7B5 -73 20 C6FA7AF3BEDBAD3A3D65F36AABC97431B1BBE4C2D2F6E0E47CA60203452F5D61 -12 04 DEADBEEF -13 01 00 -14 01 01 -15 47 30..CF - -*/ - -#define TLV_BUFFER_LENGTH 255 /* >= 247, APDU max payload */ -static uint8_t tlv_buffer[TLV_BUFFER_LENGTH] = {0}; - -typedef enum { - STRUCT_TYPE = 0x01, - STRUCT_VERSION = 0x02, - TRUSTED_NAME_TYPE = 0x70, - TRUSTED_NAME_SOURCE = 0x71, - TRUSTED_NAME = 0x20, - CHAIN_ID = 0x23, - ADDRESS = 0x22, - SOURCE_CONTRACT = 0x73, - CHALLENGE = 0x12, - SIGNER_KEY_ID = 0x13, - SIGNER_ALGO = 0x14, - SIGNATURE = 0x15, -} e_tlv_tag; - -typedef enum { KEY_ID_TEST = 0x00, KEY_ID_PROD = 0x03 } e_key_id; - -typedef struct { - uint8_t *buf; - uint8_t size; - uint8_t expected_size; -} s_tlv_payload; - -typedef struct { - e_tlv_tag tag; - uint8_t length; - const uint8_t *value; -} s_tlv_data; - -typedef struct { - uint32_t rcv_flags; - bool valid; - uint8_t struct_version; - uint8_t token_account[MAX_ADDRESS_LENGTH + 1]; - uint8_t *owner; - uint8_t spl_token[MAX_ADDRESS_LENGTH + 1]; - uint64_t chain_id; - uint8_t name_type; - uint8_t name_source; -} s_trusted_name_info; - -typedef struct { - e_key_id key_id; - uint8_t input_sig_size; - const uint8_t *input_sig; - cx_sha256_t hash_ctx; -} s_sig_ctx; - -typedef bool(t_tlv_handler)(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx); - -typedef struct { - e_tlv_tag tag; - t_tlv_handler *func; - e_tlv_rcv_bit rcv_bit; -} s_tlv_handler; - -static s_tlv_payload g_tlv_payload = {0}; -static s_trusted_name_info g_trusted_name_info = {0}; - -uint8_t g_trusted_token_account_owner_pubkey[MAX_ADDRESS_LENGTH + 1] = {0}; -bool g_trusted_token_account_owner_pubkey_set = false; - -/** - * Get uint from tlv data - * - * Get an unsigned integer from variable length tlv data (up to 4 bytes) - * - * @param[in] data tlv data - * @param[out] value the returned value - * @return whether it was successful - */ -static bool get_uint_from_data(const s_tlv_data *data, uint32_t *value) { - uint8_t size_diff; - uint8_t buffer[sizeof(uint32_t)]; - - if (data->length > sizeof(buffer)) { - PRINTF("Unexpectedly long value (%u bytes) for tag 0x%x\n", data->length, data->tag); - return false; - } - size_diff = sizeof(buffer) - data->length; - memset(buffer, 0, size_diff); - memcpy(buffer + size_diff, data->value, data->length); - *value = U4BE(buffer, 0); - return true; +#define KEY_ID_TEST 0x00 +#define KEY_ID_PROD 0x03 + +// Reuse the size of the protobuf structure (-1 because TLV data is not NULL terminated) +#define MAX_ADDRESS_LENGTH (sizeof(G_swap_ctx.swap_transaction.payout_address) - 1) + +static bool all_received_v2(uint32_t rcv_flags) { + uint32_t required_flags = (get_tag_flag(STRUCT_TYPE) \ + | get_tag_flag(STRUCT_VERSION) \ + | get_tag_flag(TRUSTED_NAME_TYPE) \ + | get_tag_flag(TRUSTED_NAME_SOURCE) \ + | get_tag_flag(TRUSTED_NAME) \ + | get_tag_flag(CHAIN_ID) \ + | get_tag_flag(ADDRESS) \ + | get_tag_flag(SOURCE_CONTRACT) \ + | get_tag_flag(CHALLENGE) \ + | get_tag_flag(SIGNER_KEY_ID) \ + | get_tag_flag(SIGNER_ALGO) \ + | get_tag_flag(SIGNATURE)); + return ((rcv_flags & required_flags) == required_flags); } -/** - * Handler for tag \ref STRUCT_TYPE - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_struct_type(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - - (void) trusted_name_info; - (void) sig_ctx; - if (!get_uint_from_data(data, &value)) { - return false; - } - return (value == STRUCT_TYPE_TRUSTED_NAME); +static bool handle_struct_type(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->struct_type); } -/** - * Handler for tag \ref STRUCT_VERSION - * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_struct_version(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - - (void) sig_ctx; - if (!get_uint_from_data(data, &value) || (value > UINT8_MAX)) { - return false; - } - trusted_name_info->struct_version = value; - return true; +static bool handle_struct_version(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->struct_version); } -/** - * Handler for tag \ref CHALLENGE - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_challenge(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - (void) trusted_name_info; - (void) sig_ctx; - - if (!get_uint_from_data(data, &value)) { - return false; - } - return (value == get_challenge()); +static bool handle_challenge(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint_from_tlv_data(data, &trusted_name_info->challenge); } -/** - * Handler for tag \ref SIGNER_KEY_ID - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[out] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_sign_key_id(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - (void) trusted_name_info; - - if (!get_uint_from_data(data, &value) || (value > UINT8_MAX)) { - return false; - } - sig_ctx->key_id = value; - return true; +static bool handle_sign_key_id(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->key_id); } -/** - * Handler for tag \ref SIGNER_ALGO - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_sign_algo(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - - (void) trusted_name_info; - (void) sig_ctx; - if (!get_uint_from_data(data, &value)) { - return false; - } - return (value == ALGO_SECP256K1); +static bool handle_sign_algo(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->sig_algorithm); } -/** - * Handler for tag \ref SIGNATURE - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[out] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_signature(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - (void) trusted_name_info; - sig_ctx->input_sig_size = data->length; - sig_ctx->input_sig = data->value; - return true; +static bool handle_signature(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_cbuf_from_tlv_data(data, &trusted_name_info->input_sig, 1, 0); } -/** - * Handler for tag \ref SOURCE_CONTRACT - * - * @param[in] data the tlv data - * @param[] trusted_name_info the trusted name information - * @param[out] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_source_contract(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - (void) sig_ctx; - if (data->length > MAX_ADDRESS_LENGTH) { - PRINTF("SPL Token address too long! (%u)\n", data->length); - return false; - } - - memcpy(trusted_name_info->spl_token, data->value, data->length); - - trusted_name_info->spl_token[data->length] = '\0'; - return true; +static bool handle_source_contract(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_cbuf_from_tlv_data(data, &trusted_name_info->spl_token, 0, MAX_ADDRESS_LENGTH); } -/** - * Tests if the given account name character is valid (in our subset of allowed characters) - * - * @param[in] c given character - * @return whether the character is valid - */ -/*static bool is_valid_account_character(char c) { - if (isalpha((int) c)) { - if (!islower((int) c)) { - return false; - } - } else if (!isdigit((int) c)) { - switch (c) { - case '.': - case '-': - case '_': - break; - default: - return false; - } - } - return true; -}*/ - -/** - * Handler for tag \ref TRUSTED_NAME - * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_trusted_name(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - (void) sig_ctx; - if (data->length > MAX_ADDRESS_LENGTH) { - PRINTF("Token Account address too long! (%u)\n", data->length); - return false; - } - - memcpy(trusted_name_info->token_account, data->value, data->length); +static bool handle_trusted_name(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_cbuf_from_tlv_data(data, &trusted_name_info->token_account, 1, MAX_ADDRESS_LENGTH); +} - trusted_name_info->token_account[data->length] = '\0'; - return true; +static bool handle_address(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_cbuf_from_tlv_data(data, &trusted_name_info->owner, 1, MAX_ADDRESS_LENGTH); } -/** - * Handler for tag \ref ADDRESS - * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_address(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - (void) sig_ctx; - if (data->length > MAX_ADDRESS_LENGTH) { - PRINTF("Address too long! (%u)\n", data->length); - return false; - } - memcpy(trusted_name_info->owner, data->value, data->length); - trusted_name_info->owner[data->length] = '\0'; - return true; +static bool handle_trusted_name_type(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->name_type); } -/** - * Handler for tag \ref CHAIN_ID - * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_chain_id(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - (void) sig_ctx; - bool res = false; +static bool handle_trusted_name_source(const tlv_data_t *data, tlv_out_t *trusted_name_info) { + return get_uint8_t_from_tlv_data(data, &trusted_name_info->name_source); +} +static bool handle_chain_id(const tlv_data_t *data, tlv_out_t *trusted_name_info) { switch (data->length) { - case 1: { + case 1: trusted_name_info->chain_id = data->value[0]; - res = true; - break; - } - case 2: { + return true; + case 2: trusted_name_info->chain_id = (data->value[0] << 8) | data->value[1]; - res = true; - break; - } + return true; default: PRINTF("Error while parsing chain ID: length = %d\n", data->length); + return false; } - return res; } /** - * Handler for tag \ref TRUSTED_NAME_TYPE + * Verify the validity of the received trusted struct * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful + * @param[in] trusted_name_info the trusted name information + * @return whether the struct is valid */ -static bool handle_trusted_name_type(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - - (void) trusted_name_info; - (void) sig_ctx; - if (!get_uint_from_data(data, &value) || (value > UINT8_MAX)) { - return false; - } +static swap_error_e verify_struct(const tlv_out_t *trusted_name_info, uint32_t received_tags_flags) { - if (value != TYPE_ADDRESS) { - PRINTF("Error: unsupported trusted name type (%u)!\n", value); - return false; - } - trusted_name_info->name_type = value; - return true; -} - -/** - * Handler for tag \ref TRUSTED_NAME_SOURCE - * - * @param[in] data the tlv data - * @param[out] trusted_name_info the trusted name information - * @param[] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_trusted_name_source(const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - uint32_t value; - - (void) trusted_name_info; - (void) sig_ctx; - if (!get_uint_from_data(data, &value) || (value > UINT8_MAX)) { - return false; + if (!(get_tag_flag(STRUCT_TYPE) & received_tags_flags)) { + PRINTF("Error: no struct type specified!\n"); + return MISSING_TLV_CONTENT; } - - if (value != TYPE_DYN_RESOLVER) { - PRINTF("Error: unsupported trusted name source (%u)!\n", value); - return false; + if (!(get_tag_flag(STRUCT_VERSION) & received_tags_flags)) { + PRINTF("Error: no struct version specified!\n"); + return MISSING_TLV_CONTENT; } - trusted_name_info->name_source = value; - return true; -} - -static int check_signature_with_pubkey(uint8_t *buffer, - const uint8_t bufLen, - const uint8_t keyUsageExp, - uint8_t *signature, - const uint8_t sigLen) { - cx_err_t error = CX_INTERNAL_ERROR; - uint8_t key_usage = 0; - size_t trusted_name_len = 0; - uint8_t trusted_name[CERTIFICATE_TRUSTED_NAME_MAXLEN] = {0}; - cx_ecfp_384_public_key_t public_key = {0}; - - PRINTF( - "=======================================================================================\n"); - - error = os_pki_get_info(&key_usage, trusted_name, &trusted_name_len, &public_key); - if ((error == 0) && (key_usage == keyUsageExp)) { - PRINTF("[%s] Certificate '%s' loaded for usage 0x%x \n", - tag, - trusted_name, - key_usage); - - // Checking the signature with PKI - if (!os_pki_verify(buffer, bufLen, signature, sigLen)) { - PRINTF("%s: Invalid signature\n", tag); - error = CX_INTERNAL_ERROR; - goto end; - } - } else { - PRINTF( - "********** Issue when loading PKI certificate, cannot check signature " - "**********\n"); - goto end; - } - error = CX_OK; -end: - return error; -} + uint32_t expected_challenge = get_challenge(); -/** - * Verify the signature context - * - * Verify the SHA-256 hash of the payload against the public key - * - * @param[in] sig_ctx the signature context - * @return whether it was successful - */ -static bool verify_signature(const s_sig_ctx *sig_ctx) { - uint8_t hash[INT256_LENGTH]; - cx_err_t error = CX_INTERNAL_ERROR; - -#ifdef HAVE_TRUSTED_NAME_TEST - e_key_id valid_key_id = KEY_ID_TEST; +#ifdef TRUSTED_NAME_TEST_KEY + uint8_t valid_key_id = KEY_ID_TEST; #else - e_key_id valid_key_id = KEY_ID_PROD; + uint8_t valid_key_id = KEY_ID_PROD; #endif - bool ret_code = false; - - if (sig_ctx->key_id != valid_key_id) { - PRINTF("Error: Unknown metadata key ID %u\n", sig_ctx->key_id); - return false; - } - - CX_CHECK( - cx_hash_no_throw((cx_hash_t *) &sig_ctx->hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH)); - - CX_CHECK(check_signature_with_pubkey(hash, - sizeof(hash), - CERTIFICATE_PUBLIC_KEY_USAGE_TRUSTED_NAME, - (uint8_t *) (sig_ctx->input_sig), - sig_ctx->input_sig_size)); - - ret_code = true; -end: - return ret_code; -} - -/** - * Calls the proper handler for the given TLV data - * - * Checks if there is a proper handler function for the given TLV tag and then calls it - * - * @param[in] handlers list of tag / handler function pairs - * @param[in] handler_count number of handlers - * @param[in] data the TLV data - * @param[out] trusted_name_info the trusted name information - * @param[out] sig_ctx the signature context - * @return whether it was successful - */ -static bool handle_tlv_data(s_tlv_handler *handlers, - int handler_count, - const s_tlv_data *data, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - t_tlv_handler *fptr; - - // check if a handler exists for this tag - for (int idx = 0; idx < handler_count; ++idx) { - if (handlers[idx].tag == data->tag) { - trusted_name_info->rcv_flags |= RCV_FLAG(handlers[idx].rcv_bit); - fptr = PIC(handlers[idx].func); - if (!(*fptr)(data, trusted_name_info, sig_ctx)) { - PRINTF("Error while handling tag 0x%x\n", handlers[idx].tag); - return false; - } - break; - } - } - return true; -} - -/** - * Verify the validity of the received trusted struct - * - * @param[in] trusted_name_info the trusted name information - * @return whether the struct is valid - */ -static bool verify_struct(const s_trusted_name_info *trusted_name_info) { - uint32_t required_flags; - - if (!(RCV_FLAG(STRUCT_VERSION_RCV_BIT) & trusted_name_info->rcv_flags)) { - PRINTF("Error: no struct version specified!\n"); - return false; - } - required_flags = RCV_FLAG(STRUCT_TYPE_RCV_BIT) | RCV_FLAG(STRUCT_VERSION_RCV_BIT) | - RCV_FLAG(TRUSTED_NAME_TYPE_RCV_BIT) | RCV_FLAG(TRUSTED_NAME_SOURCE_RCV_BIT) | - RCV_FLAG(TRUSTED_NAME_RCV_BIT) | RCV_FLAG(CHAIN_ID_RCV_BIT) | - RCV_FLAG(ADDRESS_RCV_BIT) | RCV_FLAG(SOURCE_CONTRACT_RCV_BIT) | - RCV_FLAG(CHALLENGE_RCV_BIT) | RCV_FLAG(SIGNER_KEY_ID_RCV_BIT) | - RCV_FLAG(SIGNER_ALGO_RCV_BIT) | RCV_FLAG(SIGNATURE_RCV_BIT); switch (trusted_name_info->struct_version) { case 2: - if ((trusted_name_info->rcv_flags & required_flags) != required_flags) { + if (!all_received_v2(received_tags_flags)) { PRINTF("Error: missing required fields in struct version 2\n"); - return false; + return MISSING_TLV_CONTENT; + } + if (trusted_name_info->challenge != expected_challenge) { + // No risk printing it as DEBUG cannot be used in prod + PRINTF("Error: wrong challenge, received %u expected %u\n", trusted_name_info->challenge, expected_challenge); + return WRONG_CHALLENGE; + } + if (trusted_name_info->struct_type != STRUCT_TYPE_TRUSTED_NAME) { + PRINTF("Error: unexpected struct type %d\n", trusted_name_info->struct_type); + return WRONG_TLV_CONTENT; } - switch (trusted_name_info->name_type) { - case TYPE_ADDRESS: - if (trusted_name_info->name_source != TYPE_DYN_RESOLVER) { - PRINTF("Error: unsupported trusted name source (%u)!\n", - trusted_name_info->name_source); - return false; - } - break; - default: - PRINTF("Error: unsupported trusted name type (%u)!\n", - trusted_name_info->name_type); - return false; + if (trusted_name_info->name_type != TYPE_ADDRESS) { + PRINTF("Error: unsupported name type %d\n", trusted_name_info->name_type); + return WRONG_TLV_CONTENT; + } + if (trusted_name_info->name_source != TYPE_DYN_RESOLVER) { + PRINTF("Error: unsupported name source %d\n", trusted_name_info->name_source); + return WRONG_TLV_CONTENT; + } + if (trusted_name_info->sig_algorithm != ALGO_SECP256K1) { + PRINTF("Error: unsupported sig algorithm %d\n", trusted_name_info->sig_algorithm); + return WRONG_TLV_CONTENT; + } + if (trusted_name_info->key_id != valid_key_id) { + PRINTF("Error: wrong metadata key ID %u\n", trusted_name_info->key_id); + return WRONG_TLV_KEY_ID; } break; default: - PRINTF("Error: unsupported trusted name struct version (%u) !\n", - trusted_name_info->struct_version); - return false; + PRINTF("Error: unsupported struct version %d\n", trusted_name_info->struct_version); + return WRONG_TLV_CONTENT; } - return true; + return SUCCESS; } -/** Parse DER-encoded value - * - * Parses a DER-encoded value (up to 4 bytes long) - * https://en.wikipedia.org/wiki/X.690 - * - * @param[in] payload the TLV payload - * @param[in,out] offset the payload offset - * @param[out] value the parsed value - * @return whether it was successful - */ -static bool parse_der_value(const s_tlv_payload *payload, size_t *offset, uint32_t *value) { - bool ret = false; - uint8_t byte_length; - uint8_t buf[sizeof(*value)]; - - if (value != NULL) { - if (payload->buf[*offset] & DER_LONG_FORM_FLAG) { // long form - byte_length = payload->buf[*offset] & DER_FIRST_BYTE_VALUE_MASK; - *offset += 1; - if ((*offset + byte_length) > payload->size) { - PRINTF("TLV payload too small for DER encoded value\n"); +static bool apply_trusted_name(char *current_address, size_t current_address_size, const cbuf_t *trusted_name, const cbuf_t *owner) { + uint8_t current_length = strlen(current_address); + if (trusted_name->size == current_length) { + PRINTF("Checking against current address %.*H\n", current_length, current_address); + if (memcmp(trusted_name->bytes, current_address, current_length) == 0) { + PRINTF("Current address matches, replacing it\n"); + if (owner->size < current_address_size) { + explicit_bzero(current_address, current_address_size); + memcpy(current_address, owner->bytes, owner->size); + return true; } else { - if (byte_length > sizeof(buf) || byte_length == 0) { - PRINTF("Unexpectedly long DER-encoded value (%u bytes)\n", byte_length); - } else { - memset(buf, 0, (sizeof(buf) - byte_length)); - memcpy(buf + (sizeof(buf) - byte_length), &payload->buf[*offset], byte_length); - *value = U4BE(buf, 0); - *offset += byte_length; - ret = true; - } + // Should never happen thanks to the parser but let's double check + PRINTF("Error, owner address does not fit\n"); } - } else { // short form - *value = payload->buf[*offset]; - *offset += 1; - ret = true; } } - return ret; + return false; } -/** - * Get DER-encoded value as an uint8 - * - * Parses the value and checks if it fits in the given \ref uint8_t value - * - * @param[in] payload the TLV payload - * @param[in,out] offset - * @param[out] value the parsed value - * @return whether it was successful - */ -static bool get_der_value_as_uint8(const s_tlv_payload *payload, size_t *offset, uint8_t *value) { - bool ret = false; - uint32_t tmp_value; - - if (value != NULL) { - if (!parse_der_value(payload, offset, &tmp_value)) { - } else { - if (tmp_value <= UINT8_MAX) { - *value = tmp_value; - ret = true; - } else { - PRINTF("TLV DER-encoded value larger than 8 bits\n"); - } - } - } - return ret; -} +static int trusted_name_descriptor_handler_internal(const command_t *cmd) { + PRINTF("Received chunk of trusted info, length = %d\n", cmd->data.size); + PRINTF("Content = %.*H\n", cmd->data.size, cmd->data.bytes); + swap_error_e ret; -/** - * Parse the TLV payload - * - * Does the TLV parsing but also the SHA-256 hash of the payload. - * - * @param[in] payload the raw TLV payload - * @param[out] trusted_name_info the trusted name information - * @param[out] sig_ctx the signature context - * @return whether it was successful - */ -static bool parse_tlv(const s_tlv_payload *payload, - s_trusted_name_info *trusted_name_info, - s_sig_ctx *sig_ctx) { - s_tlv_handler handlers[] = { + // Main structure that will received the parsed TLV data + tlv_out_t trusted_name_info; + memset(&trusted_name_info, 0, sizeof(trusted_name_info)); + + // Will be filled by the parser with the flags of received tags + uint32_t received_tags_flags = 0; + + // The parser will fill it with the hash of the whole TLV payload (except SIGN tag) + uint8_t tlv_hash[INT256_LENGTH]; + memset(&tlv_hash, 0, sizeof(tlv_hash)); + + // Mapping of tags to handler functions. Given ot the parser + tlv_handler_t handlers[TLV_COUNT] = { {.tag = STRUCT_TYPE, .func = &handle_struct_type}, {.tag = STRUCT_VERSION, .func = &handle_struct_version}, {.tag = TRUSTED_NAME_TYPE, .func = &handle_trusted_name_type}, @@ -730,147 +214,60 @@ static bool parse_tlv(const s_tlv_payload *payload, {.tag = SIGNER_ALGO, .func = &handle_sign_algo}, {.tag = SIGNATURE, .func = &handle_signature}, }; - e_tlv_step step = TLV_TAG; - s_tlv_data data; - size_t offset = 0; - size_t tag_start_off; - - for (size_t i = 0; i < ARRAYLEN(handlers); ++i) handlers[i].rcv_bit = i; - cx_sha256_init(&sig_ctx->hash_ctx); - // handle TLV payload - while (offset < payload->size) { - switch (step) { - case TLV_TAG: - tag_start_off = offset; - if (!get_der_value_as_uint8(payload, &offset, &data.tag)) { - return false; - } - step = TLV_LENGTH; - break; - - case TLV_LENGTH: - if (!get_der_value_as_uint8(payload, &offset, &data.length)) { - return false; - } - step = TLV_VALUE; - break; - - case TLV_VALUE: - if ((offset + data.length) > payload->size) { - PRINTF("Error: value would go beyond the TLV payload!\n"); - return false; - } - data.value = &payload->buf[offset]; - if (!handle_tlv_data(handlers, - (sizeof(handlers) / sizeof(handlers[0])), - &data, - trusted_name_info, - sig_ctx)) { - return false; - } - offset += data.length; - if (data.tag != SIGNATURE) { // the signature wasn't computed on itself - CX_ASSERT(cx_hash_no_throw((cx_hash_t *) &sig_ctx->hash_ctx, - 0, - &payload->buf[tag_start_off], - (offset - tag_start_off), - NULL, - 0)); - } - step = TLV_TAG; - break; - - default: - return false; - } - } - if (step != TLV_TAG) { - PRINTF("Error: unexpected data at the end of the TLV payload!\n"); - return false; - } - return verify_struct(trusted_name_info); -} -/** - * Deallocate and unassign TLV payload - * - * @param[in] payload payload structure - */ -static void free_payload(s_tlv_payload *payload) { - memset(tlv_buffer, 0, sizeof(tlv_buffer)); - memset(payload, 0, sizeof(*payload)); -} - -static bool init_tlv_payload(uint8_t length, s_tlv_payload *payload) { - // check if no payload is already in memory - if (payload->buf != NULL) { - free_payload(payload); - return false; + // Call the parser to extract the raw TLV payload into our parsed structure + if (!parse_tlv(handlers, &cmd->data, &trusted_name_info, SIGNATURE, tlv_hash, &received_tags_flags)) { + PRINTF("Failed to parse tlv payload\n"); + return reply_error(WRONG_TLV_FORMAT); } - payload->buf = tlv_buffer; - payload->expected_size = length; - - return true; -} - -/** - * Handle provide trusted info APDU - * - * @param[in] is_first_chunk first APDU instruction parameter - * @param[in] data APDU payload - * @param[in] length payload size - */ -int trusted_name_descriptor_handler(const command_t *cmd) { - s_sig_ctx sig_ctx; - - uint8_t *data = cmd->data.bytes; - uint8_t data_length = cmd->data.size; - - PRINTF("Received chunk of trusted info, length = %d\n", data_length); - if (!init_tlv_payload(data_length, &g_tlv_payload)) { - free_payload(&g_tlv_payload); - PRINTF("Error while initializing TLV payload\n"); - return reply_error(INTERNAL_ERROR); + // Verify that the fields received are correct in our context + ret = verify_struct(&trusted_name_info, received_tags_flags); + if (ret != SUCCESS) { + PRINTF("Failed to verify tlv payload\n"); + return reply_error(ret); } - PRINTF("Expected size of trusted info: %d\n", g_tlv_payload.expected_size); + // Verify that the TLV is properly signed using the PKI + if (check_signature_with_pki(tlv_hash, + INT256_LENGTH, + CERTIFICATE_PUBLIC_KEY_USAGE_TRUSTED_NAME, + &trusted_name_info.input_sig) != CX_OK) { + PRINTF("Failed to verify signature of trusted name info\n"); + return reply_error(WRONG_TLV_SIGNATURE); + } - if ((g_tlv_payload.size + data_length) > g_tlv_payload.expected_size) { - free_payload(&g_tlv_payload); - PRINTF("TLV payload size mismatch!\n"); + // Should never happen thanks to apdu_parser check but let's check again anyway + if (G_swap_ctx.subcommand != SWAP && G_swap_ctx.subcommand != SWAP_NG) { + PRINTF("Trusted name descriptor is only for SWAP based flows\n"); return reply_error(INTERNAL_ERROR); } - // feed into tlv payload - memcpy(g_tlv_payload.buf + g_tlv_payload.size, data, data_length); - g_tlv_payload.size += data_length; - - PRINTF("Received %d bytes of trusted info\n", g_tlv_payload.size); - - // everything has been received - if (g_tlv_payload.size == g_tlv_payload.expected_size) { - g_trusted_name_info.owner = g_trusted_token_account_owner_pubkey; - g_trusted_token_account_owner_pubkey_set = true; - if (!parse_tlv(&g_tlv_payload, &g_trusted_name_info, &sig_ctx) || - !verify_signature(&sig_ctx)) { - free_payload(&g_tlv_payload); - roll_challenge(); // prevent brute-force guesses - g_trusted_name_info.rcv_flags = 0; - memset(g_trusted_token_account_owner_pubkey, - 0, - sizeof(g_trusted_token_account_owner_pubkey)); - g_trusted_token_account_owner_pubkey_set = false; - return reply_error(INTERNAL_ERROR); - } - PRINTF("Token account : %s owned by %s\n", - g_trusted_name_info.token_account, - g_trusted_token_account_owner_pubkey); - - free_payload(&g_tlv_payload); - roll_challenge(); // prevent replays - return reply_success(); - } - return reply_error(INTERNAL_ERROR); + PRINTF("Token account : %.*H owned by %.*H\n", + trusted_name_info.token_account.size, + trusted_name_info.token_account.bytes, + trusted_name_info.owner.size, + trusted_name_info.owner.bytes); + + PRINTF("Checking against PAYOUT address\n"); + apply_trusted_name(G_swap_ctx.swap_transaction.payout_address, + sizeof(G_swap_ctx.swap_transaction.payout_address), + &trusted_name_info.token_account, + &trusted_name_info.owner); + + PRINTF("Checking against REFUND address\n"); + apply_trusted_name(G_swap_ctx.swap_transaction.refund_address, + sizeof(G_swap_ctx.swap_transaction.refund_address), + &trusted_name_info.token_account, + &trusted_name_info.owner); + + return reply_success(); } +// Wrapper around trusted_name_descriptor_handler_internal to handle the challenge reroll +int trusted_name_descriptor_handler(const command_t *cmd) { + int ret = trusted_name_descriptor_handler_internal(cmd); + // prevent brute-force guesses + roll_challenge(); + return ret; +} diff --git a/src/trusted_name_descriptor_handler.h b/src/trusted_name_descriptor_handler.h index d10b5524..1d20f035 100644 --- a/src/trusted_name_descriptor_handler.h +++ b/src/trusted_name_descriptor_handler.h @@ -3,8 +3,3 @@ #include "commands.h" int trusted_name_descriptor_handler(const command_t *cmd); - -#define MAX_ADDRESS_LENGTH 44 - -extern uint8_t g_trusted_token_account_owner_pubkey[MAX_ADDRESS_LENGTH + 1]; -extern bool g_trusted_token_account_owner_pubkey_set; \ No newline at end of file diff --git a/test/python/apps/cal.py b/test/python/apps/cal.py index d76300a1..f057f10d 100644 --- a/test/python/apps/cal.py +++ b/test/python/apps/cal.py @@ -14,7 +14,7 @@ from .litecoin import LTC_PACKED_DERIVATION_PATH, LTC_CONF from .bitcoin import BTC_PACKED_DERIVATION_PATH, BTC_CONF from .stellar import XLM_PACKED_DERIVATION_PATH, XLM_CONF -from .solana_utils import SOL_PACKED_DERIVATION_PATH, SOL_CONF, JUP_CONF, JUP_PACKED_DERIVATION_PATH +from .solana_utils import SOL_PACKED_DERIVATION_PATH, SOL_CONF from .xrp import XRP_PACKED_DERIVATION_PATH, XRP_CONF from .tezos import XTZ_PACKED_DERIVATION_PATH, XTZ_CONF from .polkadot import DOT_PACKED_DERIVATION_PATH, DOT_CONF @@ -54,7 +54,6 @@ def get_conf_for_ticker(self, overload_signer: Optional[SigningAuthority]=None) USDC_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="USDC", conf=TRX_USDC_CONF, packed_derivation_path=TRX_PACKED_DERIVATION_PATH) TUSD_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="TUSD", conf=TRX_TUSD_CONF, packed_derivation_path=TRX_PACKED_DERIVATION_PATH) USDD_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="USDD", conf=TRX_USDD_CONF, packed_derivation_path=TRX_PACKED_DERIVATION_PATH) -JUP_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="JUP", conf=JUP_CONF, packed_derivation_path=JUP_PACKED_DERIVATION_PATH) # Helper that can be called from outside if we want to generate errors easily diff --git a/test/python/apps/exchange.py b/test/python/apps/exchange.py index 279da076..f354b241 100644 --- a/test/python/apps/exchange.py +++ b/test/python/apps/exchange.py @@ -3,15 +3,23 @@ from enum import IntEnum from ragger.backend.interface import BackendInterface, RAPDU +from ragger.firmware import Firmware +from ragger.utils import prefix_with_len from ..utils import handle_lib_call_start_or_stop, int_to_minimally_sized_bytes, prefix_with_len_custom, get_version_from_makefile from .exchange_transaction_builder import SubCommand +from .solana_keychain import Key, sign_data + +from .solana_tlv import FieldTag, format_tlv MAX_CHUNK_SIZE = 255 P2_EXTEND = 0x01 << 4 P2_MORE = 0x02 << 4 +KEY_ID_TEST = 0x00 +KEY_ID_PROD = 0x03 + class Command(IntEnum): GET_VERSION = 0x02 START_NEW_TRANSACTION = 0x03 @@ -19,6 +27,8 @@ class Command(IntEnum): CHECK_PARTNER = 0x05 PROCESS_TRANSACTION_RESPONSE = 0x06 CHECK_TRANSACTION_SIGNATURE = 0x07 + GET_CHALLENGE = 0x10 + SEND_TRUSTED_NAME_DESCRIPTOR = 0x11 CHECK_ASSET_IN_LEGACY_AND_DISPLAY = 0x08 CHECK_ASSET_IN_AND_DISPLAY = 0x0B CHECK_ASSET_IN_NO_DISPLAY = 0x0D @@ -49,6 +59,12 @@ class Errors(IntEnum): AMOUNT_FORMATTING_FAILED = 0x6A8B APPLICATION_NOT_INSTALLED = 0x6A8C WRONG_EXTRA_ID_OR_EXTRA_DATA = 0x6A8D + WRONG_CHALLENGE = 0x6A8E + WRONG_TLV_CONTENT = 0x6A8F + MISSING_TLV_CONTENT = 0x6A90 + WRONG_TLV_FORMAT = 0x6A91 + WRONG_TLV_KEY_ID = 0x6A92 + WRONG_TLV_SIGNATURE = 0x6A93 CLASS_NOT_SUPPORTED = 0x6E00 MALFORMED_APDU = 0x6E01 INVALID_DATA_LENGTH = 0x6E02 @@ -64,6 +80,21 @@ class PayinExtraDataID(IntEnum): EXCHANGE_CLASS = 0xE0 + +class PKIClient: + _CLA: int = 0xB0 + _INS: int = 0x06 + + def __init__(self, client: BackendInterface) -> None: + self._client = client + + def send_certificate(self, payload: bytes) -> RAPDU: + return self._client.exchange(cla=self._CLA, + ins=self._INS, + p1=0x04, # PubKeyUsage = 0x04 + p2=0x00, + data=payload) + class ExchangeClient: CLA = EXCHANGE_CLASS def __init__(self, @@ -80,6 +111,7 @@ def __init__(self, self._client = client self._rate = rate self._subcommand = subcommand + self._pki_client = PKIClient(self._client) @property def rate(self) -> Rate: @@ -99,6 +131,20 @@ def _exchange_async(self, ins: int, payload: bytes = b"") -> Generator[RAPDU, No p2=self.subcommand, data=payload) as response: yield response + def _exchange_split(self, ins: int, payload: bytes) -> RAPDU: + payload_split = [payload[x:x + MAX_CHUNK_SIZE] for x in range(0, len(payload), MAX_CHUNK_SIZE)] + for i, p in enumerate(payload_split): + p2 = self.subcommand + # Send all chunks with P2_MORE except for the last chunk + if i != len(payload_split) - 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, ins=ins, p1=self.rate, p2=p2, data=p) + + return rapdu + def get_version(self) -> RAPDU: return self._exchange(Command.GET_VERSION) @@ -115,24 +161,80 @@ def check_partner_key(self, signed_credentials: bytes) -> RAPDU: def process_transaction(self, transaction: bytes) -> RAPDU: if self.subcommand == SubCommand.SWAP or self.subcommand == SubCommand.FUND or self.subcommand == SubCommand.SELL: return self._exchange(Command.PROCESS_TRANSACTION_RESPONSE, payload=transaction) - else: - payload_split = [transaction[x:x + MAX_CHUNK_SIZE] for x in range(0, len(transaction), MAX_CHUNK_SIZE)] - for i, p in enumerate(payload_split): - p2 = self.subcommand - # Send all chunks with P2_MORE except for the last chunk - if i != len(payload_split) - 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 + return self._exchange_split(Command.PROCESS_TRANSACTION_RESPONSE, payload=transaction) def check_transaction_signature(self, encoded_transaction: bytes) -> RAPDU: return self._exchange(Command.CHECK_TRANSACTION_SIGNATURE, payload=encoded_transaction) + def get_challenge(self) -> RAPDU: + return self._exchange(Command.GET_CHALLENGE) + + def send_trusted_name_descriptor(self, + structure_type: Optional[int] = 3, + version: Optional[int] = 2, + trusted_name_type: Optional[int] = 0x06, + trusted_name_source: Optional[int] = 0x06, + trusted_name: Optional[bytes] = b"Whatever", + chain_id: Optional[int] = 0, + address: Optional[bytes] = b"Whatever", + trusted_name_source_contract: Optional[int] = b"", + challenge: Optional[bytes] = bytes.fromhex("01010101"), + signer_key_id: Optional[int] = 0, # test key + signer_algo: Optional[int] = 1, # secp256k1 + skip_signature_field: bool = False, + fake_signature_field: bool = False) -> RAPDU: + + # send PKI certificate + if self._pki_client is None: + print(f"Ledger-PKI Not supported on '{self._client.firmware.name}'") + else: + # pylint: disable=line-too-long + if self._client.firmware == Firmware.NANOSP: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F6534010135010315473045022100D494B106E217B46BB90BF20A4E9285529C4C8382D9B80FF462F74942579785F802202D68D0F85CD7CA36BDF351FD41332F310E93163BD175F6A92446C14A3329CC8B" # noqa: E501 + elif self._client.firmware == Firmware.NANOX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F653401013501021546304402207FCD665B94B43A6E838E8CD68BE52403D38A7E6A98E2CE291AB1C5D24A41101D02207AB1863E5CB127D9E8A680AC63FF2F2CBEA79CE76652A72832EF154BF1AD6477" # noqa: E501 + elif self._client.firmware == Firmware.STAX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F65340101350104154730450221008F8FB0117C8D51F0D13A77680C18CA98B4B317C3D6C67F23BF9198410BEDF1A1022023B1052CA43E86E2411831990C64B1E027D85E142AD39F480948E3EF9517E55E" # noqa: E501 + elif self._client.firmware == Firmware.FLEX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F6534010135010515473045022100CEF28780DCAFA3A485D83406D519F9AC12FD9B9C3AA7AE798896013F07DD178D022020F01B1AB1D2AAEDA70357F615EAC55E17FE94EC36DF9DE850CEFACBC98D16C8" # noqa: E501 + # pylint: enable=line-too-long + + self._pki_client.send_certificate(bytes.fromhex(cert_apdu)) + + payload = b"" + if structure_type is not None: + payload += format_tlv(FieldTag.TAG_STRUCTURE_TYPE, structure_type) + if version is not None: + payload += format_tlv(FieldTag.TAG_VERSION, version) + if trusted_name_type is not None: + payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_TYPE, trusted_name_type) + if trusted_name_source is not None: + payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_SOURCE, trusted_name_source) + if trusted_name is not None: + payload += format_tlv(FieldTag.TAG_TRUSTED_NAME, trusted_name) + if chain_id is not None: + payload += format_tlv(FieldTag.TAG_CHAIN_ID, chain_id) + if address is not None: + payload += format_tlv(FieldTag.TAG_ADDRESS, address) + if trusted_name_source_contract is not None: + payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_SOURCE_CONTRACT, trusted_name_source_contract) + if challenge is not None: + payload += format_tlv(FieldTag.TAG_CHALLENGE, challenge) + if signer_key_id is not None: + payload += format_tlv(FieldTag.TAG_SIGNER_KEY_ID, signer_key_id) + if signer_algo is not None: + payload += format_tlv(FieldTag.TAG_SIGNER_ALGO, signer_algo) + if not skip_signature_field: + if fake_signature_field: + payload += format_tlv(FieldTag.TAG_DER_SIGNATURE, + sign_data(Key.TRUSTED_NAME, payload + b"0")) + else: + payload += format_tlv(FieldTag.TAG_DER_SIGNATURE, + sign_data(Key.TRUSTED_NAME, payload)) + + return self._exchange_split(Command.SEND_TRUSTED_NAME_DESCRIPTOR, payload=payload) + def check_payout_address(self, payout_configuration: bytes) -> RAPDU: return self._exchange(Command.CHECK_PAYOUT_ADDRESS, payload=payout_configuration) diff --git a/test/python/apps/keychain/trusted_name.pem b/test/python/apps/keychain/trusted_name.pem new file mode 100644 index 00000000..9c0384b3 --- /dev/null +++ b/test/python/apps/keychain/trusted_name.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQACg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIHfwyko1dEHTTQ7es7EUy2ajZo1IRRcEC8/9b+MDOzUaoAcGBSuBBAAK +oUQDQgAEuR++wXPjukpxTgFOvIJ7b4man6f0rHac3ihDF6APT2UPCfCapP9aMXYC +Vf5d/IETKbO1C+mRlPyhFhnmXy7f6g== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/test/python/apps/solana.py b/test/python/apps/solana.py index e5afaec2..69bd96bc 100644 --- a/test/python/apps/solana.py +++ b/test/python/apps/solana.py @@ -88,10 +88,11 @@ def get_public_key(self, derivation_path: bytes) -> bytes: def split_and_prefix_message(self, derivation_path : bytes, message: bytes) -> List[bytes]: assert len(message) <= 65535, "Message to send is too long" header: bytes = _extend_and_serialize_multiple_derivations_paths([derivation_path]) - max_size = MAX_CHUNK_SIZE + # Check to see if this data needs to be split up and sent in chunks. + max_size = MAX_CHUNK_SIZE - len(header) message_splited = [message[x:x + max_size] for x in range(0, len(message), max_size)] - # The first chunk is the header, then all chunks with max size - return [header] + message_splited + # Add the header to every chunk + return [header + s for s in message_splited] def send_first_message_batch(self, messages: List[bytes], p1: int) -> RAPDU: diff --git a/test/python/apps/solana_keychain.py b/test/python/apps/solana_keychain.py new file mode 100644 index 00000000..e630a6df --- /dev/null +++ b/test/python/apps/solana_keychain.py @@ -0,0 +1,30 @@ +import os +import hashlib +from ecdsa import SigningKey +from ecdsa.util import sigencode_der +from enum import Enum, auto + + +# Private key PEM files have to be named the same (lowercase) as their corresponding enum entries +# Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem +class Key(Enum): + TRUSTED_NAME = auto() + + +_keys: dict[Key, SigningKey] = dict() + + +# Open the corresponding PEM file and load its key in the global dict +def _init_key(key: Key): + global _keys + with open("%s/keychain/%s.pem" % (os.path.dirname(__file__), key.name.lower())) as pem_file: + _keys[key] = SigningKey.from_pem(pem_file.read(), hashlib.sha256) + assert (key in _keys) and (_keys[key] is not None) + + +# Generate a SECP256K1 signature of the given data with the given key +def sign_data(key: Key, data: bytes) -> bytes: + global _keys + if key not in _keys: + _init_key(key) + return _keys[key].sign_deterministic(data, sigencode=sigencode_der) \ No newline at end of file diff --git a/test/python/apps/solana_tlv.py b/test/python/apps/solana_tlv.py new file mode 100644 index 00000000..f22fa45b --- /dev/null +++ b/test/python/apps/solana_tlv.py @@ -0,0 +1,42 @@ +from typing import Union +from enum import IntEnum + +def der_encode(value: int) -> bytes: + # max() to have minimum length of 1 + value_bytes = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big') + if value >= 0x80: + value_bytes = (0x80 | len(value_bytes)).to_bytes(1, 'big') + value_bytes + return value_bytes + +# https://ledgerhq.atlassian.net/wiki/spaces/TrustServices/pages/3736863735/LNS+Arch+Nano+Trusted+Names+Descriptor+Format+APIs#TLV-description +class FieldTag(IntEnum): + TAG_STRUCTURE_TYPE = 0x01 + TAG_VERSION = 0x02 + TAG_TRUSTED_NAME_TYPE = 0x70 + TAG_TRUSTED_NAME_SOURCE = 0x71 + TAG_TRUSTED_NAME = 0x20 + TAG_CHAIN_ID = 0x23 + TAG_ADDRESS = 0x22 + TAG_TRUSTED_NAME_NFT_ID = 0x72 + TAG_TRUSTED_NAME_SOURCE_CONTRACT = 0x73 + TAG_CHALLENGE = 0x12 + TAG_NOT_VALID_AFTER = 0x10 + TAG_SIGNER_KEY_ID = 0x13 + TAG_SIGNER_ALGO = 0x14 + TAG_DER_SIGNATURE = 0x15 + +def format_tlv(tag: int, value: Union[int, str, bytes]) -> bytes: + if isinstance(value, int): + # max() to have minimum length of 1 + value = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big') + elif isinstance(value, str): + value = value.encode() + + assert isinstance(value, bytes), f"Unhandled TLV formatting for type : {type(value)}" + + tlv = bytearray() + tlv += der_encode(tag) + tlv += der_encode(len(value)) + tlv += value + return tlv + diff --git a/test/python/apps/solana_utils.py b/test/python/apps/solana_utils.py index 61d697d3..d45db03c 100644 --- a/test/python/apps/solana_utils.py +++ b/test/python/apps/solana_utils.py @@ -2,8 +2,6 @@ from ragger.utils import create_currency_config from ragger.bip import pack_derivation_path -from typing import Optional -import struct ### Some utilities functions for amounts conversions ### @@ -36,18 +34,15 @@ def lamports_to_bytes(lamports: int) -> str: ### Proposed foreign and owned addresses ### # "Foreign" Solana public key (actually the device public key derived on m/44'/501'/11111') -FOREIGN_ADDRESS_STR = "AxmUF3qkdz1zs151Q5WttVMkFpFGQPwghZs4d1mwY55d" -FOREIGN_ADDRESS = FOREIGN_ADDRESS_STR.encode('utf-8') +FOREIGN_ADDRESS = b"AxmUF3qkdz1zs151Q5WttVMkFpFGQPwghZs4d1mwY55d" FOREIGN_PUBLIC_KEY = base58.b58decode(FOREIGN_ADDRESS) # "Foreign" Solana public key (actually the device public key derived on m/44'/501'/11112') -FOREIGN_ADDRESS_2_STR = "8bjDMujLMttbmkTtoFgfw2sPYchSzzcTCEPGYDaNs3nj" -FOREIGN_ADDRESS_2 = FOREIGN_ADDRESS_2_STR.encode('utf-8') +FOREIGN_ADDRESS_2 = b"8bjDMujLMttbmkTtoFgfw2sPYchSzzcTCEPGYDaNs3nj" FOREIGN_PUBLIC_KEY_2 = base58.b58decode(FOREIGN_ADDRESS_2) # Device Solana public key (derived on m/44'/501'/12345') -OWNED_ADDRESS_STR = "3GJzvStsiYZonWE7WTsmt1BpWXkfcgWMGinaDwNs9HBc" -OWNED_ADDRESS = OWNED_ADDRESS_STR.encode('utf-8') +OWNED_ADDRESS = b"3GJzvStsiYZonWE7WTsmt1BpWXkfcgWMGinaDwNs9HBc" OWNED_PUBLIC_KEY = base58.b58decode(OWNED_ADDRESS) @@ -60,17 +55,3 @@ def lamports_to_bytes(lamports: int) -> str: ### Package this currency configuration in exchange format ### SOL_CONF = create_currency_config("SOL", "Solana") - -def get_sub_config(ticker: str, decimals: int, chain_id: Optional[int] = None) -> bytes: - cfg = bytearray() - cfg.append(len(ticker)) - cfg += ticker.encode() - cfg.append(decimals) - if chain_id is not None: - cfg += struct.pack(">Q", chain_id) - return cfg - -JUP_CONF = create_currency_config("JUP", "Solana", get_sub_config("JUP", 6)) -JUP_PACKED_DERIVATION_PATH = SOL_PACKED_DERIVATION_PATH - -JUP_MINT_ADDRESS_STR = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" \ No newline at end of file diff --git a/test/python/test_flow_order.py b/test/python/test_flow_order.py index b36feea5..d0fbf865 100644 --- a/test/python/test_flow_order.py +++ b/test/python/test_flow_order.py @@ -56,6 +56,8 @@ def all_commands_for_subcommand_except(s: SubCommand, cs: List[Command]) -> List Command.CHECK_PARTNER, Command.PROCESS_TRANSACTION_RESPONSE, Command.CHECK_TRANSACTION_SIGNATURE, + Command.GET_CHALLENGE, + Command.SEND_TRUSTED_NAME_DESCRIPTOR, Command.PROMPT_UI_DISPLAY, Command.START_SIGNING_TRANSACTION] if s == SubCommand.SWAP or s == SubCommand.SWAP_NG: @@ -85,7 +87,8 @@ def try_all_commands_for_subcommand_except(ex: ExchangeClient, s: SubCommand, c: @pytest.mark.parametrize("subcommand", ALL_SUBCOMMANDS) @pytest.mark.parametrize("prompt_ui_separately", [True, False]) -def test_wrong_flow_order(backend, subcommand, prompt_ui_separately, exchange_navigation_helper): +@pytest.mark.parametrize("send_trusted_name", [0, 1]) +def test_wrong_flow_order(backend, subcommand, prompt_ui_separately, send_trusted_name, exchange_navigation_helper): # Mutualize new and legacy snapshots. Eg SubCommand.SWAP_NG => "swap" # Also mutualize prompt_ui_separately tests suffix = "_" + str(subcommand).split('.')[1].split('_')[0].lower() @@ -112,33 +115,43 @@ def test_wrong_flow_order(backend, subcommand, prompt_ui_separately, exchange_na try_all_commands_for_subcommand_except(ex, subcommand, Command.CHECK_TRANSACTION_SIGNATURE) ex.check_transaction_signature(tx_signature) - if subcommand == SubCommand.SWAP or subcommand == SubCommand.SWAP_NG: - try_all_commands_for_subcommand_except(ex, subcommand, Command.CHECK_PAYOUT_ADDRESS) - ex.check_payout_address(CURRENCY_TO.get_conf_for_ticker()) - - if prompt_ui_separately: - try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_REFUND_ADDRESS_AND_DISPLAY, Command.CHECK_REFUND_ADDRESS_NO_DISPLAY]) - ex.check_refund_address_no_display(CURRENCY_FROM.get_conf_for_ticker()) - - try_all_commands_for_subcommand_except(ex, subcommand, Command.PROMPT_UI_DISPLAY) - with ex.prompt_ui_display(): - exchange_navigation_helper.simple_accept() - else: - try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_REFUND_ADDRESS_AND_DISPLAY, Command.CHECK_REFUND_ADDRESS_NO_DISPLAY]) - with ex.check_refund_address(CURRENCY_FROM.get_conf_for_ticker()): - exchange_navigation_helper.simple_accept() - else: - if subcommand == SubCommand.FUND or subcommand == SubCommand.SELL: - try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_LEGACY_AND_DISPLAY, Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY]) - else: - try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY]) - if prompt_ui_separately: - ex.check_asset_in_no_display(CURRENCY_FROM.get_conf_for_ticker()) - with ex.prompt_ui_display(): - exchange_navigation_helper.simple_accept() - else: - with ex.check_asset_in(CURRENCY_FROM.get_conf_for_ticker()): - exchange_navigation_helper.simple_accept() - - try_all_commands_for_subcommand_except(ex, subcommand, Command.START_SIGNING_TRANSACTION) - ex.start_signing_transaction() + if send_trusted_name != 0: + # if subcommand == SubCommand.SWAP or subcommand == SubCommand.SWAP_NG: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_PAYOUT_ADDRESS, Command.GET_CHALLENGE]) + # if subcommand == SubCommand.FUND or subcommand == SubCommand.SELL: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_LEGACY_AND_DISPLAY, Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY, Command.GET_CHALLENGE]) + # else: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY, Command.GET_CHALLENGE]) + ex.get_challenge() + + + # if subcommand == SubCommand.SWAP or subcommand == SubCommand.SWAP_NG: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_PAYOUT_ADDRESS, Command.GET_CHALLENGE]) + # ex.check_payout_address(CURRENCY_TO.get_conf_for_ticker()) + + # if prompt_ui_separately: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_REFUND_ADDRESS_AND_DISPLAY, Command.CHECK_REFUND_ADDRESS_NO_DISPLAY]) + # ex.check_refund_address_no_display(CURRENCY_FROM.get_conf_for_ticker()) + + # try_all_commands_for_subcommand_except(ex, subcommand, Command.PROMPT_UI_DISPLAY) + # with ex.prompt_ui_display(): + # exchange_navigation_helper.simple_accept() + # else: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_REFUND_ADDRESS_AND_DISPLAY, Command.CHECK_REFUND_ADDRESS_NO_DISPLAY]) + # with ex.check_refund_address(CURRENCY_FROM.get_conf_for_ticker()): + # exchange_navigation_helper.simple_accept() + # else: + # if subcommand == SubCommand.FUND or subcommand == SubCommand.SELL: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_LEGACY_AND_DISPLAY, Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY, Command.GET_CHALLENGE]) + # else: + # try_all_commands_for_subcommand_except(ex, subcommand, [Command.CHECK_ASSET_IN_AND_DISPLAY, Command.CHECK_ASSET_IN_NO_DISPLAY, Command.GET_CHALLENGE]) + # if prompt_ui_separately: + # ex.check_asset_in_no_display(CURRENCY_FROM.get_conf_for_ticker()) + # with ex.prompt_ui_display(): + # exchange_navigation_helper.simple_accept() + # else: + # with ex.check_asset_in(CURRENCY_FROM.get_conf_for_ticker()): + # exchange_navigation_helper.simple_accept() + + # try_all_commands_for_subcommand_except(ex, subcommand, Command.START_SIGNING_TRANSACTION) + # ex.start_signing_transaction() diff --git a/test/python/test_solana.py b/test/python/test_solana.py index b2082c33..e0e50847 100644 --- a/test/python/test_solana.py +++ b/test/python/test_solana.py @@ -55,93 +55,93 @@ class TestsSolana: def test_solana(self, backend, exchange_navigation_helper, test_to_run): GenericSolanaTests(backend, exchange_navigation_helper).run_test(test_to_run) -class SPLTokenTests(GenericSolanaTests): - currency_configuration = cal.JUP_CURRENCY_CONFIGURATION - - valid_destination_1 = str( - get_associated_token_address( - Pubkey.from_string(SOL.FOREIGN_ADDRESS_STR), - Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) - ) - ) - valid_destination_2 = str( - get_associated_token_address( - Pubkey.from_string(SOL.FOREIGN_ADDRESS_2_STR), - Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) - ) - ) - valid_refund = str( - get_associated_token_address( - Pubkey.from_string(SOL.OWNED_ADDRESS_STR), - Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) - ) - ) - valid_send_amount_1 = SOL.AMOUNT - valid_send_amount_2 = SOL.AMOUNT_2 - valid_fees_1 = SOL.FEES - valid_fees_2 = SOL.FEES_2 - fake_refund = SOL.FOREIGN_ADDRESS_STR - fake_payout = SOL.FOREIGN_ADDRESS_STR - signature_refusal_error_code = ErrorType.SOLANA_SUMMARY_FINALIZE_FAILED - - def perform_final_tx(self, destination, send_amount, fees, memo): - # Get the sender public key - sender_public_key = Pubkey.from_string(SOL.OWNED_ADDRESS_STR) - - # Get the associated token addresses for the sender - sender_ata = get_associated_token_address(sender_public_key, SOL.JUP_MINT_ADDRESS) - - # Define the amount to send (in the smallest unit, e.g., if JUP has 6 decimals, 1 JUP = 1_000_000) - amount = send_amount - - # Create the transaction - transfer_instruction = transfer_checked( - TransferCheckedParams( - program_id=TOKEN_PROGRAM_ID, - source=sender_ata, - mint=SOL.JUP_MINT_ADDRESS, - dest=destination, - owner=sender_public_key, - amount=amount, - decimals=6 # Number of decimals for JUP token - ) - ) - - blockhash = Hash.default() - message = Message.new_with_blockhash([transfer_instruction], sender_public_key, blockhash) - tx = Transaction.new_unsigned(message) - - # Dump the message embedded in the transaction - message = tx.message_data() - - sol = SolanaClient(self.backend) - with sol.send_async_sign_message(SOL.SOL_PACKED_DERIVATION_PATH, message): - pass - signature: bytes = sol.get_async_response().data - verify_signature(SOL.OWNED_PUBLIC_KEY, message, signature) - -# Use a class to reuse the same Speculos instance -class TestsSPLToken: - @pytest.mark.parametrize('test_to_run', ALL_TESTS_EXCEPT_MEMO_THORSWAP_AND_FEES) - def test_solana_spl_token(self, backend, exchange_navigation_helper, test_to_run): - SPLTokenTests(backend, exchange_navigation_helper).run_test(test_to_run) - -# ExchangeTestRunner implementation for Solana -class SolanaPriorityFeesTests(GenericSolanaTests): - def perform_final_tx(self, destination, send_amount, fees, memo): - decoded_destination = SOLANA_ADDRESS_DECODER[destination] - computeUnitLimit: ComputeBudgetInstructionSetComputeUnitLimit = ComputeBudgetInstructionSetComputeUnitLimit(300) - computeUnitPrice: ComputeBudgetInstructionSetComputeUnitPrice = ComputeBudgetInstructionSetComputeUnitPrice(20000) - instruction: SystemInstructionTransfer = SystemInstructionTransfer(SOL.OWNED_PUBLIC_KEY, decoded_destination, send_amount) - message: bytes = Message([computeUnitLimit, computeUnitPrice, instruction]).serialize() - sol = SolanaClient(self.backend) - with sol.send_async_sign_message(SOL.SOL_PACKED_DERIVATION_PATH, message): - pass - signature: bytes = sol.get_async_response().data - verify_signature(SOL.OWNED_PUBLIC_KEY, message, signature) - -# Use a class to reuse the same Speculos instance -class TestsSolanaPriorityFees: - @pytest.mark.parametrize('test_to_run', ALL_TESTS_EXCEPT_MEMO_THORSWAP_AND_FEES) - def test_solana_priority_fees(self, backend, exchange_navigation_helper, test_to_run): - SolanaPriorityFeesTests(backend, exchange_navigation_helper).run_test(test_to_run) +# class SPLTokenTests(GenericSolanaTests): +# currency_configuration = cal.JUP_CURRENCY_CONFIGURATION + +# valid_destination_1 = str( +# get_associated_token_address( +# Pubkey.from_string(SOL.FOREIGN_ADDRESS_STR), +# Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) +# ) +# ) +# valid_destination_2 = str( +# get_associated_token_address( +# Pubkey.from_string(SOL.FOREIGN_ADDRESS_2_STR), +# Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) +# ) +# ) +# valid_refund = str( +# get_associated_token_address( +# Pubkey.from_string(SOL.OWNED_ADDRESS_STR), +# Pubkey.from_string(SOL.JUP_MINT_ADDRESS_STR) +# ) +# ) +# valid_send_amount_1 = SOL.AMOUNT +# valid_send_amount_2 = SOL.AMOUNT_2 +# valid_fees_1 = SOL.FEES +# valid_fees_2 = SOL.FEES_2 +# fake_refund = SOL.FOREIGN_ADDRESS_STR +# fake_payout = SOL.FOREIGN_ADDRESS_STR +# signature_refusal_error_code = ErrorType.SOLANA_SUMMARY_FINALIZE_FAILED + +# def perform_final_tx(self, destination, send_amount, fees, memo): +# # Get the sender public key +# sender_public_key = Pubkey.from_string(SOL.OWNED_ADDRESS_STR) + +# # Get the associated token addresses for the sender +# sender_ata = get_associated_token_address(sender_public_key, SOL.JUP_MINT_ADDRESS) + +# # Define the amount to send (in the smallest unit, e.g., if JUP has 6 decimals, 1 JUP = 1_000_000) +# amount = send_amount + +# # Create the transaction +# transfer_instruction = transfer_checked( +# TransferCheckedParams( +# program_id=TOKEN_PROGRAM_ID, +# source=sender_ata, +# mint=SOL.JUP_MINT_ADDRESS, +# dest=destination, +# owner=sender_public_key, +# amount=amount, +# decimals=6 # Number of decimals for JUP token +# ) +# ) + +# blockhash = Hash.default() +# message = Message.new_with_blockhash([transfer_instruction], sender_public_key, blockhash) +# tx = Transaction.new_unsigned(message) + +# # Dump the message embedded in the transaction +# message = tx.message_data() + +# sol = SolanaClient(self.backend) +# with sol.send_async_sign_message(SOL.SOL_PACKED_DERIVATION_PATH, message): +# pass +# signature: bytes = sol.get_async_response().data +# verify_signature(SOL.OWNED_PUBLIC_KEY, message, signature) + +# # Use a class to reuse the same Speculos instance +# class TestsSPLToken: +# @pytest.mark.parametrize('test_to_run', ALL_TESTS_EXCEPT_MEMO_THORSWAP_AND_FEES) +# def test_solana_spl_token(self, backend, exchange_navigation_helper, test_to_run): +# SPLTokenTests(backend, exchange_navigation_helper).run_test(test_to_run) + +# # ExchangeTestRunner implementation for Solana +# class SolanaPriorityFeesTests(GenericSolanaTests): +# def perform_final_tx(self, destination, send_amount, fees, memo): +# decoded_destination = SOLANA_ADDRESS_DECODER[destination] +# computeUnitLimit: ComputeBudgetInstructionSetComputeUnitLimit = ComputeBudgetInstructionSetComputeUnitLimit(300) +# computeUnitPrice: ComputeBudgetInstructionSetComputeUnitPrice = ComputeBudgetInstructionSetComputeUnitPrice(20000) +# instruction: SystemInstructionTransfer = SystemInstructionTransfer(SOL.OWNED_PUBLIC_KEY, decoded_destination, send_amount) +# message: bytes = Message([computeUnitLimit, computeUnitPrice, instruction]).serialize() +# sol = SolanaClient(self.backend) +# with sol.send_async_sign_message(SOL.SOL_PACKED_DERIVATION_PATH, message): +# pass +# signature: bytes = sol.get_async_response().data +# verify_signature(SOL.OWNED_PUBLIC_KEY, message, signature) + +# # Use a class to reuse the same Speculos instance +# class TestsSolanaPriorityFees: +# @pytest.mark.parametrize('test_to_run', ALL_TESTS_EXCEPT_MEMO_THORSWAP_AND_FEES) +# def test_solana_priority_fees(self, backend, exchange_navigation_helper, test_to_run): +# SolanaPriorityFeesTests(backend, exchange_navigation_helper).run_test(test_to_run) diff --git a/test/python/test_trusted_name.py b/test/python/test_trusted_name.py new file mode 100644 index 00000000..74c2bc37 --- /dev/null +++ b/test/python/test_trusted_name.py @@ -0,0 +1,217 @@ +import pytest +from dataclasses import dataclass +from ragger.utils import prefix_with_len +from ragger.error import ExceptionRAPDU +from typing import Optional + +from .apps.exchange import ExchangeClient, Rate, SubCommand, Errors +from .apps.litecoin import LitecoinClient + +from .apps.signing_authority import SigningAuthority, LEDGER_SIGNER +from .apps.exchange_transaction_builder import get_partner_curve, craft_and_sign_tx, ALL_SUBCOMMANDS, get_credentials +from .apps import cal as cal +from .utils import handle_lib_call_start_or_stop + +CURRENCY_FROM = cal.ETH_CURRENCY_CONFIGURATION +CURRENCY_TO = cal.BTC_CURRENCY_CONFIGURATION + +# Some valid infos for TX. Content is irrelevant for the test + +SWAP_TX_INFOS = { + "payin_address": b"0xd692Cb1346262F584D17B4B470954501f6715a82", + "payin_extra_id": b"", + "refund_address": b"0xDad77910DbDFdE764fC21FCD4E74D71bBACA6D8D", + "refund_extra_id": b"", + "payout_address": b"bc1qqtl9jlrwcr3fsfcjj2du7pu6fcgaxl5dsw2vyg", + "payout_extra_id": b"", + "currency_from": CURRENCY_FROM.ticker, + "currency_to": CURRENCY_TO.ticker, + "amount_to_provider": bytes.fromhex("013fc3a717fb5000"), + "amount_to_wallet": b"\x0b\xeb\xc2\x00", +} +FUND_TX_INFOS = { + "user_id": "John Wick", + "account_name": "Remember Daisy", + "in_currency": CURRENCY_FROM.ticker, + "in_amount": b"\032\200\250]$T\000", + "in_address": "0x252fb4acbe0de4f0bd2409a5ed59a71e4ef1d2bc" +} +SELL_TX_INFOS = { + "trader_email": "john@doe.lost", + "out_currency": "USD", + "out_amount": {"coefficient": b"\x01", "exponent": 3}, + "in_currency": CURRENCY_FROM.ticker, + "in_amount": b"\032\200\250]$T\000", + "in_address": "0x252fb4acbe0de4f0bd2409a5ed59a71e4ef1d2bc" +} +TX_INFOS = { + SubCommand.SWAP: SWAP_TX_INFOS, + SubCommand.SWAP_NG: SWAP_TX_INFOS, + SubCommand.FUND: FUND_TX_INFOS, + SubCommand.FUND_NG: FUND_TX_INFOS, + SubCommand.SELL: SELL_TX_INFOS, + SubCommand.SELL_NG: SELL_TX_INFOS, +} +FEES = 100 + +class TestTrustedName: + + @pytest.mark.parametrize("subcommand", [SubCommand.SWAP, SubCommand.SWAP_NG]) + def test_trusted_name_wrong_challenge(self, backend, subcommand): + ex = ExchangeClient(backend, Rate.FIXED, subcommand) + partner = SigningAuthority(curve=get_partner_curve(subcommand), name="Name") + transaction_id = ex.init_transaction().data + credentials = get_credentials(subcommand, partner) + ex.set_partner_key(credentials) + ex.check_partner_key(LEDGER_SIGNER.sign(credentials)) + tx, tx_signature = craft_and_sign_tx(subcommand, TX_INFOS[subcommand], transaction_id, FEES, partner) + ex.process_transaction(tx) + ex.check_transaction_signature(tx_signature) + + challenge = ex.get_challenge().data + assert len(challenge) == 4 + wrong_challenge = challenge[:-1] + bytes([challenge[-1] + 1]) + with pytest.raises(ExceptionRAPDU) as e: + ex.send_trusted_name_descriptor(challenge=wrong_challenge) + assert e.value.status == Errors.WRONG_CHALLENGE + # Ensure reroll happened + with pytest.raises(ExceptionRAPDU) as e: + ex.send_trusted_name_descriptor(challenge=challenge) + assert e.value.status == Errors.WRONG_CHALLENGE + + @pytest.mark.parametrize("subcommand", [SubCommand.SWAP, SubCommand.SWAP_NG]) + @pytest.mark.parametrize("field_to_test", ["payout_address", "refund_address"]) + @pytest.mark.parametrize("value", [b"0xcafedeca", b"f" * 62]) + def test_trusted_name_valid(self, backend, subcommand, field_to_test, value): + ex = ExchangeClient(backend, Rate.FIXED, subcommand) + partner = SigningAuthority(curve=get_partner_curve(subcommand), name="Name") + transaction_id = ex.init_transaction().data + credentials = get_credentials(subcommand, partner) + ex.set_partner_key(credentials) + ex.check_partner_key(LEDGER_SIGNER.sign(credentials)) + + tx_infos = TX_INFOS[subcommand].copy() + actual_address = tx_infos[field_to_test] + temp_address = value + tx_infos[field_to_test] = temp_address + tx, tx_signature = craft_and_sign_tx(subcommand, tx_infos, transaction_id, FEES, partner) + ex.process_transaction(tx) + ex.check_transaction_signature(tx_signature) + + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, trusted_name=temp_address, address=actual_address) + ex.check_payout_address(CURRENCY_TO.get_conf_for_ticker()) + ex.check_refund_address_no_display(CURRENCY_FROM.get_conf_for_ticker()) + + @pytest.mark.parametrize("subcommand", [SubCommand.SELL, SubCommand.SELL_NG, SubCommand.FUND, SubCommand.FUND_NG]) + def test_trusted_name_invalid_subcommand(self, backend, subcommand): + ex = ExchangeClient(backend, Rate.FIXED, subcommand) + partner = SigningAuthority(curve=get_partner_curve(subcommand), name="Name") + + transaction_id = ex.init_transaction().data + + credentials = get_credentials(subcommand, partner) + ex.set_partner_key(credentials) + ex.check_partner_key(LEDGER_SIGNER.sign(credentials)) + tx, tx_signature = craft_and_sign_tx(subcommand, TX_INFOS[subcommand], transaction_id, FEES, partner) + ex.process_transaction(tx) + ex.check_transaction_signature(tx_signature) + + with pytest.raises(ExceptionRAPDU) as e: + ex.get_challenge() + assert e.value.status == Errors.INVALID_INSTRUCTION + with pytest.raises(ExceptionRAPDU) as e: + ex.send_trusted_name_descriptor() + assert e.value.status == Errors.INVALID_INSTRUCTION + + @pytest.mark.parametrize("subcommand", [SubCommand.SWAP, SubCommand.SWAP_NG]) + def test_trusted_name_missing_field(self, backend, subcommand): + ex = ExchangeClient(backend, Rate.FIXED, subcommand) + partner = SigningAuthority(curve=get_partner_curve(subcommand), name="Name") + transaction_id = ex.init_transaction().data + credentials = get_credentials(subcommand, partner) + ex.set_partner_key(credentials) + ex.check_partner_key(LEDGER_SIGNER.sign(credentials)) + tx, tx_signature = craft_and_sign_tx(subcommand, TX_INFOS[subcommand], transaction_id, FEES, partner) + ex.process_transaction(tx) + ex.check_transaction_signature(tx_signature) + + for skip in ["structure_type", "version", "trusted_name_type", "trusted_name_source", "trusted_name", "chain_id", "address", "trusted_name_source_contract", "challenge", "signer_key_id", "signer_algo", "der_signature"]: + with pytest.raises(ExceptionRAPDU) as e: + if skip == "structure_type": + ex.send_trusted_name_descriptor(structure_type=None) + elif skip == "version": + ex.send_trusted_name_descriptor(version=None) + elif skip == "trusted_name_type": + ex.send_trusted_name_descriptor(trusted_name_type=None) + elif skip == "trusted_name_source": + ex.send_trusted_name_descriptor(trusted_name_source=None) + elif skip == "trusted_name": + ex.send_trusted_name_descriptor(trusted_name=None) + elif skip == "chain_id": + ex.send_trusted_name_descriptor(chain_id=None) + elif skip == "address": + ex.send_trusted_name_descriptor(address=None) + elif skip == "trusted_name_source_contract": + ex.send_trusted_name_descriptor(trusted_name_source_contract=None) + elif skip == "challenge": + ex.send_trusted_name_descriptor(challenge=None) + elif skip == "signer_key_id": + ex.send_trusted_name_descriptor(signer_key_id=None) + elif skip == "signer_algo": + ex.send_trusted_name_descriptor(signer_algo=None) + elif skip == "der_signature": + ex.send_trusted_name_descriptor(skip_signature_field=True) + assert e.value.status == Errors.MISSING_TLV_CONTENT + + @pytest.mark.parametrize("subcommand", [SubCommand.SWAP, SubCommand.SWAP_NG]) + def test_trusted_name_wrong_field(self, backend, subcommand): + ex = ExchangeClient(backend, Rate.FIXED, subcommand) + partner = SigningAuthority(curve=get_partner_curve(subcommand), name="Name") + transaction_id = ex.init_transaction().data + credentials = get_credentials(subcommand, partner) + ex.set_partner_key(credentials) + ex.check_partner_key(LEDGER_SIGNER.sign(credentials)) + tx, tx_signature = craft_and_sign_tx(subcommand, TX_INFOS[subcommand], transaction_id, FEES, partner) + ex.process_transaction(tx) + ex.check_transaction_signature(tx_signature) + + for fake in ["structure_type", "version", "trusted_name_type", "trusted_name_source", "signer_algo"]: + with pytest.raises(ExceptionRAPDU) as e: + if fake == "structure_type": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, structure_type=4) + elif fake == "version": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, version=3) + elif fake == "trusted_name_type": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, trusted_name_type=0x07) + elif fake == "trusted_name_source": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, trusted_name_source=0x07) + elif fake == "signer_algo": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, signer_algo=2) + assert e.value.status == Errors.WRONG_TLV_CONTENT + + with pytest.raises(ExceptionRAPDU) as e: + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, signer_key_id=1) + assert e.value.status == Errors.WRONG_TLV_KEY_ID + + with pytest.raises(ExceptionRAPDU) as e: + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, fake_signature_field=True) + assert e.value.status == Errors.WRONG_TLV_SIGNATURE + + for fake in ["trusted_name", "address"]: + for value in [b"", b"F"*63]: + with pytest.raises(ExceptionRAPDU) as e: + if fake == "trusted_name": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, trusted_name=value) + elif fake == "address": + challenge = ex.get_challenge().data + ex.send_trusted_name_descriptor(challenge=challenge, address=value) + assert e.value.status == Errors.WRONG_TLV_FORMAT