From 3b35d0d946ce7d109efc924f90757f0f9a1fe930 Mon Sep 17 00:00:00 2001 From: Alexis Grojean Date: Tue, 7 Nov 2023 17:41:15 +0100 Subject: [PATCH] Add Ragger tests and remove obsolete functional tests. --- test/menu.apdu | 1 - test/overflow.apdu | 1 - test/quit.apdu | 1 - test/sign.apdu | 1 - test/test_cmds.py | 31 ---- tests/application_client/__init__.py | 0 .../boilerplate_command_sender.py | 127 ++++++++++++++++ .../boilerplate_response_unpacker.py | 69 +++++++++ .../boilerplate_transaction.py | 52 +++++++ tests/application_client/boilerplate_utils.py | 61 ++++++++ tests/application_client/py.typed | 0 tests/boil-freeze.txt | 60 ++++++++ tests/conftest.py | 15 ++ tests/requirements.txt | 4 + tests/setup.cfg | 21 +++ .../nanos/test_app_mainmenu/00000.png | Bin 0 -> 455 bytes .../nanos/test_app_mainmenu/00001.png | Bin 0 -> 309 bytes .../nanos/test_app_mainmenu/00002.png | Bin 0 -> 327 bytes .../nanos/test_app_mainmenu/00003.png | Bin 0 -> 274 bytes .../00000.png | Bin 0 -> 375 bytes .../00001.png | Bin 0 -> 517 bytes .../00002.png | Bin 0 -> 555 bytes .../00003.png | Bin 0 -> 530 bytes .../00004.png | Bin 0 -> 548 bytes .../00005.png | Bin 0 -> 535 bytes .../00006.png | Bin 0 -> 535 bytes .../00007.png | Bin 0 -> 500 bytes .../00008.png | Bin 0 -> 413 bytes .../00009.png | Bin 0 -> 341 bytes .../00010.png | Bin 0 -> 455 bytes .../00000.png | Bin 0 -> 375 bytes .../00001.png | Bin 0 -> 517 bytes .../00002.png | Bin 0 -> 555 bytes .../00003.png | Bin 0 -> 530 bytes .../00004.png | Bin 0 -> 548 bytes .../00005.png | Bin 0 -> 535 bytes .../00006.png | Bin 0 -> 535 bytes .../00007.png | Bin 0 -> 500 bytes .../00008.png | Bin 0 -> 413 bytes .../00009.png | Bin 0 -> 341 bytes .../00010.png | Bin 0 -> 340 bytes .../00011.png | Bin 0 -> 455 bytes tests/test_app_mainmenu.py | 21 +++ tests/test_appname_cmd.py | 12 ++ tests/test_error_cmd.py | 56 +++++++ tests/test_name_version.py | 15 ++ tests/test_pubkey_cmd.py | 87 +++++++++++ tests/test_sign_cmd.py | 141 ++++++++++++++++++ tests/test_version_cmd.py | 16 ++ tests/usage.md | 74 +++++++++ tests/utils.py | 23 +++ 51 files changed, 854 insertions(+), 35 deletions(-) delete mode 100644 test/menu.apdu delete mode 100644 test/overflow.apdu delete mode 100644 test/quit.apdu delete mode 100644 test/sign.apdu delete mode 100644 test/test_cmds.py 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/boil-freeze.txt create mode 100644 tests/conftest.py create mode 100644 tests/requirements.txt create mode 100644 tests/setup.cfg create mode 100644 tests/snapshots/nanos/test_app_mainmenu/00000.png create mode 100644 tests/snapshots/nanos/test_app_mainmenu/00001.png create mode 100644 tests/snapshots/nanos/test_app_mainmenu/00002.png create mode 100644 tests/snapshots/nanos/test_app_mainmenu/00003.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00005.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00006.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00007.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00008.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00009.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_accepted/00010.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00005.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00007.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00008.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00009.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00010.png create mode 100644 tests/snapshots/nanos/test_get_public_key_confirm_refused/00011.png 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/test/menu.apdu b/test/menu.apdu deleted file mode 100644 index 9135215..0000000 --- a/test/menu.apdu +++ /dev/null @@ -1 +0,0 @@ -8004 \ No newline at end of file diff --git a/test/overflow.apdu b/test/overflow.apdu deleted file mode 100644 index eacb2f7..0000000 --- a/test/overflow.apdu +++ /dev/null @@ -1 +0,0 @@ -80050008 \ No newline at end of file diff --git a/test/quit.apdu b/test/quit.apdu deleted file mode 100644 index d8d26d5..0000000 --- a/test/quit.apdu +++ /dev/null @@ -1 +0,0 @@ -80FF \ No newline at end of file diff --git a/test/sign.apdu b/test/sign.apdu deleted file mode 100644 index 5cbddb7..0000000 --- a/test/sign.apdu +++ /dev/null @@ -1 +0,0 @@ -800300002000112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210 \ No newline at end of file diff --git a/test/test_cmds.py b/test/test_cmds.py deleted file mode 100644 index 80d9260..0000000 --- a/test/test_cmds.py +++ /dev/null @@ -1,31 +0,0 @@ -from ledgerblue.commTCP import getDongle as getDongleTCP -from ledgerblue.comm import getDongle - -from random import getrandbits as rnd -from binascii import hexlify, unhexlify - -rand_msg = hexlify(rnd(256).to_bytes(32, 'big')).decode() - -CMDS = [ - "8002", - "8003000020" + "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210", - "8003000020" + rand_msg, - "8004", - "80050008", - "80FE", - "80FF", -] - -d = getDongleTCP(port=9999) # Speculos -# d = getDongle() # Nano - -from time import sleep -for cmd in map(unhexlify,CMDS): - r = None - try: - r = d.exchange(cmd, 20) - sleep(1) - except Exception as e: - print(e) - if r is not None: - print("Response : ", hexlify(r)) 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..4e6fc9f --- /dev/null +++ b/tests/application_client/boilerplate_response_unpacker.py @@ -0,0 +1,69 @@ +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..02bd01f --- /dev/null +++ b/tests/application_client/boilerplate_transaction.py @@ -0,0 +1,52 @@ +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, + do_check: bool = True) -> 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 do_check: + 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/boil-freeze.txt b/tests/boil-freeze.txt new file mode 100644 index 0000000..a74fc92 --- /dev/null +++ b/tests/boil-freeze.txt @@ -0,0 +1,60 @@ +aniso8601==9.0.1 +asn1crypto==1.5.1 +attrs==22.2.0 +bip-utils==2.7.0 +cbor2==5.4.6 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.0.1 +click==8.1.3 +coincurve==17.0.0 +construct==2.10.68 +crcmod==1.7 +cryptography==39.0.1 +ecdsa==0.16.1 +ed25519-blake2b==1.4 +exceptiongroup==1.1.0 +Flask==2.1.2 +Flask-RESTful==0.3.9 +hidapi==0.13.1 +idna==3.4 +importlib-metadata==6.0.0 +importlib-resources==5.12.0 +iniconfig==2.0.0 +intelhex==2.3.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jsonschema==4.17.3 +ledgerwallet==0.2.3 +MarkupSafe==2.1.2 +mnemonic==0.20 +packaging==23.0 +Pillow==9.4.0 +pkg_resources==0.0.0 +pkgutil_resolve_name==1.3.10 +pluggy==1.0.0 +protobuf==3.20.3 +py-sr25519-bindings==0.1.4 +pycparser==2.21 +pycryptodome==3.17 +pyelftools==0.29 +PyNaCl==1.5.0 +PyQt5==5.15.9 +PyQt5-Qt5==5.15.2 +PyQt5-sip==12.11.1 +pyrsistent==0.19.3 +pysha3==1.0.2 +pytesseract==0.3.10 +pytest==7.2.1 +pytz==2022.7.1 +ragger==1.6.0 +requests==2.28.2 +semver==2.13.0 +six==1.16.0 +speculos==0.1.224 +tabulate==0.9.0 +toml==0.10.2 +tomli==2.0.1 +urllib3==1.26.14 +Werkzeug==2.2.3 +zipp==3.15.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..909ec8b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +from ragger.conftest import configuration + +########################### +### 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..0913153 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest +ragger[speculos,ledgerwallet]>=1.11.4 +ecdsa>=0.16.1,<0.17.0 +pysha3>=1.0.0,<2.0.0 diff --git a/tests/setup.cfg b/tests/setup.cfg new file mode 100644 index 0000000..7d0d7e3 --- /dev/null +++ b/tests/setup.cfg @@ -0,0 +1,21 @@ +[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 +max-line-length=100 +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/snapshots/nanos/test_app_mainmenu/00000.png b/tests/snapshots/nanos/test_app_mainmenu/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..68ffb394818b8a1144c7bdffd0e413ebb828eca8 GIT binary patch literal 455 zcmV;&0XY7NP)ktdMRJ7$jtQV?E0Jw2 zEX<>cv$fC(3cFo%Dj&aPIUE4ZO{b?Km?d}fT6HH2&km6xtYY2t?0?Vdg78W$~UYw9Gu>A)BWT- z@}KCar_^|OW15hRAhQF2ZeR=QhlZ+`4$E`Eas@aBM%J9qx?7F%p>;S934?&BM2?}J zlQ_IdDoMLegCO&ry@(xg1W(hjcYU6aul_=@0gb}dlu*bos`+p@Zo;(uu+MEe@Hx8( z6#{B>R8eT>+?LGx?SfhmU2dMPhSs@oxE!RZhALO+^5VzU*c}iW!q%g(soRz@=ckE) x>DmKY`<^6J1d`P&@3BZxxL8|~DvF{=%pcj5^`~Z2UDE&n002ovPDHLkV1l(T)B*qi literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_app_mainmenu/00001.png b/tests/snapshots/nanos/test_app_mainmenu/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..23b5eb1e71187fe3135a142873701080f43548f6 GIT binary patch literal 309 zcmV-50m}Y~P)sTVe;Ey_ss6>#15a3mJmGiPlP%>P?nGC+$O1{krj(r z5|Afh4bB(BsaXn->`fXq<*($eu{7a@cYLdXjbwWf(xm1<&z00000NkvXX Hu0mjftNnp} literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_app_mainmenu/00002.png b/tests/snapshots/nanos/test_app_mainmenu/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..3476b972aad8b16723e63d03d83db7de63b698cd GIT binary patch literal 327 zcmV-N0l5B&P)q%Lk}$JN=*+Ya3V52_$ri_Tw`0y zW~o~dSBJ_)jqx$FVQ-p{pr~clG9JlqkTUKDen}9lbG=+9r&_RRCT`@ta9}o@5 zoGR6f6v&VngBn2&rF;h11n5AoQ(n^?5wLi<0+<0A9oYrVM?&+_T6|lSEB|J;2qA=^ ZvjZ`uW$88COrih)002ovPDHLkV1i+HiM9X$ literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_app_mainmenu/00003.png b/tests/snapshots/nanos/test_app_mainmenu/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..e2279803e79ec6d6443deff45a1f9326b562e303 GIT binary patch literal 274 zcmV+t0qy>YP)D zPq`+h_bpEEKgJ+x(tj8nSi&r1$%l|}@K=b}8w!8IP1np9%K2s%)5~a1UA{VmQ8=+t z-ij#s)$}gEr*9l(42Z6SzZpj z4zQ3%^HmGlPh~1v2GAk(f;HI^<5u-M*+NCj@{yh^gq{Uirn7i#*V}GdEKhr=Zw7}* zt5j31N!mTC(DIvvE9c@ezzQfgzOhWK!PxiBAiv5j&VV}*elA@@y9Ib(^y^EIU|^3Q$%tW{d*b$=;&(vHQyOux1*O|6wz}NGtxk0|_8)0e-jJm90ssJT3tq77 VvGI(pU6=p>002ovPDHLkV1n$ksMG)e literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..dce1ab738131945fb8fab9d2430efceb1d35d823 GIT binary patch literal 517 zcmV+g0{Z=lP)J59)GMhFOU7v7^kr*))=~vlD&iBeOp>r7v*4K=i= zF=2JPIr36PHE3l^4dPy8K`KrF4!B$Cb_+$H?ohf_{n|2s^Ea(Y#j%yDiUcHkCqMF}do1EMRcAz+?ZVt3+qOVGs08JJGIcLMvXv?qI9mH@Rs1NS2> zxJwn*DS}nVuO#xhbq7SVd;1evjgya(3nO9VUH(uBx&u`eCTiy|+)PCjHbv#!*0Fy< zNB<-p{ga63uco0+o0S*@BGaMe*gzd9MMp`Xo!shdmHcfl-}ULg((RXTwnSLU;Et)2 z&h+Xl2U4SmpR8KrTz|71Wa&?>pK6o@@dQ|}xa{3hK8X&f9j%0`(w{1^%++F~MAnE7M1s_OTOuI{+eG)>bq%`JEVuTyh5`k2or00000NkvXX Hu0mjf8Y%Bd literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..81a18385deaf8fa9fbdfba064270163d8185aea2 GIT binary patch literal 555 zcmV+`0@VG9P)n%W~3qekL|cma|N|=rU4Fs=)kAYnpUN<)U`h zEE87Onjc~f(-yU4o+BX<00x;3Hqk>!O zlJq09X8ld$q@V;VQqYn!Aljoke&&fKrgD6?QF(6)>Nqh2!-?OWLfJ-!@L)s$RxJee zat0cw*>?}qt$6g8bj`>jZ?`ZSBkJoU7%uAtNkrEb^4IP12{*Bdy7oP13)eAV^&`< z>@M{^OWQ3wNak^VSqaJ#qcSqhs-~FF)<_9J0NC(`wLs*(Vxn5`PMkUfQpNJe{>mr- zewPT*Q_=B+8rigpf3ugoZ~3?y+{lz0fc)$)gk>l89wOU61Yig3>;qFD8Y$LWAZ?+i tC2l7V_&gxp^M}xx8);D#MNt&z@C2MRi1JxtPci@i002ovPDHLkV1lN>0tNs8 literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..30a64c8a3823888e7505ded812807c6bd6f76cc9 GIT binary patch literal 530 zcmV+t0`2{YP)RF|3td=~LLL_qVt2~stSkkBKuDTyttg73D2h|~Nohas@5cTdNhUzQ zR0Xh|zPhUgtT9s&Cz)6`GR19LQ*k9}WX?_|`fPO>Wx7PqgcOr1VScAIO}eA9s6ov# zVNjB)yi`#Qj%F(n*&r)+K%C(Jr?oZ|eOjKhPJulA1}CW-WMbC7iLoL96KeyNxV0`x zKTOxG>@C(F%0-;9q6IBE14dU=|3CA@65EMmTTln&ESS#t-D$4-4>^-SBw$s^sF$;# zaoTNN6?RPwb|KkqSaSz=q0I5___qCq9R}nhb6{lbn8M!|{pLkyRRR{MTrm3Qa+MtH z$+f!BZd~ph-U<=k@HEt&)mW3lW?a$OJrz$2+|dy8%5uORpsE59Bl*!RLO%4%pMbC` zqfQeomB_w~coH|rbX9p^Kg=mIhvZh#^Y1ZD?GBw()mn-(B7 zzAnd4VL!mJJAm!kuZUohSOU~$x=?H4c1VwLtK(ZQCV`#E$q9@A_llw@ilW$yCk)VU UM#UnFQ2+n{07*qoM6N<$g06z>5&!@I literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..e96693e1f1edba4dd42aef810806edf3fd91843d GIT binary patch literal 548 zcmV+<0^9wGP)7kNEc@E&+L|x_-IWS;@vCSUGU@#aA27{O2qptdV{AKzMB$)sm zR0VvVWj)k#*c_L9qf|`tWbj$moP(SyRXIQf8BrvrJe` zG)G>lsDy=RHC7fxdk5eE=T8$YwfZzSX_7kfbR=|daWZweAu?zOpkUZh!P%B1{joP; zt!>m>afWsY+HwYDEvl1eo>*cMbfX;ILp6&FOVENdJ#gmu-3gIl<&FxU9|;kFEjvq1 zB9NJu+E9hv5y2{i)DZcvbq1tP_`tpM5B*Ew#AD-uk)<+)|Jmvv(b8-Tzy_7Ot^Uh# zix|8uZd8Td5cfKV-wk4U?QW=S-KfnR=MqLQqgEf0j$|90Aiv{n_~uHPkOKXdE$3w; z>ovdLR&uIf?+iB~$B{R7`>vJj1%a=B zFfic`RkuIRFWIF3YB{oasKdW(>erhiQFEdIFdMAj+l1N@u*z;9&qvGu&?`&8HRqO~ zf}?C>f|={cu&)A5eguw*8k_5ty#tzQQeend_FXG}o(VwREkVs|q7U%1<;E(M{ZPsf mN;=ncgnuv?3EXnDVW{*rwMmdt=Q zss=vK>OJ&w*ccUKQ@f)!i=m3ojx&#@NmAqKsE@?J>O&?&@?`MUan`Y7l|}8;EHk!B z(z2H>Dq$*HJ!L`cm;pGz@zZiksZT4Ema1dVXf4qf8*`KsB8eRdD0J_*;B3p1KGd|3 znYJcSTXBXROVE}*AlsuV0gJ>kyA%F*30iP+2Bs4~I~7*{R3tr!1nepq^?U}B(*-wl zVJAg!6=Ew1^HL8;hqeb~vm3;A`w5$dlaKO-(IheL-%tG`5@<^T_IN!|{cG`*9NZJn zT7~Y0$L+(n0xGY6o8MgUW>#Y=0QIVF<28oMmPvs6aeKgjCzy^e_h$&D zl8sVyYL5$d>LRbRiROe;HW+XtG$|dqGv3HmNn>zy}3utNm5maq_v#?RJ$wr?a0TODNd@vXc27|!` Z_yx7>tR;yF1~32s002ovPDHLkV1mOn?kE5N literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00006.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00006.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d4f0db0fe698d60220a0a2e3a21ace0f185dfc GIT binary patch literal 535 zcmV+y0_gpTP)2G7B9p$wT1}c7Q zU6Q`Y6s$a3M%yU2;*9hWwB!zmdQ^Wri)19W881A7Iv{7xbozTIXMdH9CxJ-7lB!WV zGpA*G)S4=+M|7S-Y9q@IcWGyI2ZVBLg!dQrYygmtbbS_|xTNLT&b+)|(tWiYa5b?$(PFdkX`Jiz2Uyw`@uR37!nHhryYRb~^AU%ot{ z0NBYMiXYsoX4)(-FW&)IA#Z{`c;+RnUU%|&Ar@=D&p?1yW;H!VJpG0G6t*aeq9}@e Z`2rO`lsc<-N}~V(002ovPDHLkV1i8R@h<=X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00007.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..cfde67b4d8d5a664e05496ff1b161e39d1de0d91 GIT binary patch literal 500 zcmVICO{ii z0h?#^zSJ_<7?mVcd!#ahB!sW9rZSWyNsK46`mFaE6>|}33}!zMzQdYRjDbq0Hhh)| ztG;H)OBI#SYpuk}f~eR5IKcT+UvsTKbx-Q2hP-$1?-$oqgBXko0|fduRB*N-Ngpyk zYi}AO1SL=rf=bQ+Z;$Hy*+^z$JK=Xj(12kx(42Vg6pQ|@!6YCIP~|deat4yqXuDKl zX(AYfW|@qd1;|lJN!Rff{rm@E*duvhgn6dI|8Mn=)#_9kpg`q@tA8!-VuL+#uPC$| zZg&oE1+l#TYA9Izv(r$#<^v9CM(Zlj##$upxqlDf7O}f%Bvsd;5thSUqsIP`Fup5f*1Mq#v z%Ed`G#%Z*ilw71~>n>!%MOtZ$GY+xy4#w;60~!fnX(&Ba>|^Jc2L#4L$<@nxLs5N2 q(nrY|NIL0&!HG8-jYgx<*bW~+h<0}@GM`xh00004%P)9b)2)g92paL(i9{cLcOOt;HBu#1jH;My^e^WYIY`Tm zx==#ywGU^5gg0;-dK#9SqTqSI7{XpTs51Mo`ZhEP>V0Y&_Dq2K8pHOhlAF6q9}@@C@$p-4n^@LW*~M$00000NkvXX Hu0mjfmP@>b literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00009.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..66c411c2ebc833c701039f213ad4ff68cc881146 GIT binary patch literal 341 zcmV-b0jmCqP)_wxG+Yry2qARt#sf<#$}=)VJ~ zBlvESR-Jq3=$jydfC?r!$iuAw9oGJ!U-M%YMJ+y&Ch%TCM^JUq5BGkf8O_{y`#mQ} nq4{V^xv44i3V1v$0C_ea3_go|-rj`700000NkvXXu0mjfd&QW1 literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00010.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00010.png new file mode 100644 index 0000000000000000000000000000000000000000..68ffb394818b8a1144c7bdffd0e413ebb828eca8 GIT binary patch literal 455 zcmV;&0XY7NP)ktdMRJ7$jtQV?E0Jw2 zEX<>cv$fC(3cFo%Dj&aPIUE4ZO{b?Km?d}fT6HH2&km6xtYY2t?0?Vdg78W$~UYw9Gu>A)BWT- z@}KCar_^|OW15hRAhQF2ZeR=QhlZ+`4$E`Eas@aBM%J9qx?7F%p>;S934?&BM2?}J zlQ_IdDoMLegCO&ry@(xg1W(hjcYU6aul_=@0gb}dlu*bos`+p@Zo;(uu+MEe@Hx8( z6#{B>R8eT>+?LGx?SfhmU2dMPhSs@oxE!RZhALO+^5VzU*c}iW!q%g(soRz@=ckE) x>DmKY`<^6J1d`P&@3BZxxL8|~DvF{=%pcj5^`~Z2UDE&n002ovPDHLkV1l(T)B*qi literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..4eea762449cd8605b155b45a55ede17581272d0f GIT binary patch literal 375 zcmV--0f_#IP)$}gEr*9l(42Z6SzZpj z4zQ3%^HmGlPh~1v2GAk(f;HI^<5u-M*+NCj@{yh^gq{Uirn7i#*V}GdEKhr=Zw7}* zt5j31N!mTC(DIvvE9c@ezzQfgzOhWK!PxiBAiv5j&VV}*elA@@y9Ib(^y^EIU|^3Q$%tW{d*b$=;&(vHQyOux1*O|6wz}NGtxk0|_8)0e-jJm90ssJT3tq77 VvGI(pU6=p>002ovPDHLkV1n$ksMG)e literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..dce1ab738131945fb8fab9d2430efceb1d35d823 GIT binary patch literal 517 zcmV+g0{Z=lP)J59)GMhFOU7v7^kr*))=~vlD&iBeOp>r7v*4K=i= zF=2JPIr36PHE3l^4dPy8K`KrF4!B$Cb_+$H?ohf_{n|2s^Ea(Y#j%yDiUcHkCqMF}do1EMRcAz+?ZVt3+qOVGs08JJGIcLMvXv?qI9mH@Rs1NS2> zxJwn*DS}nVuO#xhbq7SVd;1evjgya(3nO9VUH(uBx&u`eCTiy|+)PCjHbv#!*0Fy< zNB<-p{ga63uco0+o0S*@BGaMe*gzd9MMp`Xo!shdmHcfl-}ULg((RXTwnSLU;Et)2 z&h+Xl2U4SmpR8KrTz|71Wa&?>pK6o@@dQ|}xa{3hK8X&f9j%0`(w{1^%++F~MAnE7M1s_OTOuI{+eG)>bq%`JEVuTyh5`k2or00000NkvXX Hu0mjf8Y%Bd literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..81a18385deaf8fa9fbdfba064270163d8185aea2 GIT binary patch literal 555 zcmV+`0@VG9P)n%W~3qekL|cma|N|=rU4Fs=)kAYnpUN<)U`h zEE87Onjc~f(-yU4o+BX<00x;3Hqk>!O zlJq09X8ld$q@V;VQqYn!Aljoke&&fKrgD6?QF(6)>Nqh2!-?OWLfJ-!@L)s$RxJee zat0cw*>?}qt$6g8bj`>jZ?`ZSBkJoU7%uAtNkrEb^4IP12{*Bdy7oP13)eAV^&`< z>@M{^OWQ3wNak^VSqaJ#qcSqhs-~FF)<_9J0NC(`wLs*(Vxn5`PMkUfQpNJe{>mr- zewPT*Q_=B+8rigpf3ugoZ~3?y+{lz0fc)$)gk>l89wOU61Yig3>;qFD8Y$LWAZ?+i tC2l7V_&gxp^M}xx8);D#MNt&z@C2MRi1JxtPci@i002ovPDHLkV1lN>0tNs8 literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..30a64c8a3823888e7505ded812807c6bd6f76cc9 GIT binary patch literal 530 zcmV+t0`2{YP)RF|3td=~LLL_qVt2~stSkkBKuDTyttg73D2h|~Nohas@5cTdNhUzQ zR0Xh|zPhUgtT9s&Cz)6`GR19LQ*k9}WX?_|`fPO>Wx7PqgcOr1VScAIO}eA9s6ov# zVNjB)yi`#Qj%F(n*&r)+K%C(Jr?oZ|eOjKhPJulA1}CW-WMbC7iLoL96KeyNxV0`x zKTOxG>@C(F%0-;9q6IBE14dU=|3CA@65EMmTTln&ESS#t-D$4-4>^-SBw$s^sF$;# zaoTNN6?RPwb|KkqSaSz=q0I5___qCq9R}nhb6{lbn8M!|{pLkyRRR{MTrm3Qa+MtH z$+f!BZd~ph-U<=k@HEt&)mW3lW?a$OJrz$2+|dy8%5uORpsE59Bl*!RLO%4%pMbC` zqfQeomB_w~coH|rbX9p^Kg=mIhvZh#^Y1ZD?GBw()mn-(B7 zzAnd4VL!mJJAm!kuZUohSOU~$x=?H4c1VwLtK(ZQCV`#E$q9@A_llw@ilW$yCk)VU UM#UnFQ2+n{07*qoM6N<$g06z>5&!@I literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..e96693e1f1edba4dd42aef810806edf3fd91843d GIT binary patch literal 548 zcmV+<0^9wGP)7kNEc@E&+L|x_-IWS;@vCSUGU@#aA27{O2qptdV{AKzMB$)sm zR0VvVWj)k#*c_L9qf|`tWbj$moP(SyRXIQf8BrvrJe` zG)G>lsDy=RHC7fxdk5eE=T8$YwfZzSX_7kfbR=|daWZweAu?zOpkUZh!P%B1{joP; zt!>m>afWsY+HwYDEvl1eo>*cMbfX;ILp6&FOVENdJ#gmu-3gIl<&FxU9|;kFEjvq1 zB9NJu+E9hv5y2{i)DZcvbq1tP_`tpM5B*Ew#AD-uk)<+)|Jmvv(b8-Tzy_7Ot^Uh# zix|8uZd8Td5cfKV-wk4U?QW=S-KfnR=MqLQqgEf0j$|90Aiv{n_~uHPkOKXdE$3w; z>ovdLR&uIf?+iB~$B{R7`>vJj1%a=B zFfic`RkuIRFWIF3YB{oasKdW(>erhiQFEdIFdMAj+l1N@u*z;9&qvGu&?`&8HRqO~ zf}?C>f|={cu&)A5eguw*8k_5ty#tzQQeend_FXG}o(VwREkVs|q7U%1<;E(M{ZPsf mN;=ncgnuv?3EXnDVW{*rwMmdt=Q zss=vK>OJ&w*ccUKQ@f)!i=m3ojx&#@NmAqKsE@?J>O&?&@?`MUan`Y7l|}8;EHk!B z(z2H>Dq$*HJ!L`cm;pGz@zZiksZT4Ema1dVXf4qf8*`KsB8eRdD0J_*;B3p1KGd|3 znYJcSTXBXROVE}*AlsuV0gJ>kyA%F*30iP+2Bs4~I~7*{R3tr!1nepq^?U}B(*-wl zVJAg!6=Ew1^HL8;hqeb~vm3;A`w5$dlaKO-(IheL-%tG`5@<^T_IN!|{cG`*9NZJn zT7~Y0$L+(n0xGY6o8MgUW>#Y=0QIVF<28oMmPvs6aeKgjCzy^e_h$&D zl8sVyYL5$d>LRbRiROe;HW+XtG$|dqGv3HmNn>zy}3utNm5maq_v#?RJ$wr?a0TODNd@vXc27|!` Z_yx7>tR;yF1~32s002ovPDHLkV1mOn?kE5N literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d4f0db0fe698d60220a0a2e3a21ace0f185dfc GIT binary patch literal 535 zcmV+y0_gpTP)2G7B9p$wT1}c7Q zU6Q`Y6s$a3M%yU2;*9hWwB!zmdQ^Wri)19W881A7Iv{7xbozTIXMdH9CxJ-7lB!WV zGpA*G)S4=+M|7S-Y9q@IcWGyI2ZVBLg!dQrYygmtbbS_|xTNLT&b+)|(tWiYa5b?$(PFdkX`Jiz2Uyw`@uR37!nHhryYRb~^AU%ot{ z0NBYMiXYsoX4)(-FW&)IA#Z{`c;+RnUU%|&Ar@=D&p?1yW;H!VJpG0G6t*aeq9}@e Z`2rO`lsc<-N}~V(002ovPDHLkV1i8R@h<=X literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00007.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..cfde67b4d8d5a664e05496ff1b161e39d1de0d91 GIT binary patch literal 500 zcmVICO{ii z0h?#^zSJ_<7?mVcd!#ahB!sW9rZSWyNsK46`mFaE6>|}33}!zMzQdYRjDbq0Hhh)| ztG;H)OBI#SYpuk}f~eR5IKcT+UvsTKbx-Q2hP-$1?-$oqgBXko0|fduRB*N-Ngpyk zYi}AO1SL=rf=bQ+Z;$Hy*+^z$JK=Xj(12kx(42Vg6pQ|@!6YCIP~|deat4yqXuDKl zX(AYfW|@qd1;|lJN!Rff{rm@E*duvhgn6dI|8Mn=)#_9kpg`q@tA8!-VuL+#uPC$| zZg&oE1+l#TYA9Izv(r$#<^v9CM(Zlj##$upxqlDf7O}f%Bvsd;5thSUqsIP`Fup5f*1Mq#v z%Ed`G#%Z*ilw71~>n>!%MOtZ$GY+xy4#w;60~!fnX(&Ba>|^Jc2L#4L$<@nxLs5N2 q(nrY|NIL0&!HG8-jYgx<*bW~+h<0}@GM`xh00004%P)9b)2)g92paL(i9{cLcOOt;HBu#1jH;My^e^WYIY`Tm zx==#ywGU^5gg0;-dK#9SqTqSI7{XpTs51Mo`ZhEP>V0Y&_Dq2K8pHOhlAF6q9}@@C@$p-4n^@LW*~M$00000NkvXX Hu0mjfmP@>b literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00009.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..66c411c2ebc833c701039f213ad4ff68cc881146 GIT binary patch literal 341 zcmV-b0jmCqP)_wxG+Yry2qARt#sf<#$}=)VJ~ zBlvESR-Jq3=$jydfC?r!$iuAw9oGJ!U-M%YMJ+y&Ch%TCM^JUq5BGkf8O_{y`#mQ} nq4{V^xv44i3V1v$0C_ea3_go|-rj`700000NkvXXu0mjfd&QW1 literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00010.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00010.png new file mode 100644 index 0000000000000000000000000000000000000000..9c7e7049cb3e9bcfb1601ec510ee465d38229d4d GIT binary patch literal 340 zcmV-a0jvIrP)b=%MgRq*37h4eRxPbkJCLY|1VIo49_}P}TKpH4$L<8?X{t;p zy+UQc_4@p%0?~E_&igM#?#L~IOHR(-<@sYotiy&C*Y&GII0yeh-p3zW9cv$Q0k>6Y_)5~SfP=m zSMUtz)%Ex|-o}7!H9hbQ(8{%C?kQVa?C`*Uj-J(h>P7(Y#?ZWvi?6}@n{fGLp>YTp myqR(V_$?>^<%seR_VWh!ktdMRJ7$jtQV?E0Jw2 zEX<>cv$fC(3cFo%Dj&aPIUE4ZO{b?Km?d}fT6HH2&km6xtYY2t?0?Vdg78W$~UYw9Gu>A)BWT- z@}KCar_^|OW15hRAhQF2ZeR=QhlZ+`4$E`Eas@aBM%J9qx?7F%p>;S934?&BM2?}J zlQ_IdDoMLegCO&ry@(xg1W(hjcYU6aul_=@0gb}dlu*bos`+p@Zo;(uu+MEe@Hx8( z6#{B>R8eT>+?LGx?SfhmU2dMPhSs@oxE!RZhALO+^5VzU*c}iW!q%g(soRz@=ckE) x>DmKY`<^6J1d`P&@3BZxxL8|~DvF{=%pcj5^`~Z2UDE&n002ovPDHLkV1l(T)B*qi literal 0 HcmV?d00001 diff --git a/tests/test_app_mainmenu.py b/tests/test_app_mainmenu.py new file mode 100644 index 0000000..de7f3ce --- /dev/null +++ b/tests/test_app_mainmenu.py @@ -0,0 +1,21 @@ +from ragger.navigator import NavInsID + +from utils import ROOT_SCREENSHOT_PATH + + +# In this test we check the behavior of the device main menu +def test_app_mainmenu(firmware, navigator, test_name): + # Navigate in the main menu + if firmware.device.startswith("nano"): + instructions = [ + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK + ] + else: + instructions = [ + NavInsID.USE_CASE_HOME_INFO, + NavInsID.USE_CASE_SETTINGS_SINGLE_PAGE_EXIT + ] + navigator.navigate_and_compare(ROOT_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..dd6446b --- /dev/null +++ b/tests/test_appname_cmd.py @@ -0,0 +1,12 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_app_name_response + + +# In this test we check that the GET_APP_NAME replies the application name +def test_app_name(backend): + # 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 + assert unpack_get_app_name_response(response.data) == "app-boilerplate-rust" diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py new file mode 100644 index 0000000..277f2f8 --- /dev/null +++ b/tests/test_error_cmd.py @@ -0,0 +1,56 @@ +import pytest + +from ragger.error import ExceptionRAPDU +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): + 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): + 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): + 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): +# # APDUs must be at least 5 bytes: CLA, INS, P1, P2, Lc. +# with pytest.raises(ExceptionRAPDU) as e: +# backend.exchange_raw(b"E0030000") +# assert e.value.status == Errors.SW_WRONG_DATA_LENGTH +# # APDUs advertises a too long length +# with pytest.raises(ExceptionRAPDU) as e: +# backend.exchange_raw(b"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): +# 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..5248663 --- /dev/null +++ b/tests/test_name_version.py @@ -0,0 +1,15 @@ +# from application_client.boilerplate_command_sender import BoilerplateCommandSender +# from application_client.boilerplate_response_unpacker import unpack_get_app_and_version_response + + +# # 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, backend_name): +# # 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) + +# assert app_name == "app-boilerplate-rust" +# assert version == "1.0.0" diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py new file mode 100644 index 0000000..f295411 --- /dev/null +++ b/tests/test_pubkey_cmd.py @@ -0,0 +1,87 @@ +import pytest + +from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors +from application_client.boilerplate_response_unpacker import unpack_get_public_key_response +from ragger.bip import calculate_public_key_and_chaincode, CurveChoice +from ragger.error import ExceptionRAPDU +from ragger.navigator import NavInsID, NavIns +from utils import ROOT_SCREENSHOT_PATH + + +# In this test we check that the GET_PUBLIC_KEY works in non-confirmation mode +def test_get_public_key_no_confirm(backend): + for path in ["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"]: + client = BoilerplateCommandSender(backend) + response = client.get_public_key(path=path).data + _, public_key, _, _ = unpack_get_public_key_response(response) + + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert public_key.hex() == ref_public_key + + +# In this test we check that the GET_PUBLIC_KEY works in confirmation mode +def test_get_public_key_confirm_accepted(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + with client.get_public_key_with_confirmation(path=path): + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavIns(NavInsID.TOUCH, (200, 335)), + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_EXIT_QR, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS + ] + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, + test_name, + instructions) + response = client.get_async_response().data + _, public_key, _, _ = unpack_get_public_key_response(response) + + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert 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 +def test_get_public_key_confirm_refused(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + + if firmware.device.startswith("nano"): + with pytest.raises(ExceptionRAPDU) as e: + with client.get_public_key_with_confirmation(path=path): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name) + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 + else: + instructions_set = [ + [ + NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_STATUS_DISMISS + ], + [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CANCEL, + NavInsID.USE_CASE_STATUS_DISMISS + ] + ] + for i, instructions in enumerate(instructions_set): + with pytest.raises(ExceptionRAPDU) as e: + with client.get_public_key_with_confirmation(path=path): + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, + test_name + f"/part{i}", + instructions) + # 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..fb9affb --- /dev/null +++ b/tests/test_sign_cmd.py @@ -0,0 +1,141 @@ +# import pytest + +# 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 ragger.error import ExceptionRAPDU +# from ragger.navigator import NavInsID +# from utils import ROOT_SCREENSHOT_PATH, check_signature_validity + +# # In this tests we check the behavior of the device when asked to sign a transaction + + +# # In this test se 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(firmware, backend, navigator, test_name): +# # 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 +# if firmware.device.startswith("nano"): +# navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, +# [NavInsID.BOTH_CLICK], +# "Approve", +# ROOT_SCREENSHOT_PATH, +# test_name) +# else: +# navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, +# [NavInsID.USE_CASE_REVIEW_CONFIRM, +# NavInsID.USE_CASE_STATUS_DISMISS], +# "Hold to sign", +# ROOT_SCREENSHOT_PATH, +# test_name) + +# # 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(firmware, backend, navigator, test_name): +# # 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): +# if firmware.device.startswith("nano"): +# navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, +# [NavInsID.BOTH_CLICK], +# "Approve", +# ROOT_SCREENSHOT_PATH, +# test_name) +# else: +# navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, +# [NavInsID.USE_CASE_REVIEW_CONFIRM, +# NavInsID.USE_CASE_STATUS_DISMISS], +# "Hold to sign", +# ROOT_SCREENSHOT_PATH, +# test_name) +# 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(firmware, backend, navigator, test_name): +# # 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) +# _, pub_key, _, _ = unpack_get_public_key_response(rapdu.data) + +# transaction = Transaction( +# nonce=1, +# to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", +# value=666, +# memo="This transaction will be refused by the user" +# ).serialize() + +# if firmware.device.startswith("nano"): +# with pytest.raises(ExceptionRAPDU) as e: +# with client.sign_tx(path=path, transaction=transaction): +# navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, +# [NavInsID.BOTH_CLICK], +# "Reject", +# ROOT_SCREENSHOT_PATH, +# test_name) + +# # Assert that we have received a refusal +# assert e.value.status == Errors.SW_DENY +# assert len(e.value.data) == 0 +# else: +# for i in range(3): +# instructions = [NavInsID.USE_CASE_REVIEW_TAP] * i +# instructions += [NavInsID.USE_CASE_REVIEW_REJECT, +# NavInsID.USE_CASE_CHOICE_CONFIRM, +# NavInsID.USE_CASE_STATUS_DISMISS] +# with pytest.raises(ExceptionRAPDU) as e: +# with client.sign_tx(path=path, transaction=transaction): +# navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, +# test_name + f"/part{i}", +# instructions) +# # 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..cc9e4a0 --- /dev/null +++ b/tests/test_version_cmd.py @@ -0,0 +1,16 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_version_response + +# Taken from the Cargo.toml, to update every time the version is bumped +MAJOR = 1 +MINOR = 0 +PATCH = 0 + +# In this test we check the behavior of the device when asked to provide the app version +def test_version(backend): + # 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 + assert unpack_get_version_response(rapdu.data) == (MAJOR, MINOR, PATCH) diff --git a/tests/usage.md b/tests/usage.md new file mode 100644 index 0000000..be8890f --- /dev/null +++ b/tests/usage.md @@ -0,0 +1,74 @@ +# 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 + +``` +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`: +``` +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 [NANOS, NANOX, NANOSP, STAX] +exit +``` + +### Run a simple test using the Speculos emulator + +You can use the following command to get your first experience with Ragger and Speculos +``` +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`: +``` +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 [NANOS, NANOX, NANOSP, STAX] +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. +``` +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 +``` + -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 +``` + --device run the test on the specified device [nanos,nanox,nanosp,stax,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..cb52233 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,23 @@ +from pathlib import Path +from hashlib import sha256 +from sha3 import keccak_256 + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + + +ROOT_SCREENSHOT_PATH = Path(__file__).parent.resolve() + + +# 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=sha256 + ) + return pk.verify(signature=signature, + data=message, + hashfunc=keccak_256, + sigdecode=sigdecode_der)