Merge pull request #412 from LedgerHQ/feature/apa/ens

Domain names support
This commit is contained in:
apaillier-ledger
2023-04-21 11:25:11 +02:00
committed by GitHub
70 changed files with 1436 additions and 332 deletions

View File

@@ -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"]'

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

17
src_bagl/ui_domain_name.c Normal file
View File

@@ -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

12
src_bagl/ui_domain_name.h Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

10
src_common/hash_bytes.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef HASH_BYTES_H_
#define HASH_BYTES_H_
#include <stdint.h>
#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_

View File

@@ -0,0 +1,14 @@
#ifdef HAVE_DOMAIN_NAME
#ifndef CHALLENGE_H_
#define CHALLENGE_H_
#include <stdint.h>
void roll_challenge(void);
uint32_t get_challenge(void);
void handle_get_challenge(void);
#endif // CHALLENGE_H_
#endif // HAVE_DOMAIN_NAME

View File

@@ -0,0 +1,38 @@
#ifdef HAVE_DOMAIN_NAME
#include <os.h>
#include <os_io.h>
#include <cx.h>
#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

View File

@@ -0,0 +1,710 @@
#ifdef HAVE_DOMAIN_NAME
#include <os.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#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

View File

@@ -0,0 +1,18 @@
#ifdef HAVE_DOMAIN_NAME
#ifndef DOMAIN_NAME_H_
#define DOMAIN_NAME_H_
#include <stdint.h>
#include <stdbool.h>
#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

View File

@@ -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_

View File

@@ -1,2 +1,4 @@
venv/
__pycache__/
snapshots-tmp/
elfs/

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
from enum import IntEnum, auto
class EIP712FieldType(IntEnum):
class EIP712FieldType(IntEnum):
CUSTOM = 0,
INT = auto()
UINT = auto()

View File

@@ -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")

View File

@@ -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

25
tests/ragger/app/tlv.py Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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", )

View File

@@ -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):

27
tests/ragger/keychain.py Normal file
View File

@@ -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)

View File

@@ -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-----

View File

@@ -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

View File

@@ -0,0 +1 @@
nanox/

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

View File

@@ -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

View File

@@ -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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 B

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -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]);

View File

@@ -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;