Merge pull request #412 from LedgerHQ/feature/apa/ens
Domain names support
119
.github/workflows/ci-workflow.yml
vendored
@@ -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"]'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
11
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
37
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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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_
|
||||
14
src_features/getChallenge/challenge.h
Normal 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
|
||||
38
src_features/getChallenge/cmd_get_challenge.c
Normal 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
|
||||
710
src_features/provideDomainName/cmd_provide_domain_name.c
Normal 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
|
||||
18
src_features/provideDomainName/domain_name.h
Normal 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
|
||||
@@ -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_
|
||||
2
tests/ragger/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
snapshots-tmp/
|
||||
elfs/
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import IntEnum, auto
|
||||
|
||||
class EIP712FieldType(IntEnum):
|
||||
class EIP712FieldType(IntEnum):
|
||||
CUSTOM = 0,
|
||||
INT = auto()
|
||||
UINT = auto()
|
||||
@@ -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")
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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", )
|
||||
|
||||
@@ -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
@@ -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)
|
||||
8
tests/ragger/keychain/domain_name.pem
Normal 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-----
|
||||
@@ -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
|
||||
|
||||
1
tests/ragger/snapshots/nanosp
Symbolic link
@@ -0,0 +1 @@
|
||||
nanox/
|
||||
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00000.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00001.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00002.png
Normal file
|
After Width: | Height: | Size: 585 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00003.png
Normal file
|
After Width: | Height: | Size: 383 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00004.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00005.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
tests/ragger/snapshots/nanox/domain_name_non_mainnet/00006.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00000.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00001.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00002.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00003.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00004.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_False/00005.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00000.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00001.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00002.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00003.png
Normal file
|
After Width: | Height: | Size: 585 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00004.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00005.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
tests/ragger/snapshots/nanox/domain_name_verbose_True/00006.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00000.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00001.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00002.png
Normal file
|
After Width: | Height: | Size: 588 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00003.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00004.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
tests/ragger/snapshots/nanox/domain_name_wrong_addr/00005.png
Normal file
|
After Width: | Height: | Size: 382 B |
128
tests/ragger/test_domain_name.py
Normal 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
|
||||
@@ -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")
|
||||
|
||||
|
Before Width: | Height: | Size: 758 B After Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 758 B |
|
Before Width: | Height: | Size: 382 B After Width: | Height: | Size: 305 B |
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||