From 021f6ff6c969f251a64a55cc69e9a1f5ee58dc3c Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 13:00:47 +0100 Subject: [PATCH 01/13] refactor: move the rust tests to a new folder --- tests/{ => old_to_delete}/.gitignore | 0 tests/{ => old_to_delete}/Cargo.lock | 0 tests/{ => old_to_delete}/Cargo.toml | 0 tests/{ => old_to_delete}/src/main.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => old_to_delete}/.gitignore (100%) rename tests/{ => old_to_delete}/Cargo.lock (100%) rename tests/{ => old_to_delete}/Cargo.toml (100%) rename tests/{ => old_to_delete}/src/main.rs (100%) diff --git a/tests/.gitignore b/tests/old_to_delete/.gitignore similarity index 100% rename from tests/.gitignore rename to tests/old_to_delete/.gitignore diff --git a/tests/Cargo.lock b/tests/old_to_delete/Cargo.lock similarity index 100% rename from tests/Cargo.lock rename to tests/old_to_delete/Cargo.lock diff --git a/tests/Cargo.toml b/tests/old_to_delete/Cargo.toml similarity index 100% rename from tests/Cargo.toml rename to tests/old_to_delete/Cargo.toml diff --git a/tests/src/main.rs b/tests/old_to_delete/src/main.rs similarity index 100% rename from tests/src/main.rs rename to tests/old_to_delete/src/main.rs From 0255d23b63b094a6668670d66556881458d1b035 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 13:02:46 +0100 Subject: [PATCH 02/13] test: add the ragger test structure from the latest boilerplate --- tests/application_client/__init__.py | 0 .../boilerplate_command_sender.py | 127 +++++++++++++++ .../boilerplate_response_unpacker.py | 70 +++++++++ .../boilerplate_transaction.py | 50 ++++++ tests/application_client/boilerplate_utils.py | 61 ++++++++ tests/application_client/py.typed | 0 tests/conftest.py | 14 ++ tests/requirements.txt | 4 + tests/setup.cfg | 23 +++ tests/test_app_mainmenu.py | 54 +++++++ tests/test_appname_cmd.py | 16 ++ tests/test_error_cmd.py | 59 +++++++ tests/test_name_version.py | 18 +++ tests/test_pubkey_cmd.py | 57 +++++++ tests/test_sign_cmd.py | 145 ++++++++++++++++++ tests/test_version_cmd.py | 16 ++ tests/usage.md | 78 ++++++++++ tests/utils.py | 76 +++++++++ 18 files changed, 868 insertions(+) create mode 100644 tests/application_client/__init__.py create mode 100644 tests/application_client/boilerplate_command_sender.py create mode 100644 tests/application_client/boilerplate_response_unpacker.py create mode 100644 tests/application_client/boilerplate_transaction.py create mode 100644 tests/application_client/boilerplate_utils.py create mode 100644 tests/application_client/py.typed create mode 100644 tests/conftest.py create mode 100644 tests/requirements.txt create mode 100644 tests/setup.cfg create mode 100644 tests/test_app_mainmenu.py create mode 100644 tests/test_appname_cmd.py create mode 100644 tests/test_error_cmd.py create mode 100644 tests/test_name_version.py create mode 100644 tests/test_pubkey_cmd.py create mode 100644 tests/test_sign_cmd.py create mode 100644 tests/test_version_cmd.py create mode 100644 tests/usage.md create mode 100644 tests/utils.py diff --git a/tests/application_client/__init__.py b/tests/application_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/application_client/boilerplate_command_sender.py b/tests/application_client/boilerplate_command_sender.py new file mode 100644 index 0000000..cb83ce7 --- /dev/null +++ b/tests/application_client/boilerplate_command_sender.py @@ -0,0 +1,127 @@ +from enum import IntEnum +from typing import Generator, List, Optional +from contextlib import contextmanager + +from ragger.backend.interface import BackendInterface, RAPDU +from ragger.bip import pack_derivation_path + + +MAX_APDU_LEN: int = 255 + +CLA: int = 0xE0 + +class P1(IntEnum): + # Parameter 1 for first APDU number. + P1_START = 0x00 + # Parameter 1 for maximum APDU number. + P1_MAX = 0x03 + # Parameter 1 for screen confirmation for GET_PUBLIC_KEY. + P1_CONFIRM = 0x01 + +class P2(IntEnum): + # Parameter 2 for last APDU to receive. + P2_LAST = 0x00 + # Parameter 2 for more APDU to receive. + P2_MORE = 0x80 + +class InsType(IntEnum): + GET_VERSION = 0x03 + GET_APP_NAME = 0x04 + GET_PUBLIC_KEY = 0x05 + SIGN_TX = 0x06 + +class Errors(IntEnum): + SW_DENY = 0x6985 + SW_WRONG_P1P2 = 0x6A86 + SW_WRONG_DATA_LENGTH = 0x6A87 + SW_INS_NOT_SUPPORTED = 0x6D00 + SW_CLA_NOT_SUPPORTED = 0x6E00 + SW_WRONG_RESPONSE_LENGTH = 0xB000 + SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 + SW_DISPLAY_ADDRESS_FAIL = 0xB002 + SW_DISPLAY_AMOUNT_FAIL = 0xB003 + SW_WRONG_TX_LENGTH = 0xB004 + SW_TX_PARSING_FAIL = 0xB005 + SW_TX_HASH_FAIL = 0xB006 + SW_BAD_STATE = 0xB007 + SW_SIGNATURE_FAIL = 0xB008 + + +def split_message(message: bytes, max_size: int) -> List[bytes]: + return [message[x:x + max_size] for x in range(0, len(message), max_size)] + + +class BoilerplateCommandSender: + def __init__(self, backend: BackendInterface) -> None: + self.backend = backend + + + def get_app_and_version(self) -> RAPDU: + return self.backend.exchange(cla=0xB0, # specific CLA for BOLOS + ins=0x01, # specific INS for get_app_and_version + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_version(self) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_VERSION, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_app_name(self) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_APP_NAME, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_public_key(self, path: str) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=pack_derivation_path(path)) + + + @contextmanager + def get_public_key_with_confirmation(self, path: str) -> Generator[None, None, None]: + with self.backend.exchange_async(cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=pack_derivation_path(path)) as response: + yield response + + + @contextmanager + def sign_tx(self, path: str, transaction: bytes) -> Generator[None, None, None]: + self.backend.exchange(cla=CLA, + ins=InsType.SIGN_TX, + p1=P1.P1_START, + p2=P2.P2_MORE, + data=pack_derivation_path(path)) + messages = split_message(transaction, MAX_APDU_LEN) + idx: int = P1.P1_START + 1 + + for msg in messages[:-1]: + self.backend.exchange(cla=CLA, + ins=InsType.SIGN_TX, + p1=idx, + p2=P2.P2_MORE, + data=msg) + idx += 1 + + with self.backend.exchange_async(cla=CLA, + ins=InsType.SIGN_TX, + p1=idx, + p2=P2.P2_LAST, + data=messages[-1]) as response: + yield response + + def get_async_response(self) -> Optional[RAPDU]: + return self.backend.last_async_response diff --git a/tests/application_client/boilerplate_response_unpacker.py b/tests/application_client/boilerplate_response_unpacker.py new file mode 100644 index 0000000..5d1f748 --- /dev/null +++ b/tests/application_client/boilerplate_response_unpacker.py @@ -0,0 +1,70 @@ +from typing import Tuple +from struct import unpack + +# remainder, data_len, data +def pop_sized_buf_from_buffer(buffer:bytes, size:int) -> Tuple[bytes, bytes]: + return buffer[size:], buffer[0:size] + +# remainder, data_len, data +def pop_size_prefixed_buf_from_buf(buffer:bytes) -> Tuple[bytes, int, bytes]: + data_len = buffer[0] + return buffer[1+data_len:], data_len, buffer[1:data_len+1] + +# Unpack from response: +# response = app_name (var) +def unpack_get_app_name_response(response: bytes) -> str: + return response.decode("ascii") + +# Unpack from response: +# response = MAJOR (1) +# MINOR (1) +# PATCH (1) +def unpack_get_version_response(response: bytes) -> Tuple[int, int, int]: + assert len(response) == 3 + major, minor, patch = unpack("BBB", response) + return (major, minor, patch) + +# Unpack from response: +# response = format_id (1) +# app_name_raw_len (1) +# app_name_raw (var) +# version_raw_len (1) +# version_raw (var) +# unused_len (1) +# unused (var) +def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: + response, _ = pop_sized_buf_from_buffer(response, 1) + response, _, app_name_raw = pop_size_prefixed_buf_from_buf(response) + response, _, version_raw = pop_size_prefixed_buf_from_buf(response) + response, _, _ = pop_size_prefixed_buf_from_buf(response) + + assert len(response) == 0 + + return app_name_raw.decode("ascii"), version_raw.decode("ascii") + +# Unpack from response: +# response = pub_key_len (1) +# pub_key (var) +# chain_code_len (1) +# chain_code (var) +def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: + response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) + response, chain_code_len, chain_code = pop_size_prefixed_buf_from_buf(response) + + assert pub_key_len == 65 + assert chain_code_len == 32 + assert len(response) == 0 + + return pub_key_len, pub_key, chain_code_len, chain_code + +# Unpack from response: +# response = der_sig_len (1) +# der_sig (var) +# v (1) +def unpack_sign_tx_response(response: bytes) -> Tuple[int, bytes, int]: + response, der_sig_len, der_sig = pop_size_prefixed_buf_from_buf(response) + response, v = pop_sized_buf_from_buffer(response, 1) + + assert len(response) == 0 + + return der_sig_len, der_sig, int.from_bytes(v, byteorder='big') diff --git a/tests/application_client/boilerplate_transaction.py b/tests/application_client/boilerplate_transaction.py new file mode 100644 index 0000000..19f0625 --- /dev/null +++ b/tests/application_client/boilerplate_transaction.py @@ -0,0 +1,50 @@ +from io import BytesIO +from typing import Union + +from .boilerplate_utils import read, read_uint, read_varint, write_varint, UINT64_MAX + + +class TransactionError(Exception): + pass + + +class Transaction: + def __init__(self, + nonce: int, + to: Union[str, bytes], + value: int, + memo: str) -> None: + self.nonce: int = nonce + self.to: bytes = bytes.fromhex(to[2:]) if isinstance(to, str) else to + self.value: int = value + self.memo: bytes = memo.encode("ascii") + + if not 0 <= self.nonce <= UINT64_MAX: + raise TransactionError(f"Bad nonce: '{self.nonce}'!") + + if not 0 <= self.value <= UINT64_MAX: + raise TransactionError(f"Bad value: '{self.value}'!") + + if len(self.to) != 20: + raise TransactionError(f"Bad address: '{self.to.hex()}'!") + + def serialize(self) -> bytes: + return b"".join([ + self.nonce.to_bytes(8, byteorder="big"), + self.to, + self.value.to_bytes(8, byteorder="big"), + write_varint(len(self.memo)), + self.memo + ]) + + @classmethod + def from_bytes(cls, hexa: Union[bytes, BytesIO]): + buf: BytesIO = BytesIO(hexa) if isinstance(hexa, bytes) else hexa + + nonce: int = read_uint(buf, 64, byteorder="big") + to: bytes = read(buf, 20) + value: int = read_uint(buf, 64, byteorder="big") + memo_len: int = read_varint(buf) + memo: str = read(buf, memo_len).decode("ascii") + + return cls(nonce=nonce, to=to, value=value, memo=memo) diff --git a/tests/application_client/boilerplate_utils.py b/tests/application_client/boilerplate_utils.py new file mode 100644 index 0000000..fd96e62 --- /dev/null +++ b/tests/application_client/boilerplate_utils.py @@ -0,0 +1,61 @@ +from io import BytesIO +from typing import Optional, Literal + + +UINT64_MAX: int = 2**64-1 +UINT32_MAX: int = 2**32-1 +UINT16_MAX: int = 2**16-1 + + +def write_varint(n: int) -> bytes: + if n < 0xFC: + return n.to_bytes(1, byteorder="little") + + if n <= UINT16_MAX: + return b"\xFD" + n.to_bytes(2, byteorder="little") + + if n <= UINT32_MAX: + return b"\xFE" + n.to_bytes(4, byteorder="little") + + if n <= UINT64_MAX: + return b"\xFF" + n.to_bytes(8, byteorder="little") + + raise ValueError(f"Can't write to varint: '{n}'!") + + +def read_varint(buf: BytesIO, + prefix: Optional[bytes] = None) -> int: + b: bytes = prefix if prefix else buf.read(1) + + if not b: + raise ValueError(f"Can't read prefix: '{b.hex()}'!") + + n: int = {b"\xfd": 2, b"\xfe": 4, b"\xff": 8}.get(b, 1) # default to 1 + + b = buf.read(n) if n > 1 else b + + if len(b) != n: + raise ValueError("Can't read varint!") + + return int.from_bytes(b, byteorder="little") + + +def read(buf: BytesIO, size: int) -> bytes: + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read {size} bytes in buffer!") + + return b + + +def read_uint(buf: BytesIO, + bit_len: int, + byteorder: Literal['big', 'little'] = 'little') -> int: + size: int = bit_len // 8 + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read u{bit_len} in buffer!") + + return int.from_bytes(b, byteorder) diff --git a/tests/application_client/py.typed b/tests/application_client/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a9ea72c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ + +########################### +### CONFIGURATION START ### +########################### + +# You can configure optional parameters by overriding the value of ragger.configuration.OPTIONAL_CONFIGURATION +# Please refer to ragger/conftest/configuration.py for their descriptions and accepted values + +######################### +### CONFIGURATION END ### +######################### + +# Pull all features from the base ragger conftest using the overridden configuration +pytest_plugins = ("ragger.conftest.base_conftest", ) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..dd07dfb --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest +ragger[speculos,ledgerwallet]>=1.11.4 +ecdsa>=0.16.1,<0.17.0 +pycryptodome diff --git a/tests/setup.cfg b/tests/setup.cfg new file mode 100644 index 0000000..2398a52 --- /dev/null +++ b/tests/setup.cfg @@ -0,0 +1,23 @@ +[tool:pytest] +addopts = --strict-markers + +[pylint] +disable = C0114, # missing-module-docstring + C0115, # missing-class-docstring + C0116, # missing-function-docstring + C0103, # invalid-name + R0801, # duplicate-code + R0913, # too-many-arguments + R0917 # too-many-positional-arguments + +max-line-length=120 +extension-pkg-whitelist=hid + +[pycodestyle] +max-line-length = 100 + +[mypy-hid.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/tests/test_app_mainmenu.py b/tests/test_app_mainmenu.py new file mode 100644 index 0000000..d5ee01f --- /dev/null +++ b/tests/test_app_mainmenu.py @@ -0,0 +1,54 @@ +from ragger.firmware import Firmware +from ragger.navigator import Navigator, NavInsID, NavIns + + +# In this test we check the behavior of the device main menu +def test_app_mainmenu(firmware: Firmware, + navigator: Navigator, + test_name: str, + default_screenshot_path: str) -> None: + # Navigate in the main menu + instructions = [] + if firmware.is_nano: + instructions += [ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + ] + elif firmware is Firmware.STAX: + instructions += [ + NavInsID.USE_CASE_HOME_SETTINGS, + NavIns(NavInsID.TOUCH, (200, 113)), + NavIns(NavInsID.TOUCH, (200, 261)), + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavIns(NavInsID.TOUCH, (200, 261)), + NavInsID.USE_CASE_SETTINGS_NEXT, + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT + ] + elif firmware is Firmware.FLEX: + instructions += [ + NavInsID.USE_CASE_HOME_SETTINGS, + NavIns(NavInsID.TOUCH, (200, 113)), + NavIns(NavInsID.TOUCH, (200, 300)), + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavIns(NavInsID.TOUCH, (200, 300)), + NavInsID.USE_CASE_SETTINGS_NEXT, + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT + ] + + assert len(instructions) > 0 + navigator.navigate_and_compare(default_screenshot_path, test_name, instructions, + screen_change_before_first_instruction=False) diff --git a/tests/test_appname_cmd.py b/tests/test_appname_cmd.py new file mode 100644 index 0000000..f1eefb8 --- /dev/null +++ b/tests/test_appname_cmd.py @@ -0,0 +1,16 @@ +from ragger.backend.interface import BackendInterface + +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_app_name_response + +from utils import verify_name + + +# In this test we check that the GET_APP_NAME replies the application name +def test_app_name(backend: BackendInterface) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the GET_APP_NAME instruction to the app + response = client.get_app_name() + # Assert that we have received the correct appname + verify_name(unpack_get_app_name_response(response.data)) diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py new file mode 100644 index 0000000..3d89a57 --- /dev/null +++ b/tests/test_error_cmd.py @@ -0,0 +1,59 @@ +import pytest + +from ragger.error import ExceptionRAPDU +from ragger.backend.interface import BackendInterface + +from application_client.boilerplate_command_sender import CLA, InsType, P1, P2, Errors + + +# Ensure the app returns an error when a bad CLA is used +def test_bad_cla(backend: BackendInterface) -> None: + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA + 1, ins=InsType.GET_VERSION) + assert e.value.status == Errors.SW_CLA_NOT_SUPPORTED + + +# Ensure the app returns an error when a bad INS is used +def test_bad_ins(backend: BackendInterface) -> None: + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=0xff) + assert e.value.status == Errors.SW_INS_NOT_SUPPORTED + + +# Ensure the app returns an error when a bad P1 or P2 is used +def test_wrong_p1p2(backend: BackendInterface) -> None: + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START + 1, p2=P2.P2_LAST) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_MORE) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START + 1, p2=P2.P2_LAST) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START, p2=P2.P2_MORE) + assert e.value.status == Errors.SW_WRONG_P1P2 + + +# Ensure the app returns an error when a bad data length is used +def test_wrong_data_length(backend: BackendInterface) -> None: + # APDUs must be at least 4 bytes: CLA, INS, P1, P2. + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange_raw(bytes.fromhex("E00300")) + assert e.value.status == Errors.SW_WRONG_DATA_LENGTH + # APDUs advertises a too long length + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange_raw(bytes.fromhex("E003000005")) + assert e.value.status == Errors.SW_WRONG_DATA_LENGTH + + +# Ensure there is no state confusion when trying wrong APDU sequences +def test_invalid_state(backend: BackendInterface) -> None: + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, + ins=InsType.SIGN_TX, + p1=P1.P1_START + 1, # Try to continue a flow instead of start a new one + p2=P2.P2_MORE, + data=b"abcde") # data is not parsed in this case + assert e.value.status == Errors.SW_BAD_STATE diff --git a/tests/test_name_version.py b/tests/test_name_version.py new file mode 100644 index 0000000..68e9c5e --- /dev/null +++ b/tests/test_name_version.py @@ -0,0 +1,18 @@ +from ragger.backend.interface import BackendInterface + +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_app_and_version_response + +from utils import verify_version, verify_name + +# Test a specific APDU asking BOLOS (and not the app) the name and version of the current app +def test_get_app_and_version(backend: BackendInterface) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the special instruction to BOLOS + response = client.get_app_and_version() + # Use an helper to parse the response, assert the values + app_name, version = unpack_get_app_and_version_response(response.data) + + verify_name(app_name) + verify_version(version) diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py new file mode 100644 index 0000000..92966db --- /dev/null +++ b/tests/test_pubkey_cmd.py @@ -0,0 +1,57 @@ +import pytest + +from ragger.bip import calculate_public_key_and_chaincode, CurveChoice +from ragger.error import ExceptionRAPDU +from ragger.backend.interface import BackendInterface +from ragger.navigator.navigation_scenario import NavigateWithScenario + +from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors +from application_client.boilerplate_response_unpacker import unpack_get_public_key_response + + +# In this test we check that the GET_PUBLIC_KEY works in non-confirmation mode +def test_get_public_key_no_confirm(backend: BackendInterface) -> None: + path_list = [ + "m/44'/1'/0'/0/0", + "m/44'/1'/0/0/0", + "m/44'/1'/911'/0/0", + "m/44'/1'/255/255/255", + "m/44'/1'/2147483647/0/0/0/0/0/0/0" + ] + for path in path_list: + client = BoilerplateCommandSender(backend) + response = client.get_public_key(path=path).data + _, public_key, _, chain_code = unpack_get_public_key_response(response) + + ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert public_key.hex() == ref_public_key + assert chain_code.hex() == ref_chain_code + + +# In this test we check that the GET_PUBLIC_KEY works in confirmation mode +def test_get_public_key_confirm_accepted(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + with client.get_public_key_with_confirmation(path=path): + scenario_navigator.address_review_approve() + + response = client.get_async_response().data + _, public_key, _, chain_code = unpack_get_public_key_response(response) + + ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert public_key.hex() == ref_public_key + assert chain_code.hex() == ref_chain_code + + +# In this test we check that the GET_PUBLIC_KEY in confirmation mode replies an error if the user refuses +def test_get_public_key_confirm_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + + with pytest.raises(ExceptionRAPDU) as e: + with client.get_public_key_with_confirmation(path=path): + scenario_navigator.address_review_reject() + + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py new file mode 100644 index 0000000..81ed8eb --- /dev/null +++ b/tests/test_sign_cmd.py @@ -0,0 +1,145 @@ +import pytest + +from ragger.backend.interface import BackendInterface +from ragger.error import ExceptionRAPDU +from ragger.firmware import Firmware +from ragger.navigator import Navigator, NavInsID +from ragger.navigator.navigation_scenario import NavigateWithScenario + +from application_client.boilerplate_transaction import Transaction +from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors +from application_client.boilerplate_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response +from utils import check_signature_validity + +# In this tests we check the behavior of the device when asked to sign a transaction + + +# In this test we send to the device a transaction to sign and validate it on screen +# The transaction is short and will be sent in one chunk +# We will ensure that the displayed information is correct by using screenshots comparison +def test_sign_tx_short_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # The path used for this entire test + path: str = "m/44'/1'/0'/0/0" + + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + # Create the transaction that will be sent to the device for signing + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo="For u EthDev" + ).serialize() + + # Send the sign device instruction. + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_tx(path=path, transaction=transaction): + # Validate the on-screen request by performing the navigation appropriate for this device + scenario_navigator.review_approve() + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + assert check_signature_validity(public_key, der_sig, transaction) + + +# In this test we send to the device a transaction to trig a blind-signing flow +# The transaction is short and will be sent in one chunk +# We will ensure that the displayed information is correct by using screenshots comparison +def test_sign_tx_short_tx_blind_sign(firmware: Firmware, + backend: BackendInterface, + navigator: Navigator, + scenario_navigator: NavigateWithScenario, + test_name: str, + default_screenshot_path: str) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # The path used for this entire test + path: str = "m/44'/1'/0'/0/0" + + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + # Create the transaction that will be sent to the device for signing + transaction = Transaction( + nonce=1, + to="0x0000000000000000000000000000000000000000", + value=0, + memo="Blind-sign" + ).serialize() + + # Send the sign device instruction. + valid_instruction = [NavInsID.RIGHT_CLICK] if firmware.is_nano else [NavInsID.USE_CASE_CHOICE_REJECT] + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_tx(path=path, transaction=transaction): + navigator.navigate_and_compare(default_screenshot_path, + test_name+"/part1", + valid_instruction, + screen_change_after_last_instruction=False) + + # Validate the on-screen request by performing the navigation appropriate for this device + scenario_navigator.review_approve() + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + assert check_signature_validity(public_key, der_sig, transaction) + +# In this test se send to the device a transaction to sign and validate it on screen +# This test is mostly the same as the previous one but with different values. +# In particular the long memo will force the transaction to be sent in multiple chunks +def test_sign_tx_long_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + path: str = "m/44'/1'/0'/0/0" + + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo=("This is a very long memo. " + "It will force the app client to send the serialized transaction to be sent in chunk. " + "As the maximum chunk size is 255 bytes we will make this memo greater than 255 characters. " + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, " + "dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam.") + ).serialize() + + with client.sign_tx(path=path, transaction=transaction): + scenario_navigator.review_approve() + + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + assert check_signature_validity(public_key, der_sig, transaction) + + +# Transaction signature refused test +# The test will ask for a transaction signature that will be refused on screen +def test_sign_tx_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + path: str = "m/44'/1'/0'/0/0" + + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo="This transaction will be refused by the user" + ).serialize() + + with pytest.raises(ExceptionRAPDU) as e: + with client.sign_tx(path=path, transaction=transaction): + scenario_navigator.review_reject() + + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 diff --git a/tests/test_version_cmd.py b/tests/test_version_cmd.py new file mode 100644 index 0000000..0f1d4be --- /dev/null +++ b/tests/test_version_cmd.py @@ -0,0 +1,16 @@ +from ragger.backend.interface import BackendInterface + +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_version_response + +from utils import verify_version + +# In this test we check the behavior of the device when asked to provide the app version +def test_version(backend: BackendInterface) -> None: + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the GET_VERSION instruction + rapdu = client.get_version() + # Use an helper to parse the response, assert the values + MAJOR, MINOR, PATCH = unpack_get_version_response(rapdu.data) + verify_version(f"{MAJOR}.{MINOR}.{PATCH}") diff --git a/tests/usage.md b/tests/usage.md new file mode 100644 index 0000000..dc2fbfa --- /dev/null +++ b/tests/usage.md @@ -0,0 +1,78 @@ +# How to use the Ragger test framework + +This framework allows testing the application on the Speculos emulator or on a real device using LedgerComm or LedgerWallet + +## Quickly get started with Ragger and Speculos + +### Install ragger and dependencies + +```shell +pip install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt +sudo apt-get update && sudo apt-get install qemu-user-static +``` + +### Compile the application + +The application to test must be compiled for all required devices. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: + +```shell +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK # replace with one of [NANOX, NANOSP, STAX, FLEX] +exit +``` + +### Run a simple test using the Speculos emulator + +You can use the following command to get your first experience with Ragger and Speculos + +```shell +pytest -v --tb=short --device nanox --display +``` + +Or you can refer to the section `Available pytest options` to configure the options you want to use + +### Run a simple test using a real device + +The application to test must be loaded and started on a Ledger device plugged in USB. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: + +```shell +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd app-/ # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK load # replace with one of [NANOX, NANOSP, STAX, FLEX] +exit +``` + +You can use the following command to get your first experience with Ragger and Ledgerwallet on a NANOX. +Make sure that the device is plugged, unlocked, and that the tested application is open. + +```shell +pytest -v --tb=short --device nanox --backend ledgerwallet +``` + +Or you can refer to the section `Available pytest options` to configure the options you want to use + +## Available pytest options + +Standard useful pytest options + +```shell + -v formats the test summary in a readable way + -s enable logs for successful tests, on Speculos it will enable app logs if compiled with DEBUG=1 + -k only run the tests that contain in their names + --tb=short in case of errors, formats the test traceback in a readable way +``` + +Custom pytest options + +```shell + --device run the test on the specified device [nanox,nanosp,stax,flex,all]. This parameter is mandatory + --backend run the tests against the backend [speculos, ledgercomm, ledgerwallet]. Speculos is the default + --display on Speculos, enables the display of the app screen using QT + --golden_run on Speculos, screen comparison functions will save the current screen instead of comparing + --log_apdu_file log all apdu exchanges to the file in parameter. The previous file content is erased +``` diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..b342855 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,76 @@ +from pathlib import Path +from typing import List +import re +from Crypto.Hash import keccak + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + + +# Check if a signature of a given message is valid +def check_signature_validity(public_key: bytes, signature: bytes, message: bytes) -> bool: + pk: VerifyingKey = VerifyingKey.from_string( + public_key, + curve=SECP256k1, + hashfunc=None + ) + # Compute message hash (keccak_256) + k = keccak.new(digest_bits=256) + k.update(message) + message_hash = k.digest() + + return pk.verify_digest(signature=signature, + digest=message_hash, + sigdecode=sigdecode_der) + + +def verify_name(name: str) -> None: + """Verify the app name, based on defines in Makefile + + Args: + name (str): Name to be checked + """ + + name_str = "" + lines = _read_makefile() + name_re = re.compile(r"^APPNAME\s?=\s?\"?(?P\w+)\"?", re.I) + for line in lines: + info = name_re.match(line) + if info: + dinfo = info.groupdict() + name_str = dinfo["val"] + assert name == name_str + + +def verify_version(version: str) -> None: + """Verify the app version, based on defines in Makefile + + Args: + Version (str): Version to be checked + """ + + vers_dict = {} + vers_str = "" + lines = _read_makefile() + version_re = re.compile(r"^APPVERSION_(?P\w)\s?=\s?(?P\d*)", re.I) + for line in lines: + info = version_re.match(line) + if info: + dinfo = info.groupdict() + vers_dict[dinfo["part"]] = dinfo["val"] + try: + vers_str = f"{vers_dict['M']}.{vers_dict['N']}.{vers_dict['P']}" + except KeyError: + pass + assert version == vers_str + + +def _read_makefile() -> List[str]: + """Read lines from the parent Makefile """ + + parent = Path(__file__).parent.parent.resolve() + makefile = f"{parent}/Makefile" + with open(makefile, "r", encoding="utf-8") as f_p: + lines = f_p.readlines() + return lines From 81281227a658dbbc293b42f0c181c27789cc2dc6 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 13:20:45 +0100 Subject: [PATCH 03/13] refactor: update instruction types and error codes to match with the current app --- .../boilerplate_command_sender.py | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/tests/application_client/boilerplate_command_sender.py b/tests/application_client/boilerplate_command_sender.py index cb83ce7..84c3ca3 100644 --- a/tests/application_client/boilerplate_command_sender.py +++ b/tests/application_client/boilerplate_command_sender.py @@ -25,26 +25,46 @@ class P2(IntEnum): P2_MORE = 0x80 class InsType(IntEnum): - GET_VERSION = 0x03 - GET_APP_NAME = 0x04 - GET_PUBLIC_KEY = 0x05 - SIGN_TX = 0x06 + GET_APP_CONFIGURATION = 0x01 + GET_PUBLIC_KEY = 0x02 + SIGN_MESSAGE = 0x03 + GET_ADDRESS = 0X04 + SIGN_TRANSACTION = 0x05 class Errors(IntEnum): - SW_DENY = 0x6985 - SW_WRONG_P1P2 = 0x6A86 - SW_WRONG_DATA_LENGTH = 0x6A87 - SW_INS_NOT_SUPPORTED = 0x6D00 - SW_CLA_NOT_SUPPORTED = 0x6E00 - SW_WRONG_RESPONSE_LENGTH = 0xB000 - SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 - SW_DISPLAY_ADDRESS_FAIL = 0xB002 - SW_DISPLAY_AMOUNT_FAIL = 0xB003 - SW_WRONG_TX_LENGTH = 0xB004 - SW_TX_PARSING_FAIL = 0xB005 - SW_TX_HASH_FAIL = 0xB006 - SW_BAD_STATE = 0xB007 - SW_SIGNATURE_FAIL = 0xB008 + SW_INVALID_DATA = 0x6B00, + SW_CELL_UNDERFLOW = 0x6B01, + SW_RANGE_CHECK_FAIL = 0x6B02, + SW_WRONG_LABEL = 0x6B03, + SW_INVALID_FLAG = 0x6B04, + SW_END_OF_STREAM = 0x6B05, + SW_SLICE_IS_EMPTY = 0x6B06, + SW_INVALID_KEY = 0x6B07, + SW_CELL_IS_EMPTY = 0x6B08, + SW_INVALID_HASH = 0x6B09, + SW_INVALID_CELL_INDEX = 0x6B10, + SW_INVALID_REQUEST = 0x6B11, + SW_INVALID_FUNCTION_ID = 0x6B12, + SW_INVALID_SRC_ADDRESS = 0x6B13, + SW_INVALID_WALLET_ID = 0x6B14, + SW_INVALID_WALLET_TYPE = 0x6B15, + SW_INVALID_TICKER_LENGTH = 0x6B16 + + # Status Word from boilerplate app + # SW_DENY = 0x6985 + # SW_WRONG_P1P2 = 0x6A86 + # SW_WRONG_DATA_LENGTH = 0x6A87 + # SW_INS_NOT_SUPPORTED = 0x6D00 + # SW_CLA_NOT_SUPPORTED = 0x6E00 + # SW_WRONG_RESPONSE_LENGTH = 0xB000 + # SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 + # SW_DISPLAY_ADDRESS_FAIL = 0xB002 + # SW_DISPLAY_AMOUNT_FAIL = 0xB003 + # SW_WRONG_TX_LENGTH = 0xB004 + # SW_TX_PARSING_FAIL = 0xB005 + # SW_TX_HASH_FAIL = 0xB006 + # SW_BAD_STATE = 0xB007 + # SW_SIGNATURE_FAIL = 0xB008 def split_message(message: bytes, max_size: int) -> List[bytes]: From 9d16f284d3ebb083beda802b7643d6f9d7f76b43 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 13:31:30 +0100 Subject: [PATCH 04/13] refactor: rename all instances of 'boilerplate' to 'everscale' --- ...mmand_sender.py => everscale_command_sender.py} | 4 ++-- ..._unpacker.py => everscale_response_unpacker.py} | 0 ...ate_transaction.py => everscale_transaction.py} | 2 +- .../{boilerplate_utils.py => everscale_utils.py} | 0 tests/test_appname_cmd.py | 6 +++--- tests/test_error_cmd.py | 2 +- ...t_version_cmd.py => test_get_app_config_cmd.py} | 10 +++++----- tests/test_name_version.py | 6 +++--- tests/test_pubkey_cmd.py | 10 +++++----- tests/test_sign_cmd.py | 14 +++++++------- 10 files changed, 27 insertions(+), 27 deletions(-) rename tests/application_client/{boilerplate_command_sender.py => everscale_command_sender.py} (98%) rename tests/application_client/{boilerplate_response_unpacker.py => everscale_response_unpacker.py} (100%) rename tests/application_client/{boilerplate_transaction.py => everscale_transaction.py} (94%) rename tests/application_client/{boilerplate_utils.py => everscale_utils.py} (100%) rename tests/{test_version_cmd.py => test_get_app_config_cmd.py} (59%) diff --git a/tests/application_client/boilerplate_command_sender.py b/tests/application_client/everscale_command_sender.py similarity index 98% rename from tests/application_client/boilerplate_command_sender.py rename to tests/application_client/everscale_command_sender.py index 84c3ca3..73093b5 100644 --- a/tests/application_client/boilerplate_command_sender.py +++ b/tests/application_client/everscale_command_sender.py @@ -50,7 +50,7 @@ class Errors(IntEnum): SW_INVALID_WALLET_TYPE = 0x6B15, SW_INVALID_TICKER_LENGTH = 0x6B16 - # Status Word from boilerplate app + # Status Word from everscale app # SW_DENY = 0x6985 # SW_WRONG_P1P2 = 0x6A86 # SW_WRONG_DATA_LENGTH = 0x6A87 @@ -71,7 +71,7 @@ def split_message(message: bytes, max_size: int) -> List[bytes]: return [message[x:x + max_size] for x in range(0, len(message), max_size)] -class BoilerplateCommandSender: +class EverscaleCommandSender: def __init__(self, backend: BackendInterface) -> None: self.backend = backend diff --git a/tests/application_client/boilerplate_response_unpacker.py b/tests/application_client/everscale_response_unpacker.py similarity index 100% rename from tests/application_client/boilerplate_response_unpacker.py rename to tests/application_client/everscale_response_unpacker.py diff --git a/tests/application_client/boilerplate_transaction.py b/tests/application_client/everscale_transaction.py similarity index 94% rename from tests/application_client/boilerplate_transaction.py rename to tests/application_client/everscale_transaction.py index 19f0625..9ef3d17 100644 --- a/tests/application_client/boilerplate_transaction.py +++ b/tests/application_client/everscale_transaction.py @@ -1,7 +1,7 @@ from io import BytesIO from typing import Union -from .boilerplate_utils import read, read_uint, read_varint, write_varint, UINT64_MAX +from .everscale_utils import read, read_uint, read_varint, write_varint, UINT64_MAX class TransactionError(Exception): diff --git a/tests/application_client/boilerplate_utils.py b/tests/application_client/everscale_utils.py similarity index 100% rename from tests/application_client/boilerplate_utils.py rename to tests/application_client/everscale_utils.py diff --git a/tests/test_appname_cmd.py b/tests/test_appname_cmd.py index f1eefb8..277c58a 100644 --- a/tests/test_appname_cmd.py +++ b/tests/test_appname_cmd.py @@ -1,7 +1,7 @@ from ragger.backend.interface import BackendInterface -from application_client.boilerplate_command_sender import BoilerplateCommandSender -from application_client.boilerplate_response_unpacker import unpack_get_app_name_response +from application_client.everscale_command_sender import EverscaleCommandSender +from application_client.everscale_response_unpacker import unpack_get_app_name_response from utils import verify_name @@ -9,7 +9,7 @@ # In this test we check that the GET_APP_NAME replies the application name def test_app_name(backend: BackendInterface) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) # Send the GET_APP_NAME instruction to the app response = client.get_app_name() # Assert that we have received the correct appname diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py index 3d89a57..12e3166 100644 --- a/tests/test_error_cmd.py +++ b/tests/test_error_cmd.py @@ -3,7 +3,7 @@ from ragger.error import ExceptionRAPDU from ragger.backend.interface import BackendInterface -from application_client.boilerplate_command_sender import CLA, InsType, P1, P2, Errors +from application_client.everscale_command_sender import CLA, InsType, P1, P2, Errors # Ensure the app returns an error when a bad CLA is used diff --git a/tests/test_version_cmd.py b/tests/test_get_app_config_cmd.py similarity index 59% rename from tests/test_version_cmd.py rename to tests/test_get_app_config_cmd.py index 0f1d4be..733d1ee 100644 --- a/tests/test_version_cmd.py +++ b/tests/test_get_app_config_cmd.py @@ -1,16 +1,16 @@ from ragger.backend.interface import BackendInterface -from application_client.boilerplate_command_sender import BoilerplateCommandSender -from application_client.boilerplate_response_unpacker import unpack_get_version_response +from application_client.everscale_command_sender import EverscaleCommandSender +from application_client.everscale_response_unpacker import unpack_get_version_response from utils import verify_version # In this test we check the behavior of the device when asked to provide the app version -def test_version(backend: BackendInterface) -> None: +def test_get_app_config(backend: BackendInterface) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) # Send the GET_VERSION instruction - rapdu = client.get_version() + rapdu = client.get_app_config() # Use an helper to parse the response, assert the values MAJOR, MINOR, PATCH = unpack_get_version_response(rapdu.data) verify_version(f"{MAJOR}.{MINOR}.{PATCH}") diff --git a/tests/test_name_version.py b/tests/test_name_version.py index 68e9c5e..ce3e067 100644 --- a/tests/test_name_version.py +++ b/tests/test_name_version.py @@ -1,14 +1,14 @@ from ragger.backend.interface import BackendInterface -from application_client.boilerplate_command_sender import BoilerplateCommandSender -from application_client.boilerplate_response_unpacker import unpack_get_app_and_version_response +from application_client.everscale_command_sender import EverscaleCommandSender +from application_client.everscale_response_unpacker import unpack_get_app_and_version_response from utils import verify_version, verify_name # Test a specific APDU asking BOLOS (and not the app) the name and version of the current app def test_get_app_and_version(backend: BackendInterface) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) # Send the special instruction to BOLOS response = client.get_app_and_version() # Use an helper to parse the response, assert the values diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py index 92966db..de3fade 100644 --- a/tests/test_pubkey_cmd.py +++ b/tests/test_pubkey_cmd.py @@ -5,8 +5,8 @@ from ragger.backend.interface import BackendInterface from ragger.navigator.navigation_scenario import NavigateWithScenario -from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors -from application_client.boilerplate_response_unpacker import unpack_get_public_key_response +from application_client.everscale_command_sender import EverscaleCommandSender, Errors +from application_client.everscale_response_unpacker import unpack_get_public_key_response # In this test we check that the GET_PUBLIC_KEY works in non-confirmation mode @@ -19,7 +19,7 @@ def test_get_public_key_no_confirm(backend: BackendInterface) -> None: "m/44'/1'/2147483647/0/0/0/0/0/0/0" ] for path in path_list: - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) response = client.get_public_key(path=path).data _, public_key, _, chain_code = unpack_get_public_key_response(response) @@ -30,7 +30,7 @@ def test_get_public_key_no_confirm(backend: BackendInterface) -> None: # In this test we check that the GET_PUBLIC_KEY works in confirmation mode def test_get_public_key_confirm_accepted(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) path = "m/44'/1'/0'/0/0" with client.get_public_key_with_confirmation(path=path): scenario_navigator.address_review_approve() @@ -45,7 +45,7 @@ def test_get_public_key_confirm_accepted(backend: BackendInterface, scenario_nav # In this test we check that the GET_PUBLIC_KEY in confirmation mode replies an error if the user refuses def test_get_public_key_confirm_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) path = "m/44'/1'/0'/0/0" with pytest.raises(ExceptionRAPDU) as e: diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py index 81ed8eb..ebc3127 100644 --- a/tests/test_sign_cmd.py +++ b/tests/test_sign_cmd.py @@ -6,9 +6,9 @@ from ragger.navigator import Navigator, NavInsID from ragger.navigator.navigation_scenario import NavigateWithScenario -from application_client.boilerplate_transaction import Transaction -from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors -from application_client.boilerplate_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response +from application_client.everscale_transaction import Transaction +from application_client.everscale_command_sender import EverscaleCommandSender, Errors +from application_client.everscale_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response from utils import check_signature_validity # In this tests we check the behavior of the device when asked to sign a transaction @@ -19,7 +19,7 @@ # We will ensure that the displayed information is correct by using screenshots comparison def test_sign_tx_short_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) # The path used for this entire test path: str = "m/44'/1'/0'/0/0" @@ -58,7 +58,7 @@ def test_sign_tx_short_tx_blind_sign(firmware: Firmware, test_name: str, default_screenshot_path: str) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) # The path used for this entire test path: str = "m/44'/1'/0'/0/0" @@ -97,7 +97,7 @@ def test_sign_tx_short_tx_blind_sign(firmware: Firmware, # In particular the long memo will force the transaction to be sent in multiple chunks def test_sign_tx_long_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) path: str = "m/44'/1'/0'/0/0" rapdu = client.get_public_key(path=path) @@ -126,7 +126,7 @@ def test_sign_tx_long_tx(backend: BackendInterface, scenario_navigator: Navigate # The test will ask for a transaction signature that will be refused on screen def test_sign_tx_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: # Use the app interface instead of raw interface - client = BoilerplateCommandSender(backend) + client = EverscaleCommandSender(backend) path: str = "m/44'/1'/0'/0/0" transaction = Transaction( From dd8b75c5491688624d4771ae2c637b7be67f727a Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 12:19:11 +0100 Subject: [PATCH 05/13] ops: add the ci workflow from the boilerplate app --- .../workflows/build_and_functional_tests.yml | 43 +++++++++++++ .github/workflows/cflite_cron.yml | 40 ++++++++++++ .github/workflows/cflite_pr.yml | 43 +++++++++++++ .github/workflows/codeql_checks.yml | 45 ++++++++++++++ .github/workflows/coding_style_checks.yml | 25 ++++++++ .../workflows/documentation_generation.yml | 29 +++++++++ .github/workflows/misspellings_checks.yml | 28 +++++++++ .github/workflows/python_client_checks.yml | 41 +++++++++++++ .github/workflows/unit_tests.yml | 61 +++++++++++++++++++ 9 files changed, 355 insertions(+) create mode 100644 .github/workflows/build_and_functional_tests.yml create mode 100644 .github/workflows/cflite_cron.yml create mode 100644 .github/workflows/cflite_pr.yml create mode 100644 .github/workflows/codeql_checks.yml create mode 100644 .github/workflows/coding_style_checks.yml create mode 100644 .github/workflows/documentation_generation.yml create mode 100644 .github/workflows/misspellings_checks.yml create mode 100644 .github/workflows/python_client_checks.yml create mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_and_functional_tests.yml new file mode 100644 index 0000000..696f27c --- /dev/null +++ b/.github/workflows/build_and_functional_tests.yml @@ -0,0 +1,43 @@ +name: Build and run functional tests using ragger through reusable workflow + +# This workflow will build the app and then run functional tests using the Ragger framework upon Speculos emulation. +# It calls a reusable workflow developed by Ledger's internal developer team to build the application and upload the +# resulting binaries. +# It then calls another reusable workflow to run the Ragger tests on the compiled application binary. +# +# The build part of this workflow is mandatory, this ensures that the app will be deployable in the Ledger App Store. +# While the test part of this workflow is optional, having functional testing on your application is mandatory and this workflow and +# tooling environment is meant to be easy to use and adapt after forking your application + +on: + workflow_dispatch: + inputs: + golden_run: + type: choice + required: true + default: 'Raise an error (default)' + description: CI behavior if the test snapshots are different than expected. + options: + - 'Raise an error (default)' + - 'Open a PR' + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + build_application: + name: Build application using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 + with: + upload_app_binaries_artifact: "compiled_app_binaries" + + ragger_tests: + name: Run ragger tests using the reusable workflow + needs: build_application + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 + with: + download_app_binaries_artifact: "compiled_app_binaries" + regenerate_snapshots: ${{ inputs.golden_run == 'Open a PR' }} diff --git a/.github/workflows/cflite_cron.yml b/.github/workflows/cflite_cron.yml new file mode 100644 index 0000000..17c1e65 --- /dev/null +++ b/.github/workflows/cflite_cron.yml @@ -0,0 +1,40 @@ +name: ClusterFuzzLite cron tasks +on: + workflow_dispatch: + push: + branches: + - main # Use your actual default branch here. + schedule: + - cron: '0 13 * * 6' # At 01:00 PM, only on Saturday +permissions: read-all +jobs: + Fuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - mode: batch + sanitizer: address + - mode: batch + sanitizer: memory + - mode: prune + sanitizer: address + - mode: coverage + sanitizer: coverage + steps: + - name: Build Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + language: c # Change this to the language you are fuzzing. + sanitizer: ${{ matrix.sanitizer }} + - name: Run Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: ${{ matrix.mode }} + sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 0000000..09f91da --- /dev/null +++ b/.github/workflows/cflite_pr.yml @@ -0,0 +1,43 @@ +name: ClusterFuzzLite PR fuzzing +on: + pull_request: + paths: + - '**' +permissions: read-all +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined, memory] # Override this with the sanitizers you want. + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: c # Change this to the language you are fuzzing. + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: ${{ matrix.sanitizer }} + # Optional but recommended: used to only run fuzzers that are affected + # by the PR. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: 'code-change' + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true + # Optional but recommended: used to download the corpus produced by + # batch fuzzing. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". diff --git a/.github/workflows/codeql_checks.yml b/.github/workflows/codeql_checks.yml new file mode 100644 index 0000000..cc2aae4 --- /dev/null +++ b/.github/workflows/codeql_checks.yml @@ -0,0 +1,45 @@ +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + # Excluded path: add the paths you want to ignore instead of deleting the workflow + paths-ignore: + - '.github/workflows/*.yml' + - 'tests/*' + +jobs: + analyse: + name: Analyse + strategy: + fail-fast: false + matrix: + sdk: ["$NANOX_SDK", "$NANOSP_SDK", "$STAX_SDK", "$FLEX_SDK"] + # 'cpp' covers C and C++ + language: ['cpp'] + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + # CodeQL will create the database during the compilation + - name: Build + run: | + make BOLOS_SDK=${{ matrix.sdk }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coding_style_checks.yml b/.github/workflows/coding_style_checks.yml new file mode 100644 index 0000000..7927239 --- /dev/null +++ b/.github/workflows/coding_style_checks.yml @@ -0,0 +1,25 @@ +name: Run coding style check through reusable workflow + +# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications. +# +# The presence of this workflow is mandatory as a minimal level of linting is required. +# You are however free to modify the content of the .clang-format file and thus the coding style of your application. +# We simply ask you to not diverge too much from the linting of the everscale application. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + check_linting: + name: Check linting using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1 + with: + source: './src' + extensions: 'h,c' + version: 12 diff --git a/.github/workflows/documentation_generation.yml b/.github/workflows/documentation_generation.yml new file mode 100644 index 0000000..31b1efb --- /dev/null +++ b/.github/workflows/documentation_generation.yml @@ -0,0 +1,29 @@ +name: Generate project documentation + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + job_generate_doc: + name: Generate project documentation + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: HTML documentation + run: doxygen .doxygen/Doxyfile + + - uses: actions/upload-artifact@v4 + with: + name: documentation + path: doc/html diff --git a/.github/workflows/misspellings_checks.yml b/.github/workflows/misspellings_checks.yml new file mode 100644 index 0000000..f38799e --- /dev/null +++ b/.github/workflows/misspellings_checks.yml @@ -0,0 +1,28 @@ +name: Misspellings checks + +# This workflow performs some misspelling checks on the repository +# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked +# applications. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + misspell: + name: Check misspellings + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Check misspellings + uses: codespell-project/actions-codespell@v2 + with: + builtin: clear,rare + check_filenames: true diff --git a/.github/workflows/python_client_checks.yml b/.github/workflows/python_client_checks.yml new file mode 100644 index 0000000..db60fd7 --- /dev/null +++ b/.github/workflows/python_client_checks.yml @@ -0,0 +1,41 @@ +name: Checks on the Python client + +# This workflow performs some checks on the Python client used by the everscale tests +# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked +# applications. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + lint: + name: everscale client linting + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + - name: Installing PIP dependencies + run: | + pip install pylint + pip install -r tests/requirements.txt + - name: Lint Python code + run: pylint --rc tests/setup.cfg tests/application_client/ + + mypy: + name: Type checking + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + - name: Installing PIP dependencies + run: | + pip install mypy + pip install -r tests/requirements.txt + - name: Mypy type checking + run: mypy tests/application_client/ diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..4eec9af --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,61 @@ +name: Unit testing with Codecov coverage checking + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + job_unit_test: + name: Unit test + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Clone SDK + uses: actions/checkout@v4 + with: + repository: ledgerHQ/ledger-secure-sdk + path: sdk + + - name: Build unit tests + run: | + cd unit-tests/ + export BOLOS_SDK=../sdk + cmake -Bbuild -H. && make -C build && make -C build test + + - name: Generate code coverage + run: | + cd unit-tests/ + lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base && \ + lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture && \ + lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info && \ + lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info && \ + genhtml coverage.info -o coverage + + - uses: actions/upload-artifact@v4 + with: + name: code-coverage + path: unit-tests/coverage + + - name: Install codecov dependencies + run: apk update && apk add curl gpg + + - name: Upload to codecov.io + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./unit-tests/coverage.info + flags: unittests + name: codecov-app-everscale + fail_ci_if_error: true + verbose: true From 26087f55a22e1e9804e71c00927f8bb9288cc024 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 14:25:11 +0100 Subject: [PATCH 06/13] build: remove "Pending review" --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a679a22..85b9957 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,9 @@ else endif APP_LOAD_PARAMS += $(COMMON_LOAD_PARAMS) -# Pending review parameters -APP_LOAD_PARAMS += --tlvraw 9F:01 -DEFINES += HAVE_PENDING_REVIEW_SCREEN +# # Pending review parameters +# APP_LOAD_PARAMS += --tlvraw 9F:01 +# DEFINES += HAVE_PENDING_REVIEW_SCREEN ################## # Define Version # From e819bcac429c0d7aed9d8d375c641c43f57228be Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 14:29:14 +0100 Subject: [PATCH 07/13] chore: update .gitignore and ledger_app.toml --- .gitignore | 2 ++ ledger_app.toml | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0666ff3..97e91e5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ dist *.spec *.DS_Store client/target + +__pycache__ \ No newline at end of file diff --git a/ledger_app.toml b/ledger_app.toml index 8f4c403..e6634a3 100644 --- a/ledger_app.toml +++ b/ledger_app.toml @@ -1,4 +1,7 @@ [app] build_directory = "./" sdk = "C" -devices = ["nanos", "nanox", "nanos+"] +devices = ["nanos+", "nanox"] + +[tests] +pytest_directory = "./tests/" From a507a330dc1e516a2644bc6fbbc3222c7e595ad2 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 16:12:40 +0100 Subject: [PATCH 08/13] test: add active_test_scope marker for pytest --- tests/conftest.py | 10 ++++++++++ tests/setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a9ea72c..76b5e0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +from ragger.conftest import configuration ########################### ### CONFIGURATION START ### @@ -6,6 +7,15 @@ # You can configure optional parameters by overriding the value of ragger.configuration.OPTIONAL_CONFIGURATION # Please refer to ragger/conftest/configuration.py for their descriptions and accepted values +# Define pytest markers +def pytest_configure(config): + config.addinivalue_line( + "markers", + "active_test_scope: marks tests related to application name functionality", + ) + # Add more markers here as needed + + ######################### ### CONFIGURATION END ### ######################### diff --git a/tests/setup.cfg b/tests/setup.cfg index 2398a52..2d024b3 100644 --- a/tests/setup.cfg +++ b/tests/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = --strict-markers +addopts = --strict-markers -m active_test_scope [pylint] disable = C0114, # missing-module-docstring From 4a94604478332d891013633c559879c59cee0337 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 16:13:08 +0100 Subject: [PATCH 09/13] test: add test for app configuration command --- tests/application_client/everscale_command_sender.py | 4 ++-- tests/{test_get_app_config_cmd.py => test_app_config_cmd.py} | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) rename tests/{test_get_app_config_cmd.py => test_app_config_cmd.py} (94%) diff --git a/tests/application_client/everscale_command_sender.py b/tests/application_client/everscale_command_sender.py index 73093b5..011fecd 100644 --- a/tests/application_client/everscale_command_sender.py +++ b/tests/application_client/everscale_command_sender.py @@ -84,9 +84,9 @@ def get_app_and_version(self) -> RAPDU: data=b"") - def get_version(self) -> RAPDU: + def get_app_config(self) -> RAPDU: return self.backend.exchange(cla=CLA, - ins=InsType.GET_VERSION, + ins=InsType.GET_APP_CONFIGURATION, p1=P1.P1_START, p2=P2.P2_LAST, data=b"") diff --git a/tests/test_get_app_config_cmd.py b/tests/test_app_config_cmd.py similarity index 94% rename from tests/test_get_app_config_cmd.py rename to tests/test_app_config_cmd.py index 733d1ee..b272b86 100644 --- a/tests/test_get_app_config_cmd.py +++ b/tests/test_app_config_cmd.py @@ -1,3 +1,4 @@ +import pytest from ragger.backend.interface import BackendInterface from application_client.everscale_command_sender import EverscaleCommandSender @@ -6,6 +7,7 @@ from utils import verify_version # In this test we check the behavior of the device when asked to provide the app version +@pytest.mark.active_test_scope def test_get_app_config(backend: BackendInterface) -> None: # Use the app interface instead of raw interface client = EverscaleCommandSender(backend) From 25dcc40a1b9e8460af616293bfadda437c6ff001 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 18:34:02 +0100 Subject: [PATCH 10/13] chore: enhance .gitignore with more comprehensive file exclusions --- .gitignore | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 97e91e5..581399e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,34 @@ +# Build and compilation directories (Répertoires de compilation) bin debug dep obj dev-env + +# Generated source files src/glyphs.c src/glyphs.h + +# IDE/editor specific files .idea .vscode gitignore + +# Dependency directories node_modules + +# Log files and test specifications *.log* +*.spec + +# Build output and distribution directories build dist -*.spec -*.DS_Store client/target -__pycache__ \ No newline at end of file +# Operating system files +*.DS_Store + +# Python cache and temporary files +__pycache__ +snapshots-tmp \ No newline at end of file From 9f90c9b43823c55c09ca8d318afb6950a9b3c7f6 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Thu, 6 Feb 2025 18:37:00 +0100 Subject: [PATCH 11/13] test: update public key retrieval tests for Everscale app - Modify public key tests to use account number instead of derivation path - Switch to Ed25519Slip curve for public key calculation - Update response unpacking to handle 32-byte public keys - Add snapshot images for confirmation tests - Add active test scope markers --- .../everscale_command_sender.py | 13 +++--- .../everscale_response_unpacker.py | 6 +-- .../00000.png | Bin 0 -> 904 bytes .../00001.png | Bin 0 -> 556 bytes .../00002.png | Bin 0 -> 369 bytes .../00003.png | Bin 0 -> 356 bytes .../00004.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 904 bytes .../00001.png | Bin 0 -> 556 bytes .../00002.png | Bin 0 -> 369 bytes .../00003.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 904 bytes .../00001.png | Bin 0 -> 556 bytes .../00002.png | Bin 0 -> 369 bytes .../00003.png | Bin 0 -> 356 bytes .../00004.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 904 bytes .../00001.png | Bin 0 -> 556 bytes .../00002.png | Bin 0 -> 369 bytes .../00003.png | Bin 0 -> 382 bytes tests/test_pubkey_cmd.py | 44 ++++++++++-------- 21 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00004.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png create mode 100644 tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_accepted/00004.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png create mode 100644 tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png diff --git a/tests/application_client/everscale_command_sender.py b/tests/application_client/everscale_command_sender.py index 011fecd..1510b60 100644 --- a/tests/application_client/everscale_command_sender.py +++ b/tests/application_client/everscale_command_sender.py @@ -48,10 +48,9 @@ class Errors(IntEnum): SW_INVALID_SRC_ADDRESS = 0x6B13, SW_INVALID_WALLET_ID = 0x6B14, SW_INVALID_WALLET_TYPE = 0x6B15, - SW_INVALID_TICKER_LENGTH = 0x6B16 - + SW_INVALID_TICKER_LENGTH = 0x6B16, + SW_DENY = 0x6985, # Status Word from everscale app - # SW_DENY = 0x6985 # SW_WRONG_P1P2 = 0x6A86 # SW_WRONG_DATA_LENGTH = 0x6A87 # SW_INS_NOT_SUPPORTED = 0x6D00 @@ -100,21 +99,21 @@ def get_app_name(self) -> RAPDU: data=b"") - def get_public_key(self, path: str) -> RAPDU: + def get_public_key(self, account_number: int) -> RAPDU: return self.backend.exchange(cla=CLA, ins=InsType.GET_PUBLIC_KEY, p1=P1.P1_START, p2=P2.P2_LAST, - data=pack_derivation_path(path)) + data=account_number.to_bytes(4, "big")) @contextmanager - def get_public_key_with_confirmation(self, path: str) -> Generator[None, None, None]: + def get_public_key_with_confirmation(self, account_number: int) -> Generator[None, None, None]: with self.backend.exchange_async(cla=CLA, ins=InsType.GET_PUBLIC_KEY, p1=P1.P1_CONFIRM, p2=P2.P2_LAST, - data=pack_derivation_path(path)) as response: + data=account_number.to_bytes(4, "big")) as response: yield response diff --git a/tests/application_client/everscale_response_unpacker.py b/tests/application_client/everscale_response_unpacker.py index 5d1f748..9cc096a 100644 --- a/tests/application_client/everscale_response_unpacker.py +++ b/tests/application_client/everscale_response_unpacker.py @@ -49,13 +49,11 @@ def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: # chain_code (var) def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) - response, chain_code_len, chain_code = pop_size_prefixed_buf_from_buf(response) - assert pub_key_len == 65 - assert chain_code_len == 32 + assert pub_key_len == 32 assert len(response) == 0 - return pub_key_len, pub_key, chain_code_len, chain_code + return pub_key_len, pub_key # Unpack from response: # response = der_sig_len (1) diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..a63f85f1db8b849a5dcbea69fdefdc20a1688f58 GIT binary patch literal 904 zcmV;319$w1P)8GLwKmkH`Ak$EC#E_E*H^E)Hz+84Xmjkj19-@3^!2iC3w87M$~`iC%H8O9Meo1) zhFE2f0|FL+CVX#V{;joSK$y+IZp*VL1Raju7Q&1kh<35Jw-2X7PzU&oaVMWRW%jJv zYVU4)&;e?SMsA|MzHU?z$q65YaQL3f)j*ezNZE714+kV&*N)}O_wZT>I*GMFyN)lJ z=D+z4z}vqlilRWz05yhUy-+Pt6;qHujQOjo#_)xgoekq7E>?k53-NqYh_pBSL3C*8 zUs!8YGkq)lR|lF48Jo$mtl=!)NkgqoLCWg~4yYFS7XSiB3)-$d;jysr`PSQljjvy3 z90v$>R|TBf)sodYgy-DN{-|p)kkg&Auf zc_S9U&09vu#O7K_oWd3+_ewSm1smTrktq+Sp>Con#&YMyTL{w7C!Qk38nFOa?4*3~ zKIMZqsnvgzTKzYfSKmxSmqb3RGLtwi9px+=m~42omeg*+prZdJfp!g7g2h?yYZK3CM9LFiusIpEP7)|%%-BU)M}Da<{$4yFUn%SLDX{q#U1%s_q`dT}Bj zE&;uf`5%*pVw1*8c{P{MvqfQ16h%=KMRB=*9{_|vH|Jwsi?1e&mz{c&@-0Xo`Ep(% z&Ssyy(y3~t^JyNJjF&7WfN8sGJZ=kGxy@ZIWP=(?IvJ=s0_FhuYtA)4<%d}vYxQGV zW6}~a;;sGjg(SWM0_W*b>e?l|tPb73iXIq!RE*e$A8&~d*b-(G-vP1Z-&-Y)kY!1H2u=wA0002sIA4*$Ip_N8bzQb) zV^Cid4V-?a$vGES(~sN-u@9kk-;J*(7s!> zE=yf&*H0xD#K8$qG=3HoVB+@1 z5WlzP;d9PGHSo-vwTn<%B0-0>TcZi12$2(yaj?K5M2>t9T zfVe+%c2LRhP*s|piC=|!`6?gkl|u}^zDrz@LjMEa9And!7O8pM$!jF&BwBV>DM`@J zpPd1A`vU+#+OY=o=O@~ssdk<-JQ3>mxWl6)F$UwJwsCs*6-C^Mi)V-Z2hm>hVEeRx zdTL*;e8xZFckhuY_Vxby<7kmyUc~ciZLDphxoQS_Hm2Gs`p0yYI7uH#kW;~)y3z*L zXs!%higRcNI1yJ@alM^zCE|PuTGwFhY}oQfY%av7DcjMH#_iG`Eouu8yvMDf_J*>y zOx+VU1C7{j5BG~O&vN1^n15xOW02l6J@wB~lT7xHA*7n@ABGyd5>)7w43bO@wnBOV u0000000000000000000000000w($$(`{Y7*AFC$-0000ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd127e517c190a1684db420b839a65dd8776a10 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`htba4!+nDh2#AYZeBfa}G~ z%X2sUSKT3Q%6Ru-)(e8M#-Se-4!|1j!jMc=4!j z_CJRO);kNf|C#^xTYZvZt?H%gi`lBKIP^;~T)*|9>>tm^$s6J??VLTmiitVa@RZl# z9D}7HW~IBg#_v|Osdspo)b@#I##$}$!vTL1erwOq|6RGGG~%xIOYz59T*k-q6J81a z$=e)pd!KrK?JqHvo`lxakeiYh%(DN_TKY9re^&30zAaAgrtV<9s?#_7_s4|tE$2+UT~i!DCi)dVeekXbfGQn;e2~0fsAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~o8GLwKmkH`Ak$EC#E_E*H^E)Hz+84Xmjkj19-@3^!2iC3w87M$~`iC%H8O9Meo1) zhFE2f0|FL+CVX#V{;joSK$y+IZp*VL1Raju7Q&1kh<35Jw-2X7PzU&oaVMWRW%jJv zYVU4)&;e?SMsA|MzHU?z$q65YaQL3f)j*ezNZE714+kV&*N)}O_wZT>I*GMFyN)lJ z=D+z4z}vqlilRWz05yhUy-+Pt6;qHujQOjo#_)xgoekq7E>?k53-NqYh_pBSL3C*8 zUs!8YGkq)lR|lF48Jo$mtl=!)NkgqoLCWg~4yYFS7XSiB3)-$d;jysr`PSQljjvy3 z90v$>R|TBf)sodYgy-DN{-|p)kkg&Auf zc_S9U&09vu#O7K_oWd3+_ewSm1smTrktq+Sp>Con#&YMyTL{w7C!Qk38nFOa?4*3~ zKIMZqsnvgzTKzYfSKmxSmqb3RGLtwi9px+=m~42omeg*+prZdJfp!g7g2h?yYZK3CM9LFiusIpEP7)|%%-BU)M}Da<{$4yFUn%SLDX{q#U1%s_q`dT}Bj zE&;uf`5%*pVw1*8c{P{MvqfQ16h%=KMRB=*9{_|vH|Jwsi?1e&mz{c&@-0Xo`Ep(% z&Ssyy(y3~t^JyNJjF&7WfN8sGJZ=kGxy@ZIWP=(?IvJ=s0_FhuYtA)4<%d}vYxQGV zW6}~a;;sGjg(SWM0_W*b>e?l|tPb73iXIq!RE*e$A8&~d*b-(G-vP1Z-&-Y)kY!1H2u=wA0002sIA4*$Ip_N8bzQb) zV^Cid4V-?a$vGES(~sN-u@9kk-;J*(7s!> zE=yf&*H0xD#K8$qG=3HoVB+@1 z5WlzP;d9PGHSo-vwTn<%B0-0>TcZi12$2(yaj?K5M2>t9T zfVe+%c2LRhP*s|piC=|!`6?gkl|u}^zDrz@LjMEa9And!7O8pM$!jF&BwBV>DM`@J zpPd1A`vU+#+OY=o=O@~ssdk<-JQ3>mxWl6)F$UwJwsCs*6-C^Mi)V-Z2hm>hVEeRx zdTL*;e8xZFckhuY_Vxby<7kmyUc~ciZLDphxoQS_Hm2Gs`p0yYI7uH#kW;~)y3z*L zXs!%higRcNI1yJ@alM^zCE|PuTGwFhY}oQfY%av7DcjMH#_iG`Eouu8yvMDf_J*>y zOx+VU1C7{j5BG~O&vN1^n15xOW02l6J@wB~lT7xHA*7n@ABGyd5>)7w43bO@wnBOV u0000000000000000000000000w($$(`{Y7*AFC$-0000ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..a58590b988714545e7960f7f400f360ffc5de41f GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|@9hba4!+nDh2#bl+hG9*4k? zQuR0Wo;SViCn%lTo!KM1sAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~o8GLwKmkH`Ak$EC#E_E*H^E)Hz+84Xmjkj19-@3^!2iC3w87M$~`iC%H8O9Meo1) zhFE2f0|FL+CVX#V{;joSK$y+IZp*VL1Raju7Q&1kh<35Jw-2X7PzU&oaVMWRW%jJv zYVU4)&;e?SMsA|MzHU?z$q65YaQL3f)j*ezNZE714+kV&*N)}O_wZT>I*GMFyN)lJ z=D+z4z}vqlilRWz05yhUy-+Pt6;qHujQOjo#_)xgoekq7E>?k53-NqYh_pBSL3C*8 zUs!8YGkq)lR|lF48Jo$mtl=!)NkgqoLCWg~4yYFS7XSiB3)-$d;jysr`PSQljjvy3 z90v$>R|TBf)sodYgy-DN{-|p)kkg&Auf zc_S9U&09vu#O7K_oWd3+_ewSm1smTrktq+Sp>Con#&YMyTL{w7C!Qk38nFOa?4*3~ zKIMZqsnvgzTKzYfSKmxSmqb3RGLtwi9px+=m~42omeg*+prZdJfp!g7g2h?yYZK3CM9LFiusIpEP7)|%%-BU)M}Da<{$4yFUn%SLDX{q#U1%s_q`dT}Bj zE&;uf`5%*pVw1*8c{P{MvqfQ16h%=KMRB=*9{_|vH|Jwsi?1e&mz{c&@-0Xo`Ep(% z&Ssyy(y3~t^JyNJjF&7WfN8sGJZ=kGxy@ZIWP=(?IvJ=s0_FhuYtA)4<%d}vYxQGV zW6}~a;;sGjg(SWM0_W*b>e?l|tPb73iXIq!RE*e$A8&~d*b-(G-vP1Z-&-Y)kY!1H2u=wA0002sIA4*$Ip_N8bzQb) zV^Cid4V-?a$vGES(~sN-u@9kk-;J*(7s!> zE=yf&*H0xD#K8$qG=3HoVB+@1 z5WlzP;d9PGHSo-vwTn<%B0-0>TcZi12$2(yaj?K5M2>t9T zfVe+%c2LRhP*s|piC=|!`6?gkl|u}^zDrz@LjMEa9And!7O8pM$!jF&BwBV>DM`@J zpPd1A`vU+#+OY=o=O@~ssdk<-JQ3>mxWl6)F$UwJwsCs*6-C^Mi)V-Z2hm>hVEeRx zdTL*;e8xZFckhuY_Vxby<7kmyUc~ciZLDphxoQS_Hm2Gs`p0yYI7uH#kW;~)y3z*L zXs!%higRcNI1yJ@alM^zCE|PuTGwFhY}oQfY%av7DcjMH#_iG`Eouu8yvMDf_J*>y zOx+VU1C7{j5BG~O&vN1^n15xOW02l6J@wB~lT7xHA*7n@ABGyd5>)7w43bO@wnBOV u0000000000000000000000000w($$(`{Y7*AFC$-0000ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd127e517c190a1684db420b839a65dd8776a10 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`htba4!+nDh2#AYZeBfa}G~ z%X2sUSKT3Q%6Ru-)(e8M#-Se-4!|1j!jMc=4!j z_CJRO);kNf|C#^xTYZvZt?H%gi`lBKIP^;~T)*|9>>tm^$s6J??VLTmiitVa@RZl# z9D}7HW~IBg#_v|Osdspo)b@#I##$}$!vTL1erwOq|6RGGG~%xIOYz59T*k-q6J81a z$=e)pd!KrK?JqHvo`lxakeiYh%(DN_TKY9re^&30zAaAgrtV<9s?#_7_s4|tE$2+UT~i!DCi)dVeekXbfGQn;e2~0fsAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~o8GLwKmkH`Ak$EC#E_E*H^E)Hz+84Xmjkj19-@3^!2iC3w87M$~`iC%H8O9Meo1) zhFE2f0|FL+CVX#V{;joSK$y+IZp*VL1Raju7Q&1kh<35Jw-2X7PzU&oaVMWRW%jJv zYVU4)&;e?SMsA|MzHU?z$q65YaQL3f)j*ezNZE714+kV&*N)}O_wZT>I*GMFyN)lJ z=D+z4z}vqlilRWz05yhUy-+Pt6;qHujQOjo#_)xgoekq7E>?k53-NqYh_pBSL3C*8 zUs!8YGkq)lR|lF48Jo$mtl=!)NkgqoLCWg~4yYFS7XSiB3)-$d;jysr`PSQljjvy3 z90v$>R|TBf)sodYgy-DN{-|p)kkg&Auf zc_S9U&09vu#O7K_oWd3+_ewSm1smTrktq+Sp>Con#&YMyTL{w7C!Qk38nFOa?4*3~ zKIMZqsnvgzTKzYfSKmxSmqb3RGLtwi9px+=m~42omeg*+prZdJfp!g7g2h?yYZK3CM9LFiusIpEP7)|%%-BU)M}Da<{$4yFUn%SLDX{q#U1%s_q`dT}Bj zE&;uf`5%*pVw1*8c{P{MvqfQ16h%=KMRB=*9{_|vH|Jwsi?1e&mz{c&@-0Xo`Ep(% z&Ssyy(y3~t^JyNJjF&7WfN8sGJZ=kGxy@ZIWP=(?IvJ=s0_FhuYtA)4<%d}vYxQGV zW6}~a;;sGjg(SWM0_W*b>e?l|tPb73iXIq!RE*e$A8&~d*b-(G-vP1Z-&-Y)kY!1H2u=wA0002sIA4*$Ip_N8bzQb) zV^Cid4V-?a$vGES(~sN-u@9kk-;J*(7s!> zE=yf&*H0xD#K8$qG=3HoVB+@1 z5WlzP;d9PGHSo-vwTn<%B0-0>TcZi12$2(yaj?K5M2>t9T zfVe+%c2LRhP*s|piC=|!`6?gkl|u}^zDrz@LjMEa9And!7O8pM$!jF&BwBV>DM`@J zpPd1A`vU+#+OY=o=O@~ssdk<-JQ3>mxWl6)F$UwJwsCs*6-C^Mi)V-Z2hm>hVEeRx zdTL*;e8xZFckhuY_Vxby<7kmyUc~ciZLDphxoQS_Hm2Gs`p0yYI7uH#kW;~)y3z*L zXs!%higRcNI1yJ@alM^zCE|PuTGwFhY}oQfY%av7DcjMH#_iG`Eouu8yvMDf_J*>y zOx+VU1C7{j5BG~O&vN1^n15xOW02l6J@wB~lT7xHA*7n@ABGyd5>)7w43bO@wnBOV u0000000000000000000000000w($$(`{Y7*AFC$-0000ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..a58590b988714545e7960f7f400f360ffc5de41f GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|@9hba4!+nDh2#bl+hG9*4k? zQuR0Wo;SViCn%lTo!KM1sAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~o None: - path_list = [ - "m/44'/1'/0'/0/0", - "m/44'/1'/0/0/0", - "m/44'/1'/911'/0/0", - "m/44'/1'/255/255/255", - "m/44'/1'/2147483647/0/0/0/0/0/0/0" + account_number_list = [ + 0, + 1, + 911, + 255, + 2147483647 ] - for path in path_list: + for account_number in account_number_list: client = EverscaleCommandSender(backend) - response = client.get_public_key(path=path).data - _, public_key, _, chain_code = unpack_get_public_key_response(response) + response = client.get_public_key(account_number=account_number).data + _, public_key = unpack_get_public_key_response(response) - ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) - assert public_key.hex() == ref_public_key - assert chain_code.hex() == ref_chain_code + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Ed25519Slip, path=PATH_PREFIX + str(account_number | HARDENED_OFFSET) + PATH_SUFFIX) + assert "00" + public_key.hex() == ref_public_key # In this test we check that the GET_PUBLIC_KEY works in confirmation mode +@pytest.mark.active_test_scope def test_get_public_key_confirm_accepted(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: client = EverscaleCommandSender(backend) - path = "m/44'/1'/0'/0/0" - with client.get_public_key_with_confirmation(path=path): + account_number = 0 + with client.get_public_key_with_confirmation(account_number=account_number): scenario_navigator.address_review_approve() response = client.get_async_response().data - _, public_key, _, chain_code = unpack_get_public_key_response(response) + _, public_key = unpack_get_public_key_response(response) - ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) - assert public_key.hex() == ref_public_key - assert chain_code.hex() == ref_chain_code + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Ed25519Slip, path=PATH_PREFIX + str(account_number | HARDENED_OFFSET) + PATH_SUFFIX) + assert "00" + public_key.hex() == ref_public_key # In this test we check that the GET_PUBLIC_KEY in confirmation mode replies an error if the user refuses +@pytest.mark.active_test_scope def test_get_public_key_confirm_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: client = EverscaleCommandSender(backend) - path = "m/44'/1'/0'/0/0" + account_number = 0 with pytest.raises(ExceptionRAPDU) as e: - with client.get_public_key_with_confirmation(path=path): + with client.get_public_key_with_confirmation(account_number=account_number): scenario_navigator.address_review_reject() # Assert that we have received a refusal From fa65f97641e22b4e96fcb4699cfcddf410561219 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Mon, 10 Feb 2025 12:25:55 +0100 Subject: [PATCH 12/13] test: add tests for Everscale address retrieval functionality - Implement test cases for address retrieval across different wallet types - Add support for non-confirmation and confirmation modes of address retrieval - Create test scenarios for address review acceptance and rejection - Extend command sender and response unpacker to handle address-related operations - Add snapshot images for address confirmation tests --- .../everscale_command_sender.py | 27 ++++++++ .../everscale_response_unpacker.py | 13 +++- .../00000.png | Bin 0 -> 400 bytes .../00001.png | Bin 0 -> 889 bytes .../00002.png | Bin 0 -> 498 bytes .../00003.png | Bin 0 -> 369 bytes .../00004.png | Bin 0 -> 356 bytes .../00005.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 400 bytes .../00001.png | Bin 0 -> 889 bytes .../00002.png | Bin 0 -> 498 bytes .../00003.png | Bin 0 -> 369 bytes .../00004.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 400 bytes .../00001.png | Bin 0 -> 889 bytes .../00002.png | Bin 0 -> 498 bytes .../00003.png | Bin 0 -> 369 bytes .../00004.png | Bin 0 -> 356 bytes .../00005.png | Bin 0 -> 382 bytes .../00000.png | Bin 0 -> 400 bytes .../00001.png | Bin 0 -> 889 bytes .../00002.png | Bin 0 -> 498 bytes .../00003.png | Bin 0 -> 369 bytes .../00004.png | Bin 0 -> 382 bytes tests/test_address_cmd.py | 64 ++++++++++++++++++ 25 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00000.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00001.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00002.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00003.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00004.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00005.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00000.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00001.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00002.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00003.png create mode 100644 tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00004.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00000.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00001.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00002.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00003.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00004.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00005.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00000.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00001.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00002.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00003.png create mode 100644 tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00004.png create mode 100644 tests/test_address_cmd.py diff --git a/tests/application_client/everscale_command_sender.py b/tests/application_client/everscale_command_sender.py index 1510b60..921f339 100644 --- a/tests/application_client/everscale_command_sender.py +++ b/tests/application_client/everscale_command_sender.py @@ -31,6 +31,17 @@ class InsType(IntEnum): GET_ADDRESS = 0X04 SIGN_TRANSACTION = 0x05 +class WalletType(IntEnum): + WALLET_V3 = 0 + EVER_WALLET = 1 + SAFE_MULTISIG_WALLET = 2 + SAFE_MULTISIG_WALLET_24H = 3 + SETCODE_MULTISIG_WALLET = 4 + BRIDGE_MULTISIG_WALLET = 5 + SURF_WALLET = 6 + MULTISIG_2 = 7 + MULTISIG_2_1 = 8 + class Errors(IntEnum): SW_INVALID_DATA = 0x6B00, SW_CELL_UNDERFLOW = 0x6B01, @@ -116,6 +127,22 @@ def get_public_key_with_confirmation(self, account_number: int) -> Generator[Non data=account_number.to_bytes(4, "big")) as response: yield response + def get_address(self, account_number: int, wallet_type: WalletType) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_ADDRESS, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=account_number.to_bytes(4, "big") + wallet_type.to_bytes(1, "big")) + + @contextmanager + def get_address_with_confirmation(self, account_number: int, wallet_type: WalletType) -> Generator[None, None, None]: + with self.backend.exchange_async(cla=CLA, + ins=InsType.GET_ADDRESS, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=account_number.to_bytes(4, "big") + wallet_type.to_bytes(1, "big")) as response: + yield response + @contextmanager def sign_tx(self, path: str, transaction: bytes) -> Generator[None, None, None]: diff --git a/tests/application_client/everscale_response_unpacker.py b/tests/application_client/everscale_response_unpacker.py index 9cc096a..24e626d 100644 --- a/tests/application_client/everscale_response_unpacker.py +++ b/tests/application_client/everscale_response_unpacker.py @@ -45,8 +45,6 @@ def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: # Unpack from response: # response = pub_key_len (1) # pub_key (var) -# chain_code_len (1) -# chain_code (var) def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) @@ -55,6 +53,17 @@ def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, by return pub_key_len, pub_key +# Unpack from response: +# response = address_len (1) +# address (var) +def unpack_get_address_response(response: bytes) -> Tuple[int, bytes]: + response, address_len, address = pop_size_prefixed_buf_from_buf(response) + + assert address_len == 32 + assert len(response) == 0 + + return address_len, address + # Unpack from response: # response = der_sig_len (1) # der_sig (var) diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00000.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..a487005a859c0e4cfa0212a67f75a4794d81978c GIT binary patch literal 400 zcmV;B0dM|^P)CI7!~g&QfPa@`!PrJQ5_gtexv+u#;<7n?ZA>=8WP3sS)E3>Z zE!xY!9dS=7B}gQ@x;^0FVDIF?`GpenfZ9e>)xCO*lgBykAKe#*G9mk>o6EkWtAGi$ zGexMpHiozruqDn$Y{#w$*~F;G!$#0S7$x##_JEJys@+nJ)BK}UfHaSYH}BKlb~|vz<*qUayO&UL-bn@O u0WKrm9KB3jyJN~A3;+NC0000$MC1WGrw>UKISsS`00007f41`H4H{t$A@?G-a3Pqu<#o}@1@9$}fku0%ThPF!~gb+dqA>^C5uIsuk{4d9T zS3Z8YzUBId<-UvGCoWpLbdQcHjvQ;%tk&B8?SK1TEBx7hMIW^f>fKOlwFUxjnajcP z-OiCaz*=tofDZr4KFg(7YgGq&@Rcn?2x)*ILvP{W9WZ1JuKl?y^aE32<+#>o0|Y>@f`GM(Ncr|y8h;0}$$;zDX`v$1xH z(jbi6P`3bmh`#a3=kr&wUJp+nQ@)@7<*7gjA>^m<%lczldl|NC>ZWrDgdC~V3l3>G zLH*?Y>#KPpJq`N~f#{lpjin%}J8sng`H@OBZ1D}8ex-s+HXp>Y3JlLRff%ROhZ5bJ z@*)#n&Bf7qth$G%;TS)&I1*~8RqRrZVXtImXeC>fH~N;|MwasF0d1GPR|EK_0enT_ zO#nDv6CE2$YTv`&V1gA8gLAv4ytCBjcS>aE$58=3`2o)E}u|pZ5;eCtMSf* zF0KY1O(Q2oIOV0}&xF!N6=4t^)Q_%usk+U?;dYTgjBCH8SmtPx1z2BFzeE^}5s3zu zaa85ivUJ>2coVR$-!6I`(&;QM7DS!V=~VZtq3>5Wvu~3QY4SId&j7U%ugmMgs}U1!Bc zH4Dp>pX&T@fwwSXfV<;$N&TJ5e>&o#V^92bwA!LdiGHNeJr(Fx>dH#0!36k{)^PQd zIv*xyS-x%b%i5=|<^;&W#mijHe?Xaf8|7>bk!L_Ejk#Ea5JCtcguI%+Tt#N5f~LHY P00000NkvXXu0mjfn=Q4X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00002.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..79ec8e7d5f4668017394572679a188b792d1ef70 GIT binary patch literal 498 zcmV*S6;srWKHAi^h!zFxUtJDqm8Ea z9rNzALm^q>xitNU(YR64UP|5nWF1Akk1LaR!Q5YZ9KACV#eURt_~q%TbGA$6H3u%p zJogO1o_6;ql!phhj?8JYj+0y%l1`fs-7^3av|+f|n&x?zb}dPI8$MPx#07*qoM6N<$g6k6JrvLx| literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00003.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..78530ed78893e8cde716b8c31d30c1cb56cec73a GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_WHba4!+nDh2VG~Xcwk(P&x z3)S!bpLC;}UuDU|MO!+Jod3P|R5r6{pX~zF0|gT%26<`T{T*_-xRm|fi)A_DwP|4u zk1alRYrXrl&7>ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00004.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_accepted/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd127e517c190a1684db420b839a65dd8776a10 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`htba4!+nDh2#AYZeBfa}G~ z%X2sUSKT3Q%6Ru-)(e8M#-Se-4!|1j!jMc=4!j z_CJRO);kNf|C#^xTYZvZt?H%gi`lBKIP^;~T)*|9>>tm^$s6J??VLTmiitVa@RZl# z9D}7HW~IBg#_v|Osdspo)b@#I##$}$!vTL1erwOq|6RGGG~%xIOYz59T*k-q6J81a z$=e)pd!KrK?JqHvo`lxakeiYh%(DN_TKY9re^&30zAaAgrtV<9s?#_7_s4|tE$2+UT~i!DCi)dVeekXbfGQn;e2~0fsAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~oCI7!~g&QfPa@`!PrJQ5_gtexv+u#;<7n?ZA>=8WP3sS)E3>Z zE!xY!9dS=7B}gQ@x;^0FVDIF?`GpenfZ9e>)xCO*lgBykAKe#*G9mk>o6EkWtAGi$ zGexMpHiozruqDn$Y{#w$*~F;G!$#0S7$x##_JEJys@+nJ)BK}UfHaSYH}BKlb~|vz<*qUayO&UL-bn@O u0WKrm9KB3jyJN~A3;+NC0000$MC1WGrw>UKISsS`00007f41`H4H{t$A@?G-a3Pqu<#o}@1@9$}fku0%ThPF!~gb+dqA>^C5uIsuk{4d9T zS3Z8YzUBId<-UvGCoWpLbdQcHjvQ;%tk&B8?SK1TEBx7hMIW^f>fKOlwFUxjnajcP z-OiCaz*=tofDZr4KFg(7YgGq&@Rcn?2x)*ILvP{W9WZ1JuKl?y^aE32<+#>o0|Y>@f`GM(Ncr|y8h;0}$$;zDX`v$1xH z(jbi6P`3bmh`#a3=kr&wUJp+nQ@)@7<*7gjA>^m<%lczldl|NC>ZWrDgdC~V3l3>G zLH*?Y>#KPpJq`N~f#{lpjin%}J8sng`H@OBZ1D}8ex-s+HXp>Y3JlLRff%ROhZ5bJ z@*)#n&Bf7qth$G%;TS)&I1*~8RqRrZVXtImXeC>fH~N;|MwasF0d1GPR|EK_0enT_ zO#nDv6CE2$YTv`&V1gA8gLAv4ytCBjcS>aE$58=3`2o)E}u|pZ5;eCtMSf* zF0KY1O(Q2oIOV0}&xF!N6=4t^)Q_%usk+U?;dYTgjBCH8SmtPx1z2BFzeE^}5s3zu zaa85ivUJ>2coVR$-!6I`(&;QM7DS!V=~VZtq3>5Wvu~3QY4SId&j7U%ugmMgs}U1!Bc zH4Dp>pX&T@fwwSXfV<;$N&TJ5e>&o#V^92bwA!LdiGHNeJr(Fx>dH#0!36k{)^PQd zIv*xyS-x%b%i5=|<^;&W#mijHe?Xaf8|7>bk!L_Ejk#Ea5JCtcguI%+Tt#N5f~LHY P00000NkvXXu0mjfn=Q4X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00002.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..79ec8e7d5f4668017394572679a188b792d1ef70 GIT binary patch literal 498 zcmV*S6;srWKHAi^h!zFxUtJDqm8Ea z9rNzALm^q>xitNU(YR64UP|5nWF1Akk1LaR!Q5YZ9KACV#eURt_~q%TbGA$6H3u%p zJogO1o_6;ql!phhj?8JYj+0y%l1`fs-7^3av|+f|n&x?zb}dPI8$MPx#07*qoM6N<$g6k6JrvLx| literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00003.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..78530ed78893e8cde716b8c31d30c1cb56cec73a GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_WHba4!+nDh2VG~Xcwk(P&x z3)S!bpLC;}UuDU|MO!+Jod3P|R5r6{pX~zF0|gT%26<`T{T*_-xRm|fi)A_DwP|4u zk1alRYrXrl&7>ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00004.png b/tests/snapshots/nanosp/test_get_address_wallet_v3_confirm_refused/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..a58590b988714545e7960f7f400f360ffc5de41f GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|@9hba4!+nDh2#bl+hG9*4k? zQuR0Wo;SViCn%lTo!KM1sAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~oCI7!~g&QfPa@`!PrJQ5_gtexv+u#;<7n?ZA>=8WP3sS)E3>Z zE!xY!9dS=7B}gQ@x;^0FVDIF?`GpenfZ9e>)xCO*lgBykAKe#*G9mk>o6EkWtAGi$ zGexMpHiozruqDn$Y{#w$*~F;G!$#0S7$x##_JEJys@+nJ)BK}UfHaSYH}BKlb~|vz<*qUayO&UL-bn@O u0WKrm9KB3jyJN~A3;+NC0000$MC1WGrw>UKISsS`00007f41`H4H{t$A@?G-a3Pqu<#o}@1@9$}fku0%ThPF!~gb+dqA>^C5uIsuk{4d9T zS3Z8YzUBId<-UvGCoWpLbdQcHjvQ;%tk&B8?SK1TEBx7hMIW^f>fKOlwFUxjnajcP z-OiCaz*=tofDZr4KFg(7YgGq&@Rcn?2x)*ILvP{W9WZ1JuKl?y^aE32<+#>o0|Y>@f`GM(Ncr|y8h;0}$$;zDX`v$1xH z(jbi6P`3bmh`#a3=kr&wUJp+nQ@)@7<*7gjA>^m<%lczldl|NC>ZWrDgdC~V3l3>G zLH*?Y>#KPpJq`N~f#{lpjin%}J8sng`H@OBZ1D}8ex-s+HXp>Y3JlLRff%ROhZ5bJ z@*)#n&Bf7qth$G%;TS)&I1*~8RqRrZVXtImXeC>fH~N;|MwasF0d1GPR|EK_0enT_ zO#nDv6CE2$YTv`&V1gA8gLAv4ytCBjcS>aE$58=3`2o)E}u|pZ5;eCtMSf* zF0KY1O(Q2oIOV0}&xF!N6=4t^)Q_%usk+U?;dYTgjBCH8SmtPx1z2BFzeE^}5s3zu zaa85ivUJ>2coVR$-!6I`(&;QM7DS!V=~VZtq3>5Wvu~3QY4SId&j7U%ugmMgs}U1!Bc zH4Dp>pX&T@fwwSXfV<;$N&TJ5e>&o#V^92bwA!LdiGHNeJr(Fx>dH#0!36k{)^PQd zIv*xyS-x%b%i5=|<^;&W#mijHe?Xaf8|7>bk!L_Ejk#Ea5JCtcguI%+Tt#N5f~LHY P00000NkvXXu0mjfn=Q4X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00002.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..79ec8e7d5f4668017394572679a188b792d1ef70 GIT binary patch literal 498 zcmV*S6;srWKHAi^h!zFxUtJDqm8Ea z9rNzALm^q>xitNU(YR64UP|5nWF1Akk1LaR!Q5YZ9KACV#eURt_~q%TbGA$6H3u%p zJogO1o_6;ql!phhj?8JYj+0y%l1`fs-7^3av|+f|n&x?zb}dPI8$MPx#07*qoM6N<$g6k6JrvLx| literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00003.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..78530ed78893e8cde716b8c31d30c1cb56cec73a GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_WHba4!+nDh2VG~Xcwk(P&x z3)S!bpLC;}UuDU|MO!+Jod3P|R5r6{pX~zF0|gT%26<`T{T*_-xRm|fi)A_DwP|4u zk1alRYrXrl&7>ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00004.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_accepted/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd127e517c190a1684db420b839a65dd8776a10 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`htba4!+nDh2#AYZeBfa}G~ z%X2sUSKT3Q%6Ru-)(e8M#-Se-4!|1j!jMc=4!j z_CJRO);kNf|C#^xTYZvZt?H%gi`lBKIP^;~T)*|9>>tm^$s6J??VLTmiitVa@RZl# z9D}7HW~IBg#_v|Osdspo)b@#I##$}$!vTL1erwOq|6RGGG~%xIOYz59T*k-q6J81a z$=e)pd!KrK?JqHvo`lxakeiYh%(DN_TKY9re^&30zAaAgrtV<9s?#_7_s4|tE$2+UT~i!DCi)dVeekXbfGQn;e2~0fsAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~oCI7!~g&QfPa@`!PrJQ5_gtexv+u#;<7n?ZA>=8WP3sS)E3>Z zE!xY!9dS=7B}gQ@x;^0FVDIF?`GpenfZ9e>)xCO*lgBykAKe#*G9mk>o6EkWtAGi$ zGexMpHiozruqDn$Y{#w$*~F;G!$#0S7$x##_JEJys@+nJ)BK}UfHaSYH}BKlb~|vz<*qUayO&UL-bn@O u0WKrm9KB3jyJN~A3;+NC0000$MC1WGrw>UKISsS`00007f41`H4H{t$A@?G-a3Pqu<#o}@1@9$}fku0%ThPF!~gb+dqA>^C5uIsuk{4d9T zS3Z8YzUBId<-UvGCoWpLbdQcHjvQ;%tk&B8?SK1TEBx7hMIW^f>fKOlwFUxjnajcP z-OiCaz*=tofDZr4KFg(7YgGq&@Rcn?2x)*ILvP{W9WZ1JuKl?y^aE32<+#>o0|Y>@f`GM(Ncr|y8h;0}$$;zDX`v$1xH z(jbi6P`3bmh`#a3=kr&wUJp+nQ@)@7<*7gjA>^m<%lczldl|NC>ZWrDgdC~V3l3>G zLH*?Y>#KPpJq`N~f#{lpjin%}J8sng`H@OBZ1D}8ex-s+HXp>Y3JlLRff%ROhZ5bJ z@*)#n&Bf7qth$G%;TS)&I1*~8RqRrZVXtImXeC>fH~N;|MwasF0d1GPR|EK_0enT_ zO#nDv6CE2$YTv`&V1gA8gLAv4ytCBjcS>aE$58=3`2o)E}u|pZ5;eCtMSf* zF0KY1O(Q2oIOV0}&xF!N6=4t^)Q_%usk+U?;dYTgjBCH8SmtPx1z2BFzeE^}5s3zu zaa85ivUJ>2coVR$-!6I`(&;QM7DS!V=~VZtq3>5Wvu~3QY4SId&j7U%ugmMgs}U1!Bc zH4Dp>pX&T@fwwSXfV<;$N&TJ5e>&o#V^92bwA!LdiGHNeJr(Fx>dH#0!36k{)^PQd zIv*xyS-x%b%i5=|<^;&W#mijHe?Xaf8|7>bk!L_Ejk#Ea5JCtcguI%+Tt#N5f~LHY P00000NkvXXu0mjfn=Q4X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00002.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..79ec8e7d5f4668017394572679a188b792d1ef70 GIT binary patch literal 498 zcmV*S6;srWKHAi^h!zFxUtJDqm8Ea z9rNzALm^q>xitNU(YR64UP|5nWF1Akk1LaR!Q5YZ9KACV#eURt_~q%TbGA$6H3u%p zJogO1o_6;ql!phhj?8JYj+0y%l1`fs-7^3av|+f|n&x?zb}dPI8$MPx#07*qoM6N<$g6k6JrvLx| literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00003.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..78530ed78893e8cde716b8c31d30c1cb56cec73a GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_WHba4!+nDh2VG~Xcwk(P&x z3)S!bpLC;}UuDU|MO!+Jod3P|R5r6{pX~zF0|gT%26<`T{T*_-xRm|fi)A_DwP|4u zk1alRYrXrl&7>ePtl`Dg1Ll7_D%QL^;QO}e@r*i)a}8^{Zkzo5pnJe9hTGs)%i9RC z2YX)kopaUu!*yX^<(ljOafd(VJ$`faOn+Rm{dn)VSh(O}xmV@2(sNgM{y)Fspw}8F z?VIlpU+W0(yO8^$vgAz6x~JdvT`wp)rTMS?c;!KRuJu=CXIz>qbCu`Eg=N1@Caz2l zXef@WjPbS{x`#dx8HeXrotLqoTi}!zV zWi2{>VN>7nJ8ahbIp@!pu@k+dTCr@8V=d>cc^8CC`cqk8A;56?-sZ_S(sh?ur-Fn$ MUHx3vIVCg!0MAmW;Q#;t literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00004.png b/tests/snapshots/nanox/test_get_address_wallet_v3_confirm_refused/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..a58590b988714545e7960f7f400f360ffc5de41f GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|@9hba4!+nDh2#bl+hG9*4k? zQuR0Wo;SViCn%lTo!KM1sAjk5Y(@*V#~rbuHu`7A_H&AxgwHf8y4cdKvg$JbB4n%ez%sZ31aBYd&Oo@(7)_WJ(Znl2o8^PD` zFE`_Y-IS+Kdq0Q2-7r_C=}F(5ev`U`H~)QH|9$1FHE$YjoVax+e0xei%S`SAGlOm! zp7{T3NA-mVOYb&rdU8MdyIHo@@7cE*uj@I+Zu{2i)KMSV-E~o None: + temp = [] + for wallet_type in range(9): + client = EverscaleCommandSender(backend) + response = client.get_address(account_number=0, wallet_type=wallet_type).data + _, address = unpack_get_address_response(response) + + assert address.hex() == EXPECTED_ADDRESSES[wallet_type], f"Error with wallet_type: {wallet_type}, expected: {EXPECTED_ADDRESSES[wallet_type]}, but got {address.hex()}" + + +# In this test we check that the GET_ADDRESS works in confirmation mode +@pytest.mark.active_test_scope +def test_get_address_wallet_v3_confirm_accepted(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + client = EverscaleCommandSender(backend) + account_number = 0 + wallet_type = WalletType.WALLET_V3 + with client.get_address_with_confirmation(account_number=account_number, wallet_type=wallet_type): + scenario_navigator.address_review_approve() + + response = client.get_async_response().data + _, address = unpack_get_address_response(response) + + assert address.hex() == EXPECTED_ADDRESSES[wallet_type], f"Error with wallet_type: {wallet_type}, expected: {EXPECTED_ADDRESSES[wallet_type]}, but got {address.hex()}" + + +# In this test we check that the GET_ADDRESS in confirmation mode replies an error if the user refuses +@pytest.mark.active_test_scope +def test_get_address_wallet_v3_confirm_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + client = EverscaleCommandSender(backend) + account_number = 0 + wallet_type = WalletType.WALLET_V3 + + with pytest.raises(ExceptionRAPDU) as e: + with client.get_address_with_confirmation(account_number=account_number, wallet_type=wallet_type): + scenario_navigator.address_review_reject() + + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 + + + + + From dea39ec4a5d2d4f573f9f9fd72475cf44f51daf0 Mon Sep 17 00:00:00 2001 From: keiff3r Date: Mon, 10 Feb 2025 16:39:49 +0100 Subject: [PATCH 13/13] test: add message and transaction signing tests for Everscale app (incomplete) - Implement test cases for signing transactions and messages - Update transaction and command sender classes to support new signing methods - Add placeholder tests with TODO comments for future implementation - Modify response unpacking and transaction serialization logic - Simplify signing methods with account number and wallet type parameters --- .../everscale_command_sender.py | 29 +-- .../everscale_response_unpacker.py | 3 +- .../everscale_transaction.py | 210 ++++++++++++++--- tests/test_sign_cmd.py | 218 +++++++++--------- tests/test_sign_message_cmd.py | 44 ++++ 5 files changed, 338 insertions(+), 166 deletions(-) create mode 100644 tests/test_sign_message_cmd.py diff --git a/tests/application_client/everscale_command_sender.py b/tests/application_client/everscale_command_sender.py index 921f339..90a16c0 100644 --- a/tests/application_client/everscale_command_sender.py +++ b/tests/application_client/everscale_command_sender.py @@ -145,28 +145,21 @@ def get_address_with_confirmation(self, account_number: int, wallet_type: Wallet @contextmanager - def sign_tx(self, path: str, transaction: bytes) -> Generator[None, None, None]: - self.backend.exchange(cla=CLA, - ins=InsType.SIGN_TX, - p1=P1.P1_START, - p2=P2.P2_MORE, - data=pack_derivation_path(path)) - messages = split_message(transaction, MAX_APDU_LEN) - idx: int = P1.P1_START + 1 - - for msg in messages[:-1]: - self.backend.exchange(cla=CLA, - ins=InsType.SIGN_TX, - p1=idx, - p2=P2.P2_MORE, - data=msg) - idx += 1 + def sign_message(self, account_number: int, wallet_type: WalletType, message: bytes) -> Generator[None, None, None]: + with self.backend.exchange_async(cla=CLA, + ins=InsType.SIGN_MESSAGE, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=account_number.to_bytes(4, "big") + wallet_type.to_bytes(1, "big") + message) as response: + yield response + @contextmanager + def sign_tx(self, account_number: int, wallet_type: WalletType, transaction: bytes) -> Generator[None, None, None]: with self.backend.exchange_async(cla=CLA, ins=InsType.SIGN_TX, - p1=idx, + p1=P1.P1_START, p2=P2.P2_LAST, - data=messages[-1]) as response: + data=account_number.to_bytes(4, "big") + wallet_type.to_bytes(1, "big") + transaction) as response: yield response def get_async_response(self) -> Optional[RAPDU]: diff --git a/tests/application_client/everscale_response_unpacker.py b/tests/application_client/everscale_response_unpacker.py index 24e626d..b08e690 100644 --- a/tests/application_client/everscale_response_unpacker.py +++ b/tests/application_client/everscale_response_unpacker.py @@ -45,7 +45,7 @@ def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: # Unpack from response: # response = pub_key_len (1) # pub_key (var) -def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: +def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes]: response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) assert pub_key_len == 32 @@ -68,6 +68,7 @@ def unpack_get_address_response(response: bytes) -> Tuple[int, bytes]: # response = der_sig_len (1) # der_sig (var) # v (1) +# TODO: check if this needs to be edited def unpack_sign_tx_response(response: bytes) -> Tuple[int, bytes, int]: response, der_sig_len, der_sig = pop_size_prefixed_buf_from_buf(response) response, v = pop_sized_buf_from_buffer(response, 1) diff --git a/tests/application_client/everscale_transaction.py b/tests/application_client/everscale_transaction.py index 9ef3d17..5094dfc 100644 --- a/tests/application_client/everscale_transaction.py +++ b/tests/application_client/everscale_transaction.py @@ -1,50 +1,188 @@ -from io import BytesIO -from typing import Union +from application_client.everscale_command_sender import WalletType -from .everscale_utils import read, read_uint, read_varint, write_varint, UINT64_MAX +class Transaction: + ADDRESS_LENGTH = 32 + CHAIN_ID_LENGTH = 4 + FLAG_WITH_WALLET_ID = 0x01 + FLAG_WITH_WORKCHAIN_ID = 0x02 + FLAG_WITH_ADDRESS = 0x04 + FLAG_WITH_CHAIN_ID = 0x08 -class TransactionError(Exception): - pass + def __init__(self, + decimals: int, + ticker: str, + message: bytes, + current_wallet_type: WalletType, + workchain_id: int | None = None, + prepend_address: bytes | None = None, + chain_id: bytes | None = None) -> None: + """ + Construct a Transaction. + + The metadata byte is deduced as follows: + - FLAG_WITH_WALLET_ID is set if a current_wallet_type is provided and + it differs from the origin_wallet_type. + - FLAG_WITH_WORKCHAIN_ID is set if workchain_id is provided. + - FLAG_WITH_ADDRESS is set if prepend_address is provided. + - FLAG_WITH_CHAIN_ID is set if chain_id is provided. + + If current_wallet_type is None or equals origin_wallet_type, then that field is omitted + and on deserialization the origin_wallet_type will be used. + """ + self.decimals = decimals + self.ticker = ticker + self.message = message + self.workchain_id = workchain_id + self.prepend_address = prepend_address + self.chain_id = chain_id + # Deduce metadata flags based on optional inputs. + metadata = 0 + if current_wallet_type is not None: + metadata |= self.FLAG_WITH_WALLET_ID + self.current_wallet_type = current_wallet_type + else: + # Do not include the field; on the device, it will be set to origin_wallet_type. + self.current_wallet_type = None -class Transaction: - def __init__(self, - nonce: int, - to: Union[str, bytes], - value: int, - memo: str) -> None: - self.nonce: int = nonce - self.to: bytes = bytes.fromhex(to[2:]) if isinstance(to, str) else to - self.value: int = value - self.memo: bytes = memo.encode("ascii") + if workchain_id is not None: + metadata |= self.FLAG_WITH_WORKCHAIN_ID + + if prepend_address is not None: + if len(prepend_address) != self.ADDRESS_LENGTH: + raise ValueError(f"prepend_address must be {self.ADDRESS_LENGTH} bytes") + metadata |= self.FLAG_WITH_ADDRESS - if not 0 <= self.nonce <= UINT64_MAX: - raise TransactionError(f"Bad nonce: '{self.nonce}'!") + if chain_id is not None: + if len(chain_id) != self.CHAIN_ID_LENGTH: + raise ValueError(f"chain_id must be {self.CHAIN_ID_LENGTH} bytes") + metadata |= self.FLAG_WITH_CHAIN_ID - if not 0 <= self.value <= UINT64_MAX: - raise TransactionError(f"Bad value: '{self.value}'!") + self.metadata = metadata - if len(self.to) != 20: - raise TransactionError(f"Bad address: '{self.to.hex()}'!") + # Nice-to-haves: check ticker length constraints. + ticker_len = len(ticker) + if ticker_len == 0 or ticker_len > 10: + raise ValueError("Ticker length must be between 1 and 10 bytes.") def serialize(self) -> bytes: - return b"".join([ - self.nonce.to_bytes(8, byteorder="big"), - self.to, - self.value.to_bytes(8, byteorder="big"), - write_varint(len(self.memo)), - self.memo - ]) + """ + Serialize the transaction into a byte-buffer with the following structure: + + [decimals:1] [ticker_length:1] [ticker:N] [metadata:1] + [optional fields (if flagged)...] [message:remaining bytes] + """ + result = bytearray() + # 1. Decimals (1 byte) + result.append(self.decimals) + + # 2. Ticker: length (1 byte) followed by its ASCII bytes + ticker_bytes = self.ticker.encode("ascii") + ticker_len = len(ticker_bytes) + result.append(ticker_len) + result.extend(ticker_bytes) + + # 3. Metadata (1 byte; deduced from optional parameters) + result.append(self.metadata) + + # 4. Conditionally append optional fields based on metadata flags. + if self.metadata & self.FLAG_WITH_WALLET_ID: + # current_wallet_type is provided and differs from origin_wallet_type. + result.append(self.current_wallet_type) + # Workchain id. + if self.metadata & self.FLAG_WITH_WORKCHAIN_ID: + result.append(self.workchain_id) + # Prepend address. + if self.metadata & self.FLAG_WITH_ADDRESS: + result.extend(self.prepend_address) + # Chain id. + if self.metadata & self.FLAG_WITH_CHAIN_ID: + result.extend(self.chain_id) + + # 5. Append the message (payload). + result.extend(self.message) + + return bytes(result) @classmethod - def from_bytes(cls, hexa: Union[bytes, BytesIO]): - buf: BytesIO = BytesIO(hexa) if isinstance(hexa, bytes) else hexa + def from_bytes(cls, data: bytes) -> "Transaction": + """ + Deserialize a byte-buffer into a Transaction object. + + The parsing order in the buffer is: + decimals (1 byte) -> + ticker_length (1 byte) -> + ticker -> + metadata (1 byte) -> + [optional fields... based on metadata] -> + message (remaining bytes) + + If the metadata does NOT include FLAG_WITH_WALLET_ID, then current_wallet_type + defaults to origin_wallet_type. + """ + offset = 0 + + # Read decimals (1 byte) + if len(data) < offset + 1: + raise ValueError("Data too short for decimals") + decimals = data[offset] + offset += 1 + + # Read ticker: first its length (1 byte) then the ticker string + if len(data) < offset + 1: + raise ValueError("Data too short for ticker length") + ticker_len = data[offset] + offset += 1 + if len(data) < offset + ticker_len: + raise ValueError("Data too short for ticker") + ticker = data[offset:offset+ticker_len].decode("ascii") + offset += ticker_len + + # Read metadata (1 byte) + if len(data) < offset + 1: + raise ValueError("Data too short for metadata") + metadata = data[offset] + offset += 1 + + # Read optional fields based on metadata flags. + current_wallet_type = None + if metadata & cls.FLAG_WITH_WALLET_ID: + if len(data) < offset + 1: + raise ValueError("Data too short for current_wallet_type") + current_wallet_type = data[offset] + offset += 1 + + workchain_id = None + if metadata & cls.FLAG_WITH_WORKCHAIN_ID: + if len(data) < offset + 1: + raise ValueError("Data too short for workchain_id") + workchain_id = data[offset] + offset += 1 + + prepend_address = None + if metadata & cls.FLAG_WITH_ADDRESS: + if len(data) < offset + cls.ADDRESS_LENGTH: + raise ValueError("Data too short for prepend_address") + prepend_address = data[offset:offset+cls.ADDRESS_LENGTH] + offset += cls.ADDRESS_LENGTH + + chain_id = None + if metadata & cls.FLAG_WITH_CHAIN_ID: + if len(data) < offset + cls.CHAIN_ID_LENGTH: + raise ValueError("Data too short for chain_id") + chain_id = data[offset:offset+cls.CHAIN_ID_LENGTH] + offset += cls.CHAIN_ID_LENGTH - nonce: int = read_uint(buf, 64, byteorder="big") - to: bytes = read(buf, 20) - value: int = read_uint(buf, 64, byteorder="big") - memo_len: int = read_varint(buf) - memo: str = read(buf, memo_len).decode("ascii") + # The remaining bytes are the message. + message = data[offset:] - return cls(nonce=nonce, to=to, value=value, memo=memo) + return cls( + decimals=decimals, + ticker=ticker, + message=message, + current_wallet_type=current_wallet_type, + workchain_id=workchain_id, + prepend_address=prepend_address, + chain_id=chain_id + ) \ No newline at end of file diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py index ebc3127..9147a3a 100644 --- a/tests/test_sign_cmd.py +++ b/tests/test_sign_cmd.py @@ -7,7 +7,7 @@ from ragger.navigator.navigation_scenario import NavigateWithScenario from application_client.everscale_transaction import Transaction -from application_client.everscale_command_sender import EverscaleCommandSender, Errors +from application_client.everscale_command_sender import EverscaleCommandSender, Errors, WalletType from application_client.everscale_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response from utils import check_signature_validity @@ -17,129 +17,125 @@ # In this test we send to the device a transaction to sign and validate it on screen # The transaction is short and will be sent in one chunk # We will ensure that the displayed information is correct by using screenshots comparison -def test_sign_tx_short_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: - # Use the app interface instead of raw interface - client = EverscaleCommandSender(backend) - # The path used for this entire test - path: str = "m/44'/1'/0'/0/0" - - # First we need to get the public key of the device in order to build the transaction - rapdu = client.get_public_key(path=path) - _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) - - # Create the transaction that will be sent to the device for signing - transaction = Transaction( - nonce=1, - to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", - value=666, - memo="For u EthDev" - ).serialize() - # Send the sign device instruction. - # As it requires on-screen validation, the function is asynchronous. - # It will yield the result when the navigation is done - with client.sign_tx(path=path, transaction=transaction): - # Validate the on-screen request by performing the navigation appropriate for this device - scenario_navigator.review_approve() - - # The device as yielded the result, parse it and ensure that the signature is correct - response = client.get_async_response().data - _, der_sig, _ = unpack_sign_tx_response(response) - assert check_signature_validity(public_key, der_sig, transaction) - - -# In this test we send to the device a transaction to trig a blind-signing flow -# The transaction is short and will be sent in one chunk -# We will ensure that the displayed information is correct by using screenshots comparison -def test_sign_tx_short_tx_blind_sign(firmware: Firmware, - backend: BackendInterface, - navigator: Navigator, - scenario_navigator: NavigateWithScenario, - test_name: str, - default_screenshot_path: str) -> None: +# TODO: Add a valid raw transaction and a valid expected signature +def test_sign_tx_short_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: # Use the app interface instead of raw interface client = EverscaleCommandSender(backend) - # The path used for this entire test - path: str = "m/44'/1'/0'/0/0" - + account_number = 0 + wallet_type = WalletType.WALLET_V3 # First we need to get the public key of the device in order to build the transaction - rapdu = client.get_public_key(path=path) + rapdu = client.get_public_key(account_number=account_number) _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) - # Create the transaction that will be sent to the device for signing - transaction = Transaction( - nonce=1, - to="0x0000000000000000000000000000000000000000", - value=0, - memo="Blind-sign" - ).serialize() + # Raw transaction + transaction = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000") # Send the sign device instruction. - valid_instruction = [NavInsID.RIGHT_CLICK] if firmware.is_nano else [NavInsID.USE_CASE_CHOICE_REJECT] # As it requires on-screen validation, the function is asynchronous. # It will yield the result when the navigation is done - with client.sign_tx(path=path, transaction=transaction): - navigator.navigate_and_compare(default_screenshot_path, - test_name+"/part1", - valid_instruction, - screen_change_after_last_instruction=False) - + with client.sign_tx(account_number=account_number, wallet_type=wallet_type, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device scenario_navigator.review_approve() # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, der_sig, _ = unpack_sign_tx_response(response) - assert check_signature_validity(public_key, der_sig, transaction) - -# In this test se send to the device a transaction to sign and validate it on screen -# This test is mostly the same as the previous one but with different values. -# In particular the long memo will force the transaction to be sent in multiple chunks -def test_sign_tx_long_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: - # Use the app interface instead of raw interface - client = EverscaleCommandSender(backend) - path: str = "m/44'/1'/0'/0/0" - - rapdu = client.get_public_key(path=path) - _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) - - transaction = Transaction( - nonce=1, - to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", - value=666, - memo=("This is a very long memo. " - "It will force the app client to send the serialized transaction to be sent in chunk. " - "As the maximum chunk size is 255 bytes we will make this memo greater than 255 characters. " - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, " - "dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam.") - ).serialize() - - with client.sign_tx(path=path, transaction=transaction): - scenario_navigator.review_approve() - - response = client.get_async_response().data - _, der_sig, _ = unpack_sign_tx_response(response) - assert check_signature_validity(public_key, der_sig, transaction) - - -# Transaction signature refused test -# The test will ask for a transaction signature that will be refused on screen -def test_sign_tx_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: - # Use the app interface instead of raw interface - client = EverscaleCommandSender(backend) - path: str = "m/44'/1'/0'/0/0" - - transaction = Transaction( - nonce=1, - to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", - value=666, - memo="This transaction will be refused by the user" - ).serialize() - - with pytest.raises(ExceptionRAPDU) as e: - with client.sign_tx(path=path, transaction=transaction): - scenario_navigator.review_reject() - - # Assert that we have received a refusal - assert e.value.status == Errors.SW_DENY - assert len(e.value.data) == 0 + assert der_sig.hex() == "0000000000000000000000000000000000000000000000000000000000000000" + + +# # In this test we send to the device a transaction to trig a blind-signing flow +# # The transaction is short and will be sent in one chunk +# # We will ensure that the displayed information is correct by using screenshots comparison +# def test_sign_tx_short_tx_blind_sign(firmware: Firmware, +# backend: BackendInterface, +# navigator: Navigator, +# scenario_navigator: NavigateWithScenario, +# test_name: str, +# default_screenshot_path: str) -> None: +# # Use the app interface instead of raw interface +# client = EverscaleCommandSender(backend) +# # The path used for this entire test +# path: str = "m/44'/1'/0'/0/0" + +# # First we need to get the public key of the device in order to build the transaction +# rapdu = client.get_public_key(path=path) +# _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + +# # Create the transaction that will be sent to the device for signing +# transaction = Transaction( +# nonce=1, +# to="0x0000000000000000000000000000000000000000", +# value=0, +# memo="Blind-sign" +# ).serialize() + +# # Send the sign device instruction. +# valid_instruction = [NavInsID.RIGHT_CLICK] if firmware.is_nano else [NavInsID.USE_CASE_CHOICE_REJECT] +# # As it requires on-screen validation, the function is asynchronous. +# # It will yield the result when the navigation is done +# with client.sign_tx(path=path, transaction=transaction): +# navigator.navigate_and_compare(default_screenshot_path, +# test_name+"/part1", +# valid_instruction, +# screen_change_after_last_instruction=False) + +# # Validate the on-screen request by performing the navigation appropriate for this device +# scenario_navigator.review_approve() + +# # The device as yielded the result, parse it and ensure that the signature is correct +# response = client.get_async_response().data +# _, der_sig, _ = unpack_sign_tx_response(response) +# assert check_signature_validity(public_key, der_sig, transaction) + +# # In this test se send to the device a transaction to sign and validate it on screen +# # This test is mostly the same as the previous one but with different values. +# # In particular the long memo will force the transaction to be sent in multiple chunks +# def test_sign_tx_long_tx(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: +# # Use the app interface instead of raw interface +# client = EverscaleCommandSender(backend) +# path: str = "m/44'/1'/0'/0/0" + +# rapdu = client.get_public_key(path=path) +# _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + +# transaction = Transaction( +# nonce=1, +# to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", +# value=666, +# memo=("This is a very long memo. " +# "It will force the app client to send the serialized transaction to be sent in chunk. " +# "As the maximum chunk size is 255 bytes we will make this memo greater than 255 characters. " +# "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, " +# "dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam.") +# ).serialize() + +# with client.sign_tx(path=path, transaction=transaction): +# scenario_navigator.review_approve() + +# response = client.get_async_response().data +# _, der_sig, _ = unpack_sign_tx_response(response) +# assert check_signature_validity(public_key, der_sig, transaction) + + +# # Transaction signature refused test +# # The test will ask for a transaction signature that will be refused on screen +# def test_sign_tx_refused(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: +# # Use the app interface instead of raw interface +# client = EverscaleCommandSender(backend) +# path: str = "m/44'/1'/0'/0/0" + +# transaction = Transaction( +# nonce=1, +# to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", +# value=666, +# memo="This transaction will be refused by the user" +# ).serialize() + +# with pytest.raises(ExceptionRAPDU) as e: +# with client.sign_tx(path=path, transaction=transaction): +# scenario_navigator.review_reject() + +# # Assert that we have received a refusal +# assert e.value.status == Errors.SW_DENY +# assert len(e.value.data) == 0 diff --git a/tests/test_sign_message_cmd.py b/tests/test_sign_message_cmd.py new file mode 100644 index 0000000..befb48a --- /dev/null +++ b/tests/test_sign_message_cmd.py @@ -0,0 +1,44 @@ +import pytest + +from ragger.backend.interface import BackendInterface +from ragger.error import ExceptionRAPDU +from ragger.firmware import Firmware +from ragger.navigator import Navigator, NavInsID +from ragger.navigator.navigation_scenario import NavigateWithScenario + +from application_client.everscale_transaction import Transaction +from application_client.everscale_command_sender import EverscaleCommandSender, Errors, WalletType +from application_client.everscale_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response +from utils import check_signature_validity + +# In this tests we check the behavior of the device when asked to sign a transaction + + +# In this test we send to the device a transaction to sign and validate it on screen +# The transaction is short and will be sent in one chunk +# We will ensure that the displayed information is correct by using screenshots comparison + +# TODO: Add a valid raw message and a valid expected signature +def test_sign_message(backend: BackendInterface, scenario_navigator: NavigateWithScenario) -> None: + # Use the app interface instead of raw interface + client = EverscaleCommandSender(backend) + account_number = 0 + wallet_type = WalletType.WALLET_V3 + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(account_number=account_number) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + # Message to sign + message = b"Hello, world!" + + # Send the sign device instruction. + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_message(account_number=account_number, wallet_type=wallet_type, message=message): + # Validate the on-screen request by performing the navigation appropriate for this device + scenario_navigator.review_approve() + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + assert der_sig.hex() == "0000000000000000000000000000000000000000000000000000000000000000" \ No newline at end of file