diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index b66a5fa..3e15b79 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -166,111 +166,18 @@ jobs: # ===================================================== build_ragger_elfs: - name: Building binaries for Ragger tests - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest - - steps: - - name: Clone - uses: actions/checkout@v3 - - - name: Build test binaries - run: | - make -j BOLOS_SDK=$NANOS_SDK CAL_CI_KEY=1 - mv bin/app.elf app-nanos.elf - make clean - make -j BOLOS_SDK=$NANOX_SDK CAL_CI_KEY=1 - mv bin/app.elf app-nanox.elf - make clean - make -j BOLOS_SDK=$NANOSP_SDK CAL_CI_KEY=1 - mv bin/app.elf app-nanosp.elf - - - name: Upload app binaries - uses: actions/upload-artifact@v3 - with: - name: ragger_elfs - path: ./app-*.elf - - create_ragger_env: - name: Cache Ragger environment - runs-on: ubuntu-latest - - steps: - - name: Clone - uses: actions/checkout@v3 - - - name: APT update - run: | - sudo apt update - - - name: Create virtual env with dependencies - run: | - cd tests/ragger - python3 -m venv ./venv - . ./venv/bin/activate - pip3 install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt - # Used for the cache key - echo "py_deps=$(pip freeze | md5sum | cut -d' ' -f1)" >> $GITHUB_ENV - - - name: Download QEMU - run: | - sudo apt install --download-only -y qemu-user-static - mkdir -p tests/ragger/packages - cp /var/cache/apt/archives/*.deb tests/ragger/packages/ - # Used for the cache key - echo "deb_deps=$(find /var/cache/apt/archives/ -maxdepth 0 -type f -name '*.deb' | md5sum | cut -d' ' -f 1)" >> $GITHUB_ENV - - - name: Set up cache - uses: actions/cache@v3 - with: - key: ${{ runner.os }}-raggenv-${{ env.py_deps }}-${{ env.deb_deps }} - path: | - tests/ragger/venv/ - tests/ragger/packages/ - outputs: - py_deps: ${{ env.py_deps }} - deb_deps: ${{ env.deb_deps }} - + name: Build app for Ragger tests + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 + with: + upload_app_binaries_artifact: "ragger_elfs" + flags: "DEBUG=1 CAL_CI_KEY=1 DOMAIN_NAME_TEST_KEY=1" + run_for_devices: '["nanos", "nanox", "nanosp"]' jobs-ragger-tests: - name: Ragger tests - strategy: - matrix: - model: ["nanos", "nanox", "nanosp"] - needs: [build_ragger_elfs, create_ragger_env] - runs-on: ubuntu-latest - - steps: - - name: Clone - uses: actions/checkout@v3 - - - name: Download previously built artifacts - uses: actions/download-artifact@v3 - with: - name: ragger_elfs - path: tmp/ - - - name: Put them where they belong - run: | - mkdir -p tests/ragger/elfs - find tmp/ -type f -name '*.elf' -exec cp {} tests/ragger/elfs/ \; - - - name: Get cached environment - uses: actions/cache@v3 - with: - key: ${{ runner.os }}-raggenv-${{ needs.create_ragger_env.outputs.py_deps }}-${{ needs.create_ragger_env.outputs.deb_deps }} - path: | - tests/ragger/venv/ - tests/ragger/packages/ - - - name: Install QEMU - run: | - sudo mv tests/ragger/packages/*.deb /var/cache/apt/archives/ - sudo apt install -y qemu-user-static - - - name: Run tests - run: | - cd tests/ragger - . ./venv/bin/activate - pytest --path ./elfs --model ${{ matrix.model }} -s -v --tb=short + name: Run Ragger tests + needs: build_ragger_elfs + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 + with: + download_app_binaries_artifact: "ragger_elfs" + test_dir: tests/ragger + run_for_devices: '["nanos", "nanox", "nanosp"]' diff --git a/CHANGELOG.md b/CHANGELOG.md index 950ee93..b95816f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [1.10.2](https://github.com/ledgerhq/app-ethereum/compare/1.10.1...1.10.2) - 2022-02-09 +## [1.10.2](https://github.com/ledgerhq/app-ethereum/compare/1.10.1...1.10.2) - 2023-XX-XX ### Added @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - (network) Velas EVM - (network) Boba Network - (network) Energi +- Domain names support (LNX / LNS+) ### Changed diff --git a/Makefile b/Makefile index 7020d17..95afe9b 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ APP_LOAD_PARAMS += --path "1517992542'/1101353413'" APPVERSION_M=1 APPVERSION_N=10 APPVERSION_P=2 -APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) +APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)-dev APP_LOAD_FLAGS= --appFlags 0xa40 --dep Ethereum:$(APPVERSION) ########################### @@ -157,6 +157,15 @@ ifneq ($(CAL_CI_KEY),0) DEFINES += HAVE_CAL_CI_KEY endif +# ENS +ifneq ($(TARGET_NAME),TARGET_NANOS) +DEFINES += HAVE_DOMAIN_NAME +DOMAIN_NAME_TEST_KEY:=0 +ifneq ($(DOMAIN_NAME_TEST_KEY),0) +DEFINES += HAVE_DOMAIN_NAME_TEST_KEY +endif +endif + # Enabling debug PRINTF DEBUG:=0 ifneq ($(DEBUG),0) diff --git a/doc/ethapp.adoc b/doc/ethapp.adoc index bc57b3b..6997b9b 100644 --- a/doc/ethapp.adoc +++ b/doc/ethapp.adoc @@ -38,6 +38,9 @@ Application version 1.9.19 - 2022-05-17 - Add EIP712 STRUCT DEFINITION & EIP712 STRUCT IMPLEMENTATION - Update to SIGN ETH EIP712 +### 1.10.2 + - Add domain names support + ## About This application describes the APDU messages interface to communicate with the Ethereum application. @@ -881,6 +884,82 @@ _Output data_ None +### GET CHALLENGE + +#### Description + +Sends a random 32-bit long value. Can prevent replay of signed payloads when the challenge +is included in said payload. + +#### Coding + +_Command_ + +[width="80%"] +|============================================================= +| *CLA* | *INS* | *P1* | *P2* | *LC* +| E0 | 20 | 00 | 00 | 00 +|============================================================= + +_Input data_ + +None + +_Output data_ + +[width="80%"] +|=========================================== +| *Description* | *Length* +| Challenge value (BE) | 4 +|=========================================== + + +### PROVIDE DOMAIN NAME + +#### Description + +This command provides a domain name (like ENS) to be displayed during transactions in place of the address it is associated to. +It shall be run just before a transaction involving the associated address that would be displayed on the device. + +The signature is computed on the TLV payload (minus the signature obviously). + +#### Coding + +_Command_ + +[width="80%"] +|============================================================== +| *CLA* | *INS* | *P1* | *P2* | *LC* +| E0 | 22 | 01 : first chunk + + 00 : following chunk + | 00 | 00 +|============================================================== + +_Input data_ + +##### If P1 == first chunk + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Payload length | 2 +| TLV payload | variable +|========================================== + +##### If P1 == following chunk + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| TLV payload | variable +|========================================== + +_Output data_ + +None + + ## Transport protocol ### General transport description diff --git a/src/apdu_constants.h b/src/apdu_constants.h index cab0688..7911d6b 100644 --- a/src/apdu_constants.h +++ b/src/apdu_constants.h @@ -24,6 +24,8 @@ #define INS_EIP712_STRUCT_DEF 0x1A #define INS_EIP712_STRUCT_IMPL 0x1C #define INS_EIP712_FILTERING 0x1E +#define INS_ENS_GET_CHALLENGE 0x20 +#define INS_ENS_PROVIDE_INFO 0x22 #define P1_CONFIRM 0x01 #define P1_NON_CONFIRM 0x00 #define P2_NO_CHAINCODE 0x00 diff --git a/src/main.c b/src/main.c index 3ba8bd6..a483bf4 100644 --- a/src/main.c +++ b/src/main.c @@ -29,6 +29,8 @@ #include "handle_get_printable_amount.h" #include "handle_check_address.h" #include "commands_712.h" +#include "challenge.h" +#include "domain_name.h" #ifdef HAVE_STARKWARE #include "stark_crypto.h" @@ -749,6 +751,19 @@ void handleApdu(unsigned int *flags, unsigned int *tx) { break; #endif // HAVE_EIP712_FULL_SUPPORT +#ifdef HAVE_DOMAIN_NAME + case INS_ENS_GET_CHALLENGE: + handle_get_challenge(); + break; + + case INS_ENS_PROVIDE_INFO: + handle_provide_domain_name(G_io_apdu_buffer[OFFSET_P1], + G_io_apdu_buffer[OFFSET_P2], + G_io_apdu_buffer + OFFSET_CDATA, + G_io_apdu_buffer[OFFSET_LC]); + break; +#endif // HAVE_DOMAIN_NAME + #if 0 case 0xFF: // return to dashboard goto return_to_dashboard; @@ -954,19 +969,22 @@ void coin_main(chain_config_t *coin_config) { G_io_app.plane_mode = os_setting_get(OS_SETTING_PLANEMODE, NULL, 0); #endif // TARGET_NANOX - if (N_storage.initialized != 0x01) { + if (!N_storage.initialized) { internalStorage_t storage; #ifdef HAVE_ALLOW_DATA - storage.dataAllowed = 0x01; + storage.dataAllowed = true; #else - storage.dataAllowed = 0x00; + storage.dataAllowed = false; #endif - storage.contractDetails = 0x00; - storage.displayNonce = 0x00; + storage.contractDetails = false; + storage.displayNonce = false; #ifdef HAVE_EIP712_FULL_SUPPORT - storage.verbose_eip712 = 0x00; + storage.verbose_eip712 = false; #endif - storage.initialized = 0x01; +#ifdef HAVE_DOMAIN_NAME + storage.verbose_domain_name = false; +#endif + storage.initialized = true; nvm_write((void *) &N_storage, (void *) &storage, sizeof(internalStorage_t)); } @@ -980,6 +998,11 @@ void coin_main(chain_config_t *coin_config) { BLE_power(1, "Nano X"); #endif // HAVE_BLE +#ifdef HAVE_DOMAIN_NAME + // to prevent it from having a fixed value at boot + roll_challenge(); +#endif // HAVE_DOMAIN_NAME + app_main(); } CATCH(EXCEPTION_IO_RESET) { diff --git a/src/shared_context.h b/src/shared_context.h index 7d3d86b..0480a59 100644 --- a/src/shared_context.h +++ b/src/shared_context.h @@ -24,13 +24,16 @@ typedef struct bip32_path_t { } bip32_path_t; typedef struct internalStorage_t { - unsigned char dataAllowed; - unsigned char contractDetails; - unsigned char displayNonce; + bool dataAllowed; + bool contractDetails; + bool displayNonce; #ifdef HAVE_EIP712_FULL_SUPPORT bool verbose_eip712; #endif // HAVE_EIP712_FULL_SUPPORT - uint8_t initialized; +#ifdef HAVE_DOMAIN_NAME + bool verbose_domain_name; +#endif // HAVE_DOMAIN_NAME + bool initialized; } internalStorage_t; #ifdef HAVE_STARKWARE diff --git a/src_bagl/ui_domain_name.c b/src_bagl/ui_domain_name.c new file mode 100644 index 0000000..599133d --- /dev/null +++ b/src_bagl/ui_domain_name.c @@ -0,0 +1,17 @@ +#ifdef HAVE_DOMAIN_NAME + +#include "ux.h" +#include "domain_name.h" + +////////////////////////////////////////////////////////////////////// +// clang-format off +UX_STEP_NOCB( + ux_domain_name_step, + bnnn_paging, + { + .title = "Domain", + .text = g_domain_name + }); +// clang-format on + +#endif // HAVE_DOMAIN_NAME diff --git a/src_bagl/ui_domain_name.h b/src_bagl/ui_domain_name.h new file mode 100644 index 0000000..bcfa8b0 --- /dev/null +++ b/src_bagl/ui_domain_name.h @@ -0,0 +1,12 @@ +#ifdef HAVE_DOMAIN_NAME + +#ifndef UI_DOMAIN_NAME_H_ +#define UI_DOMAIN_NAME_H_ + +#include "ux.h" + +extern const ux_flow_step_t ux_domain_name_step; + +#endif // UI_DOMAIN_NAME_H_ + +#endif // HAVE_DOMAIN_NAME diff --git a/src_bagl/ui_flow.c b/src_bagl/ui_flow.c index 7cac427..a98ce6b 100644 --- a/src_bagl/ui_flow.c +++ b/src_bagl/ui_flow.c @@ -7,13 +7,26 @@ #define DISABLED_STR "Disabled" #define BUF_INCREMENT (MAX(strlen(ENABLED_STR), strlen(DISABLED_STR)) + 1) -void display_settings(const ux_flow_step_t* const start_step); -void switch_settings_blind_signing(void); -void switch_settings_display_data(void); -void switch_settings_display_nonce(void); +// Reuse the strings.common.fullAmount buffer for settings displaying. +// No risk of collision as this buffer is unused in the settings menu +#define SETTING_BLIND_SIGNING_STATE (strings.common.fullAmount) +#define SETTING_DISPLAY_DATA_STATE (strings.common.fullAmount + (BUF_INCREMENT * 1)) +#define SETTING_DISPLAY_NONCE_STATE (strings.common.fullAmount + (BUF_INCREMENT * 2)) +#define SETTING_VERBOSE_EIP712_STATE (strings.common.fullAmount + (BUF_INCREMENT * 3)) +#define SETTING_VERBOSE_DOMAIN_NAME_STATE (strings.common.fullAmount + (BUF_INCREMENT * 4)) + +#define BOOL_TO_STATE_STR(b) (b ? ENABLED_STR : DISABLED_STR) + +static void display_settings(const ux_flow_step_t* const start_step); +static void switch_settings_blind_signing(void); +static void switch_settings_display_data(void); +static void switch_settings_display_nonce(void); #ifdef HAVE_EIP712_FULL_SUPPORT -void switch_settings_verbose_eip712(void); +static void switch_settings_verbose_eip712(void); #endif // HAVE_EIP712_FULL_SUPPORT +#ifdef HAVE_DOMAIN_NAME +static void switch_settings_verbose_domain_name(void); +#endif // HAVE_DOMAIN_NAME ////////////////////////////////////////////////////////////////////// // clang-format off @@ -75,7 +88,7 @@ UX_STEP_CB( "Transaction", "blind signing", #endif - strings.common.fullAddress + SETTING_BLIND_SIGNING_STATE }); UX_STEP_CB( @@ -95,7 +108,7 @@ UX_STEP_CB( "Show contract data", "details", #endif - strings.common.fullAddress + BUF_INCREMENT + SETTING_DISPLAY_DATA_STATE }); UX_STEP_CB( @@ -115,7 +128,7 @@ UX_STEP_CB( "Show account nonce", "in transactions", #endif - strings.common.fullAddress + (BUF_INCREMENT * 2) + SETTING_DISPLAY_NONCE_STATE }); #ifdef HAVE_EIP712_FULL_SUPPORT @@ -127,10 +140,23 @@ UX_STEP_CB( "Verbose EIP-712", "Ignore filtering &", "display raw content", - strings.common.fullAddress + (BUF_INCREMENT * 3) + SETTING_VERBOSE_EIP712_STATE }); #endif // HAVE_EIP712_FULL_SUPPORT +#ifdef HAVE_DOMAIN_NAME +UX_STEP_CB( + ux_settings_flow_verbose_domain_name_step, + bnnn, + switch_settings_verbose_domain_name(), + { + "Verbose domains", + "Show", + "resolved address", + SETTING_VERBOSE_DOMAIN_NAME_STATE + }); +#endif // HAVE_DOMAIN_NAME + UX_STEP_CB( ux_settings_flow_back_step, @@ -149,54 +175,61 @@ UX_FLOW(ux_settings_flow, #ifdef HAVE_EIP712_FULL_SUPPORT &ux_settings_flow_verbose_eip712_step, #endif // HAVE_EIP712_FULL_SUPPORT +#ifdef HAVE_DOMAIN_NAME + &ux_settings_flow_verbose_domain_name_step, +#endif // HAVE_DOMAIN_NAME &ux_settings_flow_back_step); -void display_settings(const ux_flow_step_t* const start_step) { - bool settings[] = {N_storage.dataAllowed, - N_storage.contractDetails, - N_storage.displayNonce, +static void display_settings(const ux_flow_step_t* const start_step) { + strlcpy(SETTING_BLIND_SIGNING_STATE, BOOL_TO_STATE_STR(N_storage.dataAllowed), BUF_INCREMENT); + strlcpy(SETTING_DISPLAY_DATA_STATE, + BOOL_TO_STATE_STR(N_storage.contractDetails), + BUF_INCREMENT); + strlcpy(SETTING_DISPLAY_NONCE_STATE, BOOL_TO_STATE_STR(N_storage.displayNonce), BUF_INCREMENT); #ifdef HAVE_EIP712_FULL_SUPPORT - N_storage.verbose_eip712 + strlcpy(SETTING_VERBOSE_EIP712_STATE, + BOOL_TO_STATE_STR(N_storage.verbose_eip712), + BUF_INCREMENT); #endif // HAVE_EIP712_FULL_SUPPORT - }; - uint8_t offset = 0; - - for (unsigned int i = 0; i < ARRAY_SIZE(settings); ++i) { - strlcpy(strings.common.fullAddress + offset, - (settings[i] ? ENABLED_STR : DISABLED_STR), - sizeof(strings.common.fullAddress) - offset); - offset += BUF_INCREMENT; - } +#ifdef HAVE_DOMAIN_NAME + strlcpy(SETTING_VERBOSE_DOMAIN_NAME_STATE, + BOOL_TO_STATE_STR(N_storage.verbose_domain_name), + BUF_INCREMENT); +#endif // HAVE_DOMAIN_NAME ux_flow_init(0, ux_settings_flow, start_step); } -void switch_settings_blind_signing(void) { - uint8_t value = (N_storage.dataAllowed ? 0 : 1); - nvm_write((void*) &N_storage.dataAllowed, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_blind_signing_step); +static void toggle_setting(volatile bool* setting, const ux_flow_step_t* ui_step) { + bool value = !*setting; + nvm_write((void*) setting, (void*) &value, sizeof(value)); + display_settings(ui_step); } -void switch_settings_display_data(void) { - uint8_t value = (N_storage.contractDetails ? 0 : 1); - nvm_write((void*) &N_storage.contractDetails, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_display_data_step); +static void switch_settings_blind_signing(void) { + toggle_setting(&N_storage.dataAllowed, &ux_settings_flow_blind_signing_step); } -void switch_settings_display_nonce(void) { - uint8_t value = (N_storage.displayNonce ? 0 : 1); - nvm_write((void*) &N_storage.displayNonce, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_display_nonce_step); +static void switch_settings_display_data(void) { + toggle_setting(&N_storage.contractDetails, &ux_settings_flow_display_data_step); +} + +static void switch_settings_display_nonce(void) { + toggle_setting(&N_storage.displayNonce, &ux_settings_flow_display_nonce_step); } #ifdef HAVE_EIP712_FULL_SUPPORT -void switch_settings_verbose_eip712(void) { - bool value = !N_storage.verbose_eip712; - nvm_write((void*) &N_storage.verbose_eip712, (void*) &value, sizeof(value)); - display_settings(&ux_settings_flow_verbose_eip712_step); +static void switch_settings_verbose_eip712(void) { + toggle_setting(&N_storage.verbose_eip712, &ux_settings_flow_verbose_eip712_step); } #endif // HAVE_EIP712_FULL_SUPPORT +#ifdef HAVE_DOMAIN_NAME +static void switch_settings_verbose_domain_name(void) { + toggle_setting(&N_storage.verbose_domain_name, &ux_settings_flow_verbose_domain_name_step); +} +#endif // HAVE_DOMAIN_NAME + ////////////////////////////////////////////////////////////////////// // clang-format off #ifdef TARGET_NANOS diff --git a/src_bagl/ui_flow_signTx.c b/src_bagl/ui_flow_signTx.c index 574dd9c..2b55922 100644 --- a/src_bagl/ui_flow_signTx.c +++ b/src_bagl/ui_flow_signTx.c @@ -8,6 +8,8 @@ #include "ui_plugin.h" #include "common_ui.h" #include "plugins.h" +#include "domain_name.h" +#include "ui_domain_name.h" // clang-format off UX_STEP_NOCB( @@ -217,7 +219,19 @@ void ux_approve_tx(bool fromPlugin) { } else { // We're in a regular transaction, just show the amount and the address ux_approval_tx_flow[step++] = &ux_approval_amount_step; - ux_approval_tx_flow[step++] = &ux_approval_address_step; +#ifdef HAVE_DOMAIN_NAME + uint64_t chain_id = get_chain_id(); + if (has_domain_name(&chain_id, tmpContent.txContent.destination)) { + ux_approval_tx_flow[step++] = &ux_domain_name_step; + if (N_storage.verbose_domain_name) { + ux_approval_tx_flow[step++] = &ux_approval_address_step; + } + } else { +#endif // HAVE_DOMAIN_NAME + ux_approval_tx_flow[step++] = &ux_approval_address_step; +#ifdef HAVE_DOMAIN_NAME + } +#endif // HAVE_DOMAIN_NAME } if (N_storage.displayNonce) { @@ -235,4 +249,4 @@ void ux_approve_tx(bool fromPlugin) { ux_approval_tx_flow[step++] = FLOW_END_STEP; ux_flow_init(0, ux_approval_tx_flow, NULL); -} \ No newline at end of file +} diff --git a/src_features/signMessageEIP712/hash_bytes.c b/src_common/hash_bytes.c similarity index 67% rename from src_features/signMessageEIP712/hash_bytes.c rename to src_common/hash_bytes.c index 6f702a4..e6b6437 100644 --- a/src_features/signMessageEIP712/hash_bytes.c +++ b/src_common/hash_bytes.c @@ -1,5 +1,3 @@ -#ifdef HAVE_EIP712_FULL_SUPPORT - #include "hash_bytes.h" /** @@ -9,7 +7,7 @@ * @param[in] n number of bytes to hash * @param[in] hash_ctx pointer to the hashing context */ -void hash_nbytes(const uint8_t *const bytes_ptr, uint8_t n, cx_hash_t *const hash_ctx) { +void hash_nbytes(const uint8_t *bytes_ptr, size_t n, cx_hash_t *hash_ctx) { cx_hash(hash_ctx, 0, bytes_ptr, n, NULL, 0); } @@ -19,8 +17,6 @@ void hash_nbytes(const uint8_t *const bytes_ptr, uint8_t n, cx_hash_t *const has * @param[in] byte byte to hash * @param[in] hash_ctx pointer to the hashing context */ -void hash_byte(uint8_t byte, cx_hash_t *const hash_ctx) { +void hash_byte(uint8_t byte, cx_hash_t *hash_ctx) { hash_nbytes(&byte, 1, hash_ctx); } - -#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_common/hash_bytes.h b/src_common/hash_bytes.h new file mode 100644 index 0000000..2928fab --- /dev/null +++ b/src_common/hash_bytes.h @@ -0,0 +1,10 @@ +#ifndef HASH_BYTES_H_ +#define HASH_BYTES_H_ + +#include +#include "cx.h" + +void hash_nbytes(const uint8_t *const bytes_ptr, size_t n, cx_hash_t *hash_ctx); +void hash_byte(uint8_t byte, cx_hash_t *hash_ctx); + +#endif // HASH_BYTES_H_ diff --git a/src_features/getChallenge/challenge.h b/src_features/getChallenge/challenge.h new file mode 100644 index 0000000..9fdb01d --- /dev/null +++ b/src_features/getChallenge/challenge.h @@ -0,0 +1,14 @@ +#ifdef HAVE_DOMAIN_NAME + +#ifndef CHALLENGE_H_ +#define CHALLENGE_H_ + +#include + +void roll_challenge(void); +uint32_t get_challenge(void); +void handle_get_challenge(void); + +#endif // CHALLENGE_H_ + +#endif // HAVE_DOMAIN_NAME diff --git a/src_features/getChallenge/cmd_get_challenge.c b/src_features/getChallenge/cmd_get_challenge.c new file mode 100644 index 0000000..93e52d9 --- /dev/null +++ b/src_features/getChallenge/cmd_get_challenge.c @@ -0,0 +1,38 @@ +#ifdef HAVE_DOMAIN_NAME + +#include +#include +#include +#include "apdu_constants.h" +#include "challenge.h" + +static uint32_t challenge; + +/** + * Generate a new challenge from the Random Number Generator + */ +void roll_challenge(void) { + challenge = cx_rng_u32(); +} + +/** + * Get the current challenge + * + * @return challenge + */ +uint32_t get_challenge(void) { + return challenge; +} + +/** + * Send back the current challenge + */ +void handle_get_challenge(void) { + PRINTF("New challenge -> %u\n", get_challenge()); + U4BE_ENCODE(G_io_apdu_buffer, 0, get_challenge()); + U2BE_ENCODE(G_io_apdu_buffer, 4, APDU_RESPONSE_OK); + + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 6); +} + +#endif // HAVE_DOMAIN_NAME diff --git a/src_features/provideDomainName/cmd_provide_domain_name.c b/src_features/provideDomainName/cmd_provide_domain_name.c new file mode 100644 index 0000000..0d0849f --- /dev/null +++ b/src_features/provideDomainName/cmd_provide_domain_name.c @@ -0,0 +1,710 @@ +#ifdef HAVE_DOMAIN_NAME + +#include +#include +#include +#include +#include "utils.h" // ARRAY_SIZE +#include "apdu_constants.h" +#include "domain_name.h" +#include "challenge.h" +#include "mem.h" +#include "hash_bytes.h" + +static const uint8_t DOMAIN_NAME_PUB_KEY[] = { +#ifdef HAVE_DOMAIN_NAME_TEST_KEY + 0x04, 0xb9, 0x1f, 0xbe, 0xc1, 0x73, 0xe3, 0xba, 0x4a, 0x71, 0x4e, 0x01, 0x4e, 0xbc, + 0x82, 0x7b, 0x6f, 0x89, 0x9a, 0x9f, 0xa7, 0xf4, 0xac, 0x76, 0x9c, 0xde, 0x28, 0x43, + 0x17, 0xa0, 0x0f, 0x4f, 0x65, 0x0f, 0x09, 0xf0, 0x9a, 0xa4, 0xff, 0x5a, 0x31, 0x76, + 0x02, 0x55, 0xfe, 0x5d, 0xfc, 0x81, 0x13, 0x29, 0xb3, 0xb5, 0x0b, 0xe9, 0x91, 0x94, + 0xfc, 0xa1, 0x16, 0x19, 0xe6, 0x5f, 0x2e, 0xdf, 0xea +#else + 0x04, 0x6a, 0x94, 0xe7, 0xa4, 0x2c, 0xd0, 0xc3, 0x3f, 0xdf, 0x44, 0x0c, 0x8e, 0x2a, + 0xb2, 0x54, 0x2c, 0xef, 0xbe, 0x5d, 0xb7, 0xaa, 0x0b, 0x93, 0xa9, 0xfc, 0x81, 0x4b, + 0x9a, 0xcf, 0xa7, 0x5e, 0xb4, 0xe5, 0x3d, 0x6f, 0x00, 0x25, 0x94, 0xbd, 0xb6, 0x05, + 0xd9, 0xb5, 0xbd, 0xa9, 0xfa, 0x4b, 0x4b, 0xf3, 0xa5, 0x49, 0x6f, 0xd3, 0x16, 0x4b, + 0xae, 0xf5, 0xaf, 0xcf, 0x90, 0xe8, 0x40, 0x88, 0x71 +#endif +}; + +#define P1_FIRST_CHUNK 0x01 +#define P1_FOLLOWING_CHUNK 0x00 + +#define ALGO_SECP256K1 1 + +#define SLIP_44_ETHEREUM 60 + +#define DER_LONG_FORM_FLAG 0x80 // 8th bit set +#define DER_FIRST_BYTE_VALUE_MASK 0x7f + +typedef enum { TLV_TAG, TLV_LENGTH, TLV_VALUE } e_tlv_step; + +typedef enum { + STRUCTURE_TYPE = 0x01, + STRUCTURE_VERSION = 0x02, + CHALLENGE = 0x12, + SIGNER_KEY_ID = 0x13, + SIGNER_ALGO = 0x14, + SIGNATURE = 0x15, + DOMAIN_NAME = 0x20, + COIN_TYPE = 0x21, + ADDRESS = 0x22 +} e_tlv_tag; + +typedef enum { KEY_ID_TEST = 0x00, KEY_ID_PROD = 0x03 } e_key_id; + +typedef struct { + uint8_t *buf; + uint16_t size; + uint16_t expected_size; +} s_tlv_payload; + +typedef struct { + e_tlv_tag tag; + uint8_t length; + const uint8_t *value; +} s_tlv_data; + +typedef struct { + bool valid; + char *name; + uint8_t addr[ADDRESS_LENGTH]; +} s_domain_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_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx); + +typedef struct { + uint8_t tag; + t_tlv_handler *func; + uint8_t found; +} s_tlv_handler; + +static s_tlv_payload g_tlv_payload = {0}; +static s_domain_name_info g_domain_name_info; +char g_domain_name[DOMAIN_NAME_MAX_LENGTH + 1]; + +/** + * Send a response APDU + * + * @param[in] success whether it should use \ref APDU_RESPONSE_OK + * @param[in] off payload offset (0 if no data other than status word) + */ +static void response_to_domain_name(bool success, uint8_t off) { + uint16_t sw; + + if (success) { + sw = APDU_RESPONSE_OK; + } else { + sw = apdu_response_code; + } + U2BE_ENCODE(G_io_apdu_buffer, off, sw); + + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, off + 2); +} + +/** + * Checks if a domain name for the given chain ID and address is known + * + * Always wipes the content of \ref g_domain_name_info + * + * @param[in] chain_id given chain ID + * @param[in] addr given address + * @return whether there is or not + */ +bool has_domain_name(const uint64_t *chain_id, const uint8_t *addr) { + bool ret = false; + + if (g_domain_name_info.valid) { + // TODO: Remove once other domain name providers are supported + if ((*chain_id == ETHEREUM_MAINNET_CHAINID) && + (memcmp(addr, g_domain_name_info.addr, ADDRESS_LENGTH) == 0)) { + ret = true; + } + } + memset(&g_domain_name_info, 0, sizeof(g_domain_name_info)); + return ret; +} + +/** + * 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; +} + +/** + * Handler for tag \ref STRUCTURE_TYPE + * + * @param[] data the tlv data + * @param[] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_structure_type(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + (void) data; + (void) domain_name_info; + (void) sig_ctx; + return true; // unhandled for now +} + +/** + * Handler for tag \ref STRUCTURE_VERSION + * + * @param[] data the tlv data + * @param[] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_structure_version(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + (void) data; + (void) domain_name_info; + (void) sig_ctx; + return true; // unhandled for now +} + +/** + * Handler for tag \ref CHALLENGE + * + * @param[in] data the tlv data + * @param[] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_challenge(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + uint32_t value; + + (void) domain_name_info; + (void) sig_ctx; + return get_uint_from_data(data, &value) && (value == get_challenge()); +} + +/** + * Handler for tag \ref SIGNER_KEY_ID + * + * @param[in] data the tlv data + * @param[] domain_name_info the domain 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_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + uint32_t value; + + (void) domain_name_info; + if (!get_uint_from_data(data, &value) || (value > UINT8_MAX)) { + return false; + } + sig_ctx->key_id = value; + return true; +} + +/** + * Handler for tag \ref SIGNER_ALGO + * + * @param[in] data the tlv data + * @param[] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_sign_algo(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + uint32_t value; + + (void) domain_name_info; + (void) sig_ctx; + return get_uint_from_data(data, &value) && (value == ALGO_SECP256K1); +} + +/** + * Handler for tag \ref SIGNATURE + * + * @param[in] data the tlv data + * @param[] domain_name_info the domain name information + * @param[out] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_signature(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + (void) domain_name_info; + sig_ctx->input_sig_size = data->length; + sig_ctx->input_sig = data->value; + return true; +} + +/** + * Tests if the given domain 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_domain_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 DOMAIN_NAME + * + * @param[in] data the tlv data + * @param[out] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_domain_name(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + (void) sig_ctx; + if (data->length > DOMAIN_NAME_MAX_LENGTH) { + PRINTF("Domain name too long! (%u)\n", data->length); + return false; + } + // TODO: Remove once other domain name providers are supported + if ((data->length < 5) || (strncmp(".eth", (char *) &data->value[data->length - 4], 4) != 0)) { + PRINTF("Unexpected TLD!\n"); + return false; + } + for (int idx = 0; idx < data->length; ++idx) { + if (!is_valid_domain_character(data->value[idx])) { + PRINTF("Domain name contains non-allowed character! (0x%x)\n", data->value[idx]); + return false; + } + domain_name_info->name[idx] = data->value[idx]; + } + domain_name_info->name[data->length] = '\0'; + return true; +} + +/** + * Handler for tag \ref COIN_TYPE + * + * @param[in] data the tlv data + * @param[] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_coin_type(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + uint32_t value; + + (void) domain_name_info; + (void) sig_ctx; + return get_uint_from_data(data, &value) && (value == SLIP_44_ETHEREUM); +} + +/** + * Handler for tag \ref ADDRESS + * + * @param[in] data the tlv data + * @param[out] domain_name_info the domain name information + * @param[] sig_ctx the signature context + * @return whether it was successful + */ +static bool handle_address(const s_tlv_data *data, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + (void) sig_ctx; + if (data->length != ADDRESS_LENGTH) { + return false; + } + memcpy(domain_name_info->addr, data->value, ADDRESS_LENGTH); + return true; +} + +/** + * 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_ecfp_public_key_t verif_key; + + cx_hash((cx_hash_t *) &sig_ctx->hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH); + switch (sig_ctx->key_id) { +#ifdef HAVE_DOMAIN_NAME_TEST_KEY + case KEY_ID_TEST: +#else + case KEY_ID_PROD: +#endif + cx_ecfp_init_public_key(CX_CURVE_256K1, + DOMAIN_NAME_PUB_KEY, + sizeof(DOMAIN_NAME_PUB_KEY), + &verif_key); + break; + default: + PRINTF("Error: Unknown metadata key ID %u\n", sig_ctx->key_id); + return false; + } + if (!cx_ecdsa_verify(&verif_key, + CX_LAST, + CX_SHA256, + hash, + sizeof(hash), + sig_ctx->input_sig, + sig_ctx->input_sig_size)) { + PRINTF("Domain name signature verification failed!\n"); +#ifndef HAVE_BYPASS_SIGNATURES + return false; +#endif + } + return true; +} + +/** + * 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] domain_name_info the domain 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_domain_name_info *domain_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) { + handlers[idx].found += 1; + fptr = PIC(handlers[idx].func); + if (!(*fptr)(data, domain_name_info, sig_ctx)) { + PRINTF("Error while handling tag 0x%x\n", handlers[idx].tag); + return false; + } + break; + } + } + return true; +} + +/** + * Checks if all the TLV tags were found during parsing + * + * @param[in,out] handlers list of tag / handler function pairs + * @param[in] handler_count number of handlers + * @return whether all tags were found + */ +static bool check_found_tlv_tags(s_tlv_handler *handlers, int handler_count) { + // prevent missing or duplicated tags + for (int idx = 0; idx < handler_count; ++idx) { + if (handlers[idx].found != 1) { + PRINTF("Found %u occurence(s) of tag 0x%x in TLV!\n", + handlers[idx].found, + handlers[idx].tag); + return false; + } + } + 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 + */ +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"); + } else { + if (byte_length > sizeof(buf)) { + 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; + } + } + } else { // short form + *value = payload->buf[*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 + */ +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)) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + } else { + if (tmp_value <= UINT8_MAX) { + *value = tmp_value; + ret = true; + } else { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("TLV DER-encoded value larger than 8 bits\n"); + } + } + } + return 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] domain_name_info the domain name information + * @param[out] sig_ctx the signature context + * @return whether it was successful + */ +static bool parse_tlv(const s_tlv_payload *payload, + s_domain_name_info *domain_name_info, + s_sig_ctx *sig_ctx) { + s_tlv_handler handlers[] = { + {.tag = STRUCTURE_TYPE, .func = &handle_structure_type, .found = 0}, + {.tag = STRUCTURE_VERSION, .func = &handle_structure_version, .found = 0}, + {.tag = CHALLENGE, .func = &handle_challenge, .found = 0}, + {.tag = SIGNER_KEY_ID, .func = &handle_sign_key_id, .found = 0}, + {.tag = SIGNER_ALGO, .func = &handle_sign_algo, .found = 0}, + {.tag = SIGNATURE, .func = &handle_signature, .found = 0}, + {.tag = DOMAIN_NAME, .func = &handle_domain_name, .found = 0}, + {.tag = COIN_TYPE, .func = &handle_coin_type, .found = 0}, + {.tag = ADDRESS, .func = &handle_address, .found = 0}}; + e_tlv_step step = TLV_TAG; + s_tlv_data data; + size_t offset = 0; + size_t tag_start_off; + + 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 >= payload->size) { + return false; + } + data.value = &payload->buf[offset]; + if (!handle_tlv_data(handlers, + ARRAY_SIZE(handlers), + &data, + domain_name_info, + sig_ctx)) { + return false; + } + offset += data.length; + if (data.tag != SIGNATURE) { // the signature wasn't computed on itself + hash_nbytes(&payload->buf[tag_start_off], + (offset - tag_start_off), + (cx_hash_t *) &sig_ctx->hash_ctx); + } + step = TLV_TAG; + break; + + default: + return false; + } + } + return check_found_tlv_tags(handlers, ARRAY_SIZE(handlers)); +} + +/** + * Allocate and assign TLV payload + * + * @param[in] payload payload structure + * @param[in] size size of the payload + * @return whether it was successful + */ +static bool alloc_payload(s_tlv_payload *payload, uint16_t size) { + if ((payload->buf = mem_alloc(size)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + payload->expected_size = size; + return true; +} + +/** + * Deallocate and unassign TLV payload + * + * @param[in] payload payload structure + */ +static void free_payload(s_tlv_payload *payload) { + mem_dealloc(payload->expected_size); + memset(payload, 0, sizeof(*payload)); +} + +static bool handle_first_chunk(const uint8_t **data, uint8_t *length, s_tlv_payload *payload) { + // check if no payload is already in memory + if (payload->buf != NULL) { + free_payload(payload); + apdu_response_code = APDU_RESPONSE_INVALID_P1_P2; + return false; + } + + // check if we at least get the size + if (*length < sizeof(payload->expected_size)) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if (!alloc_payload(payload, U2BE(*data, 0))) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + + // skip the size so we can process it like a following chunk + *data += sizeof(payload->expected_size); + *length -= sizeof(payload->expected_size); + return true; +} + +/** + * Handle domain name APDU + * + * @param[in] p1 first APDU instruction parameter + * @param[in] p2 second APDU instruction parameter + * @param[in] data APDU payload + * @param[in] length payload size + */ +void handle_provide_domain_name(uint8_t p1, uint8_t p2, const uint8_t *data, uint8_t length) { + s_sig_ctx sig_ctx; + + (void) p2; + if (p1 == P1_FIRST_CHUNK) { + if (!handle_first_chunk(&data, &length, &g_tlv_payload)) { + return response_to_domain_name(false, 0); + } + } else { + // check if a payload is already in memory + if (g_tlv_payload.buf == NULL) { + apdu_response_code = APDU_RESPONSE_INVALID_P1_P2; + return response_to_domain_name(false, 0); + } + } + + if ((g_tlv_payload.size + length) > g_tlv_payload.expected_size) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + free_payload(&g_tlv_payload); + PRINTF("TLV payload size mismatch!\n"); + return response_to_domain_name(false, 0); + } + // feed into tlv payload + memcpy(g_tlv_payload.buf + g_tlv_payload.size, data, length); + g_tlv_payload.size += length; + + // everything has been received + if (g_tlv_payload.size == g_tlv_payload.expected_size) { + g_domain_name_info.name = g_domain_name; + if (!parse_tlv(&g_tlv_payload, &g_domain_name_info, &sig_ctx) || + !verify_signature(&sig_ctx)) { + free_payload(&g_tlv_payload); + roll_challenge(); // prevent brute-force guesses + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return response_to_domain_name(false, 0); + } + g_domain_name_info.valid = true; + PRINTF("Registered : %s => %.*h\n", + g_domain_name_info.name, + ADDRESS_LENGTH, + g_domain_name_info.addr); + free_payload(&g_tlv_payload); + roll_challenge(); // prevent replays + } + return response_to_domain_name(true, 0); +} + +#endif // HAVE_DOMAIN_NAME diff --git a/src_features/provideDomainName/domain_name.h b/src_features/provideDomainName/domain_name.h new file mode 100644 index 0000000..502254f --- /dev/null +++ b/src_features/provideDomainName/domain_name.h @@ -0,0 +1,18 @@ +#ifdef HAVE_DOMAIN_NAME + +#ifndef DOMAIN_NAME_H_ +#define DOMAIN_NAME_H_ + +#include +#include + +#define DOMAIN_NAME_MAX_LENGTH 30 + +bool has_domain_name(const uint64_t *chain_id, const uint8_t *addr); +void handle_provide_domain_name(uint8_t p1, uint8_t p2, const uint8_t *data, uint8_t length); + +extern char g_domain_name[DOMAIN_NAME_MAX_LENGTH + 1]; + +#endif // DOMAIN_NAME_H_ + +#endif // HAVE_DOMAIN_NAME diff --git a/src_features/signMessageEIP712/hash_bytes.h b/src_features/signMessageEIP712/hash_bytes.h deleted file mode 100644 index 6c1d329..0000000 --- a/src_features/signMessageEIP712/hash_bytes.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef HASH_BYTES_H_ -#define HASH_BYTES_H_ - -#ifdef HAVE_EIP712_FULL_SUPPORT - -#include "cx.h" - -void hash_nbytes(const uint8_t *const bytes_ptr, uint8_t n, cx_hash_t *hash_ctx); -void hash_byte(uint8_t byte, cx_hash_t *hash_ctx); - -#endif // HAVE_EIP712_FULL_SUPPORT - -#endif // HASH_BYTES_H_ diff --git a/tests/ragger/.gitignore b/tests/ragger/.gitignore index 93526df..a3fe09d 100644 --- a/tests/ragger/.gitignore +++ b/tests/ragger/.gitignore @@ -1,2 +1,4 @@ venv/ __pycache__/ +snapshots-tmp/ +elfs/ diff --git a/tests/ragger/ethereum_client/client.py b/tests/ragger/app/client.py similarity index 56% rename from tests/ragger/ethereum_client/client.py rename to tests/ragger/app/client.py index 9622a05..9732c81 100644 --- a/tests/ragger/ethereum_client/client.py +++ b/tests/ragger/app/client.py @@ -1,17 +1,48 @@ from enum import IntEnum, auto -from typing import Iterator, Dict, List +from typing import Optional from ragger.backend import BackendInterface from ragger.utils import RAPDU -from ethereum_client.command_builder import EthereumCmdBuilder -from ethereum_client.setting import SettingType, SettingImpl -from ethereum_client.eip712 import EIP712FieldType -from ethereum_client.response_parser import EthereumRespParser +from ragger.navigator import NavInsID, NavIns, NanoNavigator +from .command_builder import EthereumCmdBuilder +from .setting import SettingType, SettingImpl +from .eip712 import EIP712FieldType +from .response_parser import EthereumRespParser +from .tlv import format_tlv import signal import time +from pathlib import Path +import keychain +import rlp -class EthereumClient: - _settings: Dict[SettingType, SettingImpl] = { +ROOT_SCREENSHOT_PATH = Path(__file__).parent.parent +WEI_IN_ETH = 1e+18 + + +class StatusWord(IntEnum): + OK = 0x9000 + ERROR_NO_INFO = 0x6a00 + INVALID_DATA = 0x6a80 + INSUFFICIENT_MEMORY = 0x6a84 + INVALID_INS = 0x6d00 + INVALID_P1_P2 = 0x6b00 + CONDITION_NOT_SATISFIED = 0x6985 + REF_DATA_NOT_FOUND = 0x6a88 + +class DOMAIN_NAME_TAG(IntEnum): + STRUCTURE_TYPE = 0x01 + STRUCTURE_VERSION = 0x02 + CHALLENGE = 0x12 + SIGNER_KEY_ID = 0x13 + SIGNER_ALGO = 0x14 + SIGNATURE = 0x15 + DOMAIN_NAME = 0x20 + COIN_TYPE = 0x21 + ADDRESS = 0x22 + + +class EthereumClient: + _settings: dict[SettingType, SettingImpl] = { SettingType.BLIND_SIGNING: SettingImpl( [ "nanos", "nanox", "nanosp" ] ), @@ -23,15 +54,20 @@ class EthereumClient: ), SettingType.VERBOSE_EIP712: SettingImpl( [ "nanox", "nanosp" ] + ), + SettingType.VERBOSE_ENS: SettingImpl( + [ "nanox", "nanosp" ] ) } _click_delay = 1/4 _eip712_filtering = False - def __init__(self, client: BackendInterface): + def __init__(self, client: BackendInterface, golden_run: bool): self._client = client + self._chain_id = 1 self._cmd_builder = EthereumCmdBuilder() self._resp_parser = EthereumRespParser() + self._nav = NanoNavigator(client, client.firmware, golden_run) signal.signal(signal.SIGALRM, self._click_signal_timeout) for setting in self._settings.values(): setting.value = False @@ -65,11 +101,11 @@ class EthereumClient: array_levels: [], key_name: str): with self._send(self._cmd_builder.eip712_send_struct_def_struct_field( - field_type, - type_name, - type_size, - array_levels, - key_name)): + field_type, + type_name, + type_size, + array_levels, + key_name)): pass return self._recv() @@ -91,8 +127,8 @@ class EthereumClient: self._disable_click_until_response() assert self._recv().status == 0x9000 - def eip712_sign_new(self, bip32): - with self._send(self._cmd_builder.eip712_sign_new(bip32)): + def eip712_sign_new(self, bip32_path: str): + with self._send(self._cmd_builder.eip712_sign_new(bip32_path)): time.sleep(0.5) # tight on timing, needed by the CI otherwise might fail sometimes if not self._settings[SettingType.VERBOSE_EIP712].value and \ not self._eip712_filtering: # need to skip the message hash @@ -104,10 +140,10 @@ class EthereumClient: return self._resp_parser.sign(resp.data) def eip712_sign_legacy(self, - bip32, + bip32_path: str, domain_hash: bytes, message_hash: bytes): - with self._send(self._cmd_builder.eip712_sign_legacy(bip32, + with self._send(self._cmd_builder.eip712_sign_legacy(bip32_path, domain_hash, message_hash)): self._client.right_click() # sign typed message screen @@ -125,7 +161,7 @@ class EthereumClient: assert resp.status == 0x9000 return self._resp_parser.sign(resp.data) - def settings_set(self, new_values: Dict[SettingType, bool]): + def settings_set(self, new_values: dict[SettingType, bool]): # Go to settings for _ in range(2): self._client.right_click() @@ -156,3 +192,61 @@ class EthereumClient: with self._send(self._cmd_builder.eip712_filtering_show_field(name, sig)): pass assert self._recv().status == 0x9000 + + def send_fund(self, + bip32_path: str, + nonce: int, + gas_price: int, + gas_limit: int, + to: bytes, + amount: float, + chain_id: int, + screenshot_collection: str = None): + data = list() + data.append(nonce) + data.append(gas_price) + data.append(gas_limit) + data.append(to) + data.append(int(amount * WEI_IN_ETH)) + data.append(bytes()) + data.append(chain_id) + data.append(bytes()) + data.append(bytes()) + + for chunk in self._cmd_builder.sign(bip32_path, rlp.encode(data)): + with self._send(chunk): + nav_ins = NavIns(NavInsID.RIGHT_CLICK) + final_ins = [ NavIns(NavInsID.BOTH_CLICK) ] + target_text = "and send" + if screenshot_collection: + self._nav.navigate_until_text_and_compare(nav_ins, + final_ins, + target_text, + ROOT_SCREENSHOT_PATH, + screenshot_collection) + else: + self._nav.navigate_until_text(nav_ins, + final_ins, + target_text) + + def get_challenge(self) -> int: + with self._send(self._cmd_builder.get_challenge()): + pass + resp = self._recv() + return self._resp_parser.challenge(resp.data) + + def provide_domain_name(self, challenge: int, name: str, addr: bytes): + payload = format_tlv(DOMAIN_NAME_TAG.STRUCTURE_TYPE, 3) # TrustedDomainName + payload += format_tlv(DOMAIN_NAME_TAG.STRUCTURE_VERSION, 1) + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_KEY_ID, 0) # test key + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_ALGO, 1) # secp256k1 + payload += format_tlv(DOMAIN_NAME_TAG.CHALLENGE, challenge) + payload += format_tlv(DOMAIN_NAME_TAG.COIN_TYPE, 0x3c) # ETH in slip-44 + payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name) + payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr) + payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE, + keychain.sign_data(keychain.Key.DOMAIN_NAME, payload)) + + for chunk in self._cmd_builder.provide_domain_name(payload): + with self._send(chunk): + pass diff --git a/tests/ragger/ethereum_client/command_builder.py b/tests/ragger/app/command_builder.py similarity index 78% rename from tests/ragger/ethereum_client/command_builder.py rename to tests/ragger/app/command_builder.py index 134405f..aac10d0 100644 --- a/tests/ragger/ethereum_client/command_builder.py +++ b/tests/ragger/app/command_builder.py @@ -1,18 +1,25 @@ from enum import IntEnum, auto -from typing import Iterator -from ethereum_client.eip712 import EIP712FieldType +from typing import Iterator, Optional +from .eip712 import EIP712FieldType +from ragger.bip import pack_derivation_path +import struct -class InsType(IntEnum): +class InsType(IntEnum): + SIGN = 0x04 EIP712_SEND_STRUCT_DEF = 0x1a EIP712_SEND_STRUCT_IMPL = 0x1c EIP712_SEND_FILTERING = 0x1e EIP712_SIGN = 0x0c + GET_CHALLENGE = 0x20 + PROVIDE_DOMAIN_NAME = 0x22 -class P1Type(IntEnum): +class P1Type(IntEnum): COMPLETE_SEND = 0x00 PARTIAL_SEND = 0x01 + SIGN_FIRST_CHUNK = 0x00 + SIGN_SUBSQT_CHUNK = 0x80 -class P2Type(IntEnum): +class P2Type(IntEnum): STRUCT_NAME = 0x00 STRUCT_FIELD = 0xff ARRAY = 0x0f @@ -22,14 +29,14 @@ class P2Type(IntEnum): FILTERING_CONTRACT_NAME = 0x0f FILTERING_FIELD_NAME = 0xff -class EthereumCmdBuilder: +class EthereumCmdBuilder: _CLA: int = 0xE0 def _serialize(self, ins: InsType, p1: int, p2: int, - cdata: bytearray = bytearray()) -> bytes: + cdata: bytearray = bytes()) -> bytes: header = bytearray() header.append(self._CLA) @@ -109,27 +116,18 @@ class EthereumCmdBuilder: data_w_length[:0xff]) data_w_length = data_w_length[0xff:] - def _format_bip32(self, bip32, data: bytearray) -> bytearray: - data.append(len(bip32)) - for idx in bip32: - data.append((idx & 0xff000000) >> 24) - data.append((idx & 0x00ff0000) >> 16) - data.append((idx & 0x0000ff00) >> 8) - data.append((idx & 0x000000ff)) - return data - - def eip712_sign_new(self, bip32) -> bytes: - data = self._format_bip32(bip32, bytearray()) + def eip712_sign_new(self, bip32_path: str) -> bytes: + data = pack_derivation_path(bip32_path) return self._serialize(InsType.EIP712_SIGN, P1Type.COMPLETE_SEND, P2Type.NEW_IMPLEM, data) def eip712_sign_legacy(self, - bip32, + bip32_path: str, domain_hash: bytes, message_hash: bytes) -> bytes: - data = self._format_bip32(bip32, bytearray()) + data = pack_derivation_path(bip32_path) data += domain_hash data += message_hash return self._serialize(InsType.EIP712_SIGN, @@ -168,3 +166,30 @@ class EthereumCmdBuilder: P1Type.COMPLETE_SEND, P2Type.FILTERING_FIELD_NAME, self._eip712_filtering_send_name(name, sig)) + + def sign(self, bip32_path: str, rlp_data: bytes) -> Iterator[bytes]: + payload = pack_derivation_path(bip32_path) + payload += rlp_data + p1 = P1Type.SIGN_FIRST_CHUNK + while len(payload) > 0: + yield self._serialize(InsType.SIGN, + p1, + 0x00, + payload[:0xff]) + payload = payload[0xff:] + p1 = P1Type.SIGN_SUBSQT_CHUNK + + def get_challenge(self) -> bytes: + return self._serialize(InsType.GET_CHALLENGE, 0x00, 0x00) + + def provide_domain_name(self, tlv_payload: bytes) -> bytes: + payload = struct.pack(">H", len(tlv_payload)) + payload += tlv_payload + p1 = 1 + while len(payload) > 0: + yield self._serialize(InsType.PROVIDE_DOMAIN_NAME, + p1, + 0x00, + payload[:0xff]) + payload = payload[0xff:] + p1 = 0 diff --git a/tests/ragger/ethereum_client/eip712.py b/tests/ragger/app/eip712.py similarity index 84% rename from tests/ragger/ethereum_client/eip712.py rename to tests/ragger/app/eip712.py index 3438a1c..f719c6e 100644 --- a/tests/ragger/ethereum_client/eip712.py +++ b/tests/ragger/app/eip712.py @@ -1,6 +1,6 @@ from enum import IntEnum, auto -class EIP712FieldType(IntEnum): +class EIP712FieldType(IntEnum): CUSTOM = 0, INT = auto() UINT = auto() diff --git a/tests/ragger/ethereum_client/response_parser.py b/tests/ragger/app/response_parser.py similarity index 62% rename from tests/ragger/ethereum_client/response_parser.py rename to tests/ragger/app/response_parser.py index 681c18d..242f4cf 100644 --- a/tests/ragger/ethereum_client/response_parser.py +++ b/tests/ragger/app/response_parser.py @@ -1,4 +1,4 @@ -class EthereumRespParser: +class EthereumRespParser: def sign(self, data: bytes): assert len(data) == (1 + 32 + 32) @@ -12,3 +12,7 @@ class EthereumRespParser: data = data[32:] return v, r, s + + def challenge(self, data: bytes) -> int: + assert len(data) == 4 + return int.from_bytes(data, "big") diff --git a/tests/ragger/ethereum_client/setting.py b/tests/ragger/app/setting.py similarity index 78% rename from tests/ragger/ethereum_client/setting.py rename to tests/ragger/app/setting.py index a965fe3..7e79da7 100644 --- a/tests/ragger/ethereum_client/setting.py +++ b/tests/ragger/app/setting.py @@ -1,13 +1,14 @@ from enum import IntEnum, auto from typing import List -class SettingType(IntEnum): +class SettingType(IntEnum): BLIND_SIGNING = 0, DEBUG_DATA = auto() NONCE = auto() VERBOSE_EIP712 = auto() + VERBOSE_ENS = auto() -class SettingImpl: +class SettingImpl: devices: List[str] value: bool diff --git a/tests/ragger/app/tlv.py b/tests/ragger/app/tlv.py new file mode 100644 index 0000000..2ff4cef --- /dev/null +++ b/tests/ragger/app/tlv.py @@ -0,0 +1,25 @@ +from typing import Any + +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 + +def format_tlv(tag: int, value: Any) -> 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() + + if not isinstance(value, bytes): + print("Unhandled TLV formatting for type : %s" % (type(value))) + return None + + tlv = bytearray() + tlv += der_encode(tag) + tlv += der_encode(len(value)) + tlv += value + return tlv diff --git a/tests/ragger/cal/cal.py b/tests/ragger/cal/cal.py deleted file mode 100644 index e75fcab..0000000 --- a/tests/ragger/cal/cal.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import hashlib -from ecdsa.util import sigencode_der -from ecdsa import SigningKey - -_key: SigningKey = None - -def _init_key(): - global _key - with open(os.path.dirname(__file__) + "/key.pem") as pem_file: - _key = SigningKey.from_pem(pem_file.read(), hashlib.sha256) - assert _key != None - -def sign(data: bytes) -> bytes: - global _key - if not _key: - _init_key() - return _key.sign_deterministic(data, sigencode=sigencode_der) diff --git a/tests/ragger/conftest.py b/tests/ragger/conftest.py index e527350..68799b2 100644 --- a/tests/ragger/conftest.py +++ b/tests/ragger/conftest.py @@ -1,68 +1,12 @@ import pytest -from pathlib import Path -from ragger.firmware import Firmware -from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface -from ethereum_client.client import EthereumClient - -FWS = [ - Firmware("nanos", "2.1"), - Firmware("nanox", "2.0.2"), - Firmware("nanosp", "1.0.3") -] - -def pytest_addoption(parser): - parser.addoption("--backend", action="store", default="speculos") - parser.addoption("--path", action="store", default="./elfs") - parser.addoption("--model", action="store", required=True) - -# accessing the value of the "--backend" option as a fixture -@pytest.fixture -def arg_backend(pytestconfig) -> str: - return pytestconfig.getoption("backend") - -@pytest.fixture -def arg_path(pytestconfig) -> str: - return pytestconfig.getoption("path") - -@pytest.fixture -def arg_model(pytestconfig) -> str: - return pytestconfig.getoption("model") - -# Providing the firmware as a fixture -@pytest.fixture -def firmware(arg_model: str) -> Firmware: - for fw in FWS: - if fw.device == arg_model: - return fw - raise ValueError("Unknown device model \"%s\"" % (arg_model)) - -def get_elf_path(arg_path: str, firmware: Firmware) -> Path: - elf_dir = Path(arg_path).resolve() - assert elf_dir.is_dir(), ("%s is not a directory" % (arg_path)) - app = elf_dir / ("app-%s.elf" % firmware.device) - assert app.is_file(), ("Firmware %s does not exist !" % (app)) - return app - -# Depending on the "--backend" option value, a different backend is -# instantiated, and the tests will either run on Speculos or on a physical -# device depending on the backend -def create_backend(backend: str, arg_path: str, firmware: Firmware) -> BackendInterface: - if backend.lower() == "ledgercomm": - return LedgerCommBackend(firmware, interface="hid") - elif backend.lower() == "ledgerwallet": - return LedgerWalletBackend(firmware) - elif backend.lower() == "speculos": - return SpeculosBackend(get_elf_path(arg_path, firmware), firmware) - else: - raise ValueError(f"Backend '{backend}' is unknown. Valid backends are: {BACKENDS}") - -# This fixture will create and return the backend client -@pytest.fixture -def backend_client(arg_backend: str, arg_path: str, firmware: Firmware) -> BackendInterface: - with create_backend(arg_backend, arg_path, firmware) as b: - yield b +from ragger.conftest import configuration +from ragger.backend import BackendInterface +from app.client import EthereumClient # This final fixture will return the properly configured app client, to be used in tests @pytest.fixture -def app_client(backend_client: BackendInterface) -> EthereumClient: - return EthereumClient(backend_client) +def app_client(backend: BackendInterface, golden_run: bool) -> EthereumClient: + return EthereumClient(backend, golden_run) + +# Pull all features from the base ragger conftest using the overridden configuration +pytest_plugins = ("ragger.conftest.base_conftest", ) diff --git a/tests/ragger/eip712/InputData.py b/tests/ragger/eip712/InputData.py index 38ca8c0..02a96eb 100644 --- a/tests/ragger/eip712/InputData.py +++ b/tests/ragger/eip712/InputData.py @@ -4,8 +4,8 @@ import json import sys import re import hashlib -from ethereum_client.client import EthereumClient, EIP712FieldType -from cal import cal +from app.client import EthereumClient, EIP712FieldType +import keychain # global variables app_client: EthereumClient = None @@ -251,7 +251,7 @@ def send_filtering_message_info(display_name: str, filters_count: int): for char in display_name: to_sign.append(ord(char)) - sig = cal.sign(to_sign) + sig = keychain.sign_data(keychain.Key.CAL, to_sign) app_client.eip712_filtering_message_info(display_name, filters_count, sig) # ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures @@ -269,7 +269,7 @@ def send_filtering_show_field(display_name): to_sign.append(ord(char)) for char in display_name: to_sign.append(ord(char)) - sig = cal.sign(to_sign) + sig = keychain.sign_data(keychain.Key.CAL, to_sign) app_client.eip712_filtering_show_field(display_name, sig) def read_filtering_file(domain, message, filtering_file_path): diff --git a/tests/ragger/eip712/__init__.py b/tests/ragger/eip712/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/ragger/ethereum_client/__init__.py b/tests/ragger/ethereum_client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/ragger/keychain.py b/tests/ragger/keychain.py new file mode 100644 index 0000000..31914a4 --- /dev/null +++ b/tests/ragger/keychain.py @@ -0,0 +1,27 @@ +import os +import hashlib +from ecdsa.util import sigencode_der +from ecdsa import SigningKey +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): + CAL = auto() + DOMAIN_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] != 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) diff --git a/tests/ragger/cal/key.pem b/tests/ragger/keychain/cal.pem similarity index 100% rename from tests/ragger/cal/key.pem rename to tests/ragger/keychain/cal.pem diff --git a/tests/ragger/keychain/domain_name.pem b/tests/ragger/keychain/domain_name.pem new file mode 100644 index 0000000..726204e --- /dev/null +++ b/tests/ragger/keychain/domain_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----- diff --git a/tests/ragger/requirements.txt b/tests/ragger/requirements.txt index 8836582..e408ead 100644 --- a/tests/ragger/requirements.txt +++ b/tests/ragger/requirements.txt @@ -1,6 +1,4 @@ -requests>=2.28,<3.0 -click>=8.0,<9.0 # needed by the CI as it installs an older version and breaks dependencies -protobuf==3.20.1 # To fix the protobuf dependency bug -ragger[speculos] +ragger[speculos]>=1.6.0,<1.7.0 pytest ecdsa +simple-rlp diff --git a/tests/ragger/snapshots/nanosp b/tests/ragger/snapshots/nanosp new file mode 120000 index 0000000..da13a6a --- /dev/null +++ b/tests/ragger/snapshots/nanosp @@ -0,0 +1 @@ +nanox/ \ No newline at end of file diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00000.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00000.png new file mode 100644 index 0000000..487ea10 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00000.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00001.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00001.png new file mode 100644 index 0000000..f9840e3 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00001.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00002.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00002.png new file mode 100644 index 0000000..6a5a9d6 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00002.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00003.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00003.png new file mode 100644 index 0000000..93112b3 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00003.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00004.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00004.png new file mode 100644 index 0000000..70c1b9a Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00004.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00005.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00005.png new file mode 100644 index 0000000..570ce28 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00005.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00006.png b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00006.png new file mode 100644 index 0000000..a58590b Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_non_mainnet/00006.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00000.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00000.png new file mode 100644 index 0000000..487ea10 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00000.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00001.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00001.png new file mode 100644 index 0000000..f9840e3 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00001.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00002.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00002.png new file mode 100644 index 0000000..6af0ec5 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00002.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00003.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00003.png new file mode 100644 index 0000000..70c1b9a Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00003.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00004.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00004.png new file mode 100644 index 0000000..570ce28 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00004.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_False/00005.png b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00005.png new file mode 100644 index 0000000..a58590b Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_False/00005.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00000.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00000.png new file mode 100644 index 0000000..487ea10 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00000.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00001.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00001.png new file mode 100644 index 0000000..f9840e3 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00001.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00002.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00002.png new file mode 100644 index 0000000..6af0ec5 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00002.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00003.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00003.png new file mode 100644 index 0000000..6a5a9d6 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00003.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00004.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00004.png new file mode 100644 index 0000000..70c1b9a Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00004.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00005.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00005.png new file mode 100644 index 0000000..570ce28 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00005.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_verbose_True/00006.png b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00006.png new file mode 100644 index 0000000..a58590b Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_verbose_True/00006.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00000.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00000.png new file mode 100644 index 0000000..487ea10 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00000.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00001.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00001.png new file mode 100644 index 0000000..f9840e3 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00001.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00002.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00002.png new file mode 100644 index 0000000..639e421 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00002.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00003.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00003.png new file mode 100644 index 0000000..70c1b9a Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00003.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00004.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00004.png new file mode 100644 index 0000000..570ce28 Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00004.png differ diff --git a/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00005.png b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00005.png new file mode 100644 index 0000000..a58590b Binary files /dev/null and b/tests/ragger/snapshots/nanox/domain_name_wrong_addr/00005.png differ diff --git a/tests/ragger/test_domain_name.py b/tests/ragger/test_domain_name.py new file mode 100644 index 0000000..317f72d --- /dev/null +++ b/tests/ragger/test_domain_name.py @@ -0,0 +1,128 @@ +import pytest +from ragger.error import ExceptionRAPDU +from app.client import EthereumClient, StatusWord +from app.setting import SettingType +import struct + +# Values used across all tests +CHAIN_ID = 1 +NAME = "ledger.eth" +ADDR = bytes.fromhex("0011223344556677889900112233445566778899") +KEY_ID = 1 +ALGO_ID = 1 +BIP32_PATH = "m/44'/60'/0'/0/0" +NONCE = 21 +GAS_PRICE = 13000000000 +GAS_LIMIT = 21000 +AMOUNT = 1.22 + +@pytest.fixture(params=[False, True]) +def verbose(request) -> bool: + return request.param + +def common(app_client: EthereumClient) -> int: + if app_client._client.firmware.device == "nanos": + pytest.skip("Not supported on LNS") + return app_client.get_challenge() + + +def test_send_fund(app_client: EthereumClient, verbose: bool): + challenge = common(app_client) + + if verbose: + app_client.settings_set({ + SettingType.VERBOSE_ENS: True + }) + + app_client.provide_domain_name(challenge, NAME, ADDR) + + app_client.send_fund(BIP32_PATH, + NONCE, + GAS_PRICE, + GAS_LIMIT, + ADDR, + AMOUNT, + CHAIN_ID, + "domain_name_verbose_" + str(verbose)) + +def test_send_fund_wrong_challenge(app_client: EthereumClient): + caught = False + challenge = common(app_client) + + try: + app_client.provide_domain_name(~challenge & 0xffffffff, NAME, ADDR) + except ExceptionRAPDU as e: + assert e.status == StatusWord.INVALID_DATA + else: + assert False # An exception should have been raised + +def test_send_fund_wrong_addr(app_client: EthereumClient): + challenge = common(app_client) + + app_client.provide_domain_name(challenge, NAME, ADDR) + + addr = bytearray(ADDR) + addr.reverse() + + app_client.send_fund(BIP32_PATH, + NONCE, + GAS_PRICE, + GAS_LIMIT, + addr, + AMOUNT, + CHAIN_ID, + "domain_name_wrong_addr") + +def test_send_fund_non_mainnet(app_client: EthereumClient): + challenge = common(app_client) + + app_client.provide_domain_name(challenge, NAME, ADDR) + + app_client.send_fund(BIP32_PATH, + NONCE, + GAS_PRICE, + GAS_LIMIT, + ADDR, + AMOUNT, + 5, + "domain_name_non_mainnet") + +def test_send_fund_domain_too_long(app_client: EthereumClient): + challenge = common(app_client) + + try: + app_client.provide_domain_name(challenge, "ledger" + "0"*25 + ".eth", ADDR) + except ExceptionRAPDU as e: + assert e.status == StatusWord.INVALID_DATA + else: + assert False # An exception should have been raised + +def test_send_fund_domain_invalid_character(app_client: EthereumClient): + challenge = common(app_client) + + try: + app_client.provide_domain_name(challenge, "l\xe8dger.eth", ADDR) + except ExceptionRAPDU as e: + assert e.status == StatusWord.INVALID_DATA + else: + assert False # An exception should have been raised + +def test_send_fund_uppercase(app_client: EthereumClient): + challenge = common(app_client) + + try: + app_client.provide_domain_name(challenge, NAME.upper(), ADDR) + except ExceptionRAPDU as e: + assert e.status == StatusWord.INVALID_DATA + else: + assert False # An exception should have been raised + +def test_send_fund_domain_non_ens(app_client: EthereumClient): + challenge = common(app_client) + + try: + app_client.provide_domain_name(challenge, "ledger.hte", ADDR) + except ExceptionRAPDU as e: + assert e.status == StatusWord.INVALID_DATA + else: + assert False # An exception should have been raised diff --git a/tests/ragger/test_eip712.py b/tests/ragger/test_eip712.py index bb53ba8..29cb843 100644 --- a/tests/ragger/test_eip712.py +++ b/tests/ragger/test_eip712.py @@ -2,23 +2,17 @@ import pytest import os import fnmatch from typing import List -from ethereum_client.client import EthereumClient, SettingType +from app.client import EthereumClient, SettingType from eip712 import InputData from pathlib import Path from configparser import ConfigParser -bip32 = [ - 0x8000002c, - 0x8000003c, - 0x80000000, - 0, - 0 -] +BIP32_PATH = "m/44'/60'/0'/0/0" def input_files() -> List[str]: files = [] - for file in os.scandir("./eip712/input_files"): + for file in os.scandir("%s/eip712/input_files" % (os.path.dirname(__file__))): if fnmatch.fnmatch(file, "*-data.json"): files.append(file.path) return sorted(files) @@ -38,7 +32,7 @@ def filtering(request) -> bool: def test_eip712_legacy(app_client: EthereumClient): v, r, s = app_client.eip712_sign_legacy( - bip32, + BIP32_PATH, bytes.fromhex('6137beb405d9ff777172aa879e33edb34a1460e701802746c5ef96e741710e59'), bytes.fromhex('eb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8') ) @@ -47,10 +41,10 @@ def test_eip712_legacy(app_client: EthereumClient): assert r == bytes.fromhex("ea66f747173762715751c889fea8722acac3fc35db2c226d37a2e58815398f64") assert s == bytes.fromhex("52d8ba9153de9255da220ffd36762c0b027701a3b5110f0a765f94b16a9dfb55") - def test_eip712_new(app_client: EthereumClient, input_file: Path, verbose: bool, filtering: bool): - print("=====> %s" % (input_file)) - if app_client._client.firmware.device != "nanos": + if app_client._client.firmware.device == "nanos": + pytest.skip("Not supported on LNS") + else: test_path = "%s/%s" % (input_file.parent, "-".join(input_file.stem.split("-")[:-1])) conf_file = "%s.ini" % (test_path) filter_file = None @@ -74,7 +68,7 @@ def test_eip712_new(app_client: EthereumClient, input_file: Path, verbose: bool, }) assert InputData.process_file(app_client, input_file, filter_file) == True - v, r, s = app_client.eip712_sign_new(bip32) + v, r, s = app_client.eip712_sign_new(BIP32_PATH) #print("[signature]") #print("v = %s" % (v.hex())) #print("r = %s" % (r.hex())) @@ -84,6 +78,4 @@ def test_eip712_new(app_client: EthereumClient, input_file: Path, verbose: bool, assert r == bytes.fromhex(config["signature"]["r"]) assert s == bytes.fromhex(config["signature"]["s"]) else: - print("No filter file found, skipping...") - else: - print("Not supported by LNS, skipping...") + pytest.skip("No filter file found") diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png index 5b3eed9..706ef7d 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png index 61861f2..5b3eed9 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png index a58590b..61861f2 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png differ diff --git a/tests/zemu/src/blind_compound_deposit.test.js b/tests/zemu/src/blind_compound_deposit.test.js index 2f999ac..670153a 100644 --- a/tests/zemu/src/blind_compound_deposit.test.js +++ b/tests/zemu/src/blind_compound_deposit.test.js @@ -5,9 +5,9 @@ import { waitForAppScreen, zemu, nano_models } from './test.fixture'; nano_models.forEach(function(model) { test('[Nano ' + model.letter + '] Deposit ETH on compound, blind sign', zemu(model, async (sim, eth) => { let clicks; - // LNS does not have an EIP712 setting + // LNS does not have EIP712 & ENS settings if (model.letter === 'S') clicks = 3; - else clicks = 4; + else clicks = 5; // Enable blind-signing await sim.navigateAndCompareSnapshots('.', model.name + '_enable_blind_signing', [-2, 0, 0, clicks, 0]); diff --git a/tests/zemu/src/erc1155.test.js b/tests/zemu/src/erc1155.notest.js similarity index 100% rename from tests/zemu/src/erc1155.test.js rename to tests/zemu/src/erc1155.notest.js diff --git a/tests/zemu/src/erc721.test.js b/tests/zemu/src/erc721.notest.js similarity index 100% rename from tests/zemu/src/erc721.test.js rename to tests/zemu/src/erc721.notest.js diff --git a/tests/zemu/src/test.fixture.js b/tests/zemu/src/test.fixture.js index c29dbad..1a2f898 100644 --- a/tests/zemu/src/test.fixture.js +++ b/tests/zemu/src/test.fixture.js @@ -26,8 +26,8 @@ const NANOS_CLONE_ELF_PATH = Resolve("elfs/ethereum_classic_nanos.elf"); const NANOX_CLONE_ELF_PATH = Resolve("elfs/ethereum_classic_nanox.elf"); const nano_models: DeviceModel[] = [ - { name: 'nanos', letter: 'S', path: NANOS_ELF_PATH, clone_path: NANOS_CLONE_ELF_PATH }, - { name: 'nanox', letter: 'X', path: NANOX_ELF_PATH, clone_path: NANOX_CLONE_ELF_PATH } + { name: 'nanos', letter: 'S', path: NANOS_ELF_PATH, clone_path: NANOS_CLONE_ELF_PATH }/*, + { name: 'nanox', letter: 'X', path: NANOX_ELF_PATH, clone_path: NANOX_CLONE_ELF_PATH }*/ ]; const TIMEOUT = 1000000;