From 37a6dab7be59db443087f6b87b590456e9dfd11b Mon Sep 17 00:00:00 2001 From: Alexandre Paillier Date: Wed, 4 Jan 2023 15:15:44 +0100 Subject: [PATCH] Updated Ragger app client for trusted name & sign APDUs Also added two new optional arguments to pytest : --gold for screenshot generation --chainid to specify an alternative app chain ID --- tests/ragger/.gitignore | 2 + tests/ragger/app/client.py | 56 +++++++++++++++++++++++++++- tests/ragger/app/command_builder.py | 57 ++++++++++++++++++++++++++++- tests/ragger/app/response_parser.py | 4 ++ tests/ragger/app/setting.py | 1 + tests/ragger/conftest.py | 18 +++++++-- 6 files changed, 133 insertions(+), 5 deletions(-) diff --git a/tests/ragger/.gitignore b/tests/ragger/.gitignore index 93526df6..a3fe09d4 100644 --- a/tests/ragger/.gitignore +++ b/tests/ragger/.gitignore @@ -1,2 +1,4 @@ venv/ __pycache__/ +snapshots-tmp/ +elfs/ diff --git a/tests/ragger/app/client.py b/tests/ragger/app/client.py index eb05ff80..804d948d 100644 --- a/tests/ragger/app/client.py +++ b/tests/ragger/app/client.py @@ -2,14 +2,18 @@ from enum import IntEnum, auto from typing import Optional from ragger.backend import BackendInterface from ragger.utils import RAPDU +from ragger.navigator import NavInsID, NavIns, NanoNavigator from app.command_builder import EthereumCmdBuilder from app.setting import SettingType, SettingImpl from app.eip712 import EIP712FieldType from app.response_parser import EthereumRespParser import signal import time +from pathlib import Path +ROOT_SCREENSHOT_PATH = Path(__file__).parent.parent + class EthereumClient: _settings: dict[SettingType, SettingImpl] = { SettingType.BLIND_SIGNING: SettingImpl( @@ -21,6 +25,9 @@ class EthereumClient: SettingType.NONCE: SettingImpl( [ "nanos", "nanox", "nanosp" ] ), + SettingType.VERBOSE_ENS: SettingImpl( + [ "nanox", "nanosp" ] + ), SettingType.VERBOSE_EIP712: SettingImpl( [ "nanox", "nanosp" ] ) @@ -28,10 +35,12 @@ class EthereumClient: _click_delay = 1/4 _eip712_filtering = False - def __init__(self, client: BackendInterface): + def __init__(self, client: BackendInterface, chain_id: int, golden_run: bool): self._client = client + self._chain_id = chain_id self._cmd_builder = EthereumCmdBuilder() self._resp_parser = EthereumRespParser() + self._nav = NanoNavigator(client, client.firmware, golden_run) signal.signal(signal.SIGALRM, self._click_signal_timeout) for setting in self._settings.values(): setting.value = False @@ -156,3 +165,48 @@ class EthereumClient: with self._send(self._cmd_builder.eip712_filtering_show_field(name, sig)): pass assert self._recv().status == 0x9000 + + def send_fund(self, + bip32_path: list[Optional[int]], + nonce: int, + gas_price: int, + gas_limit: int, + to: bytes, + amount: float, + chain_id: int, + screenshot_collection: str = None): + for chunk in self._cmd_builder.send_fund(bip32_path, + nonce, + gas_price, + gas_limit, + to, + amount, + chain_id): + with self._send(chunk): + nav_ins = NavIns(NavInsID.RIGHT_CLICK) + final_ins = [ NavIns(NavInsID.BOTH_CLICK) ] + target_text = "and send" + if screenshot_collection: + self._nav.navigate_until_text_and_compare(nav_ins, + final_ins, + target_text, + ROOT_SCREENSHOT_PATH, + screenshot_collection) + else: + self._nav.navigate_until_text(nav_ins, + final_ins, + target_text) + assert self._recv().status == 0x9000 + + def get_challenge(self) -> int: + with self._send(self._cmd_builder.get_challenge()): + pass + resp = self._recv() + assert resp.status == 0x9000 + return self._resp_parser.challenge(resp.data) + + def provide_trusted_name(self, name: str, key_id: int, algo_id: int, sig: bytes): + for chunk in self._cmd_builder.provide_trusted_name(name, key_id, algo_id, sig): + with self._send(chunk): + pass + assert self._recv().status == 0x9000 diff --git a/tests/ragger/app/command_builder.py b/tests/ragger/app/command_builder.py index 49bd60df..e84f72ba 100644 --- a/tests/ragger/app/command_builder.py +++ b/tests/ragger/app/command_builder.py @@ -5,14 +5,19 @@ import struct import rlp class InsType(IntEnum): + SIGN = 0x04 EIP712_SEND_STRUCT_DEF = 0x1a EIP712_SEND_STRUCT_IMPL = 0x1c EIP712_SEND_FILTERING = 0x1e EIP712_SIGN = 0x0c + GET_CHALLENGE = 0x20 + PROVIDE_TRUSTED_NAME = 0x22 class P1Type(IntEnum): COMPLETE_SEND = 0x00 PARTIAL_SEND = 0x01 + SIGN_FIRST_CHUNK = 0x00 + SIGN_SUBSQT_CHUNK = 0x80 class P2Type(IntEnum): STRUCT_NAME = 0x00 @@ -31,7 +36,7 @@ class EthereumCmdBuilder: ins: InsType, p1: int, p2: int, - cdata: bytearray = bytearray()) -> bytes: + cdata: bytearray = bytes()) -> bytes: header = bytearray() header.append(self._CLA) @@ -171,3 +176,53 @@ class EthereumCmdBuilder: P1Type.COMPLETE_SEND, P2Type.FILTERING_FIELD_NAME, self._eip712_filtering_send_name(name, sig)) + + def sign(self, bip32_path: list[Optional[int]], data: list) -> Iterator[bytes]: + payload = self._format_bip32(bip32_path, bytearray()) + payload += rlp.encode(data) + p1 = P1Type.SIGN_FIRST_CHUNK + while len(payload) > 0: + yield self._serialize(InsType.SIGN, + p1, + 0x00, + payload[:0xff]) + payload = payload[0xff:] + p1 = P1Type.SIGN_SUBSQT_CHUNK + + def send_fund(self, + bip32_path: list[Optional[int]], + nonce: int, + gas_price: int, + gas_limit: int, + to: bytes, + amount: float, + chain_id: int) -> Iterator[bytes]: + data = list() + data.append(nonce) + data.append(gas_price) + data.append(gas_limit) + data.append(to) + data.append(int(amount * 1000000000000000000)) + data.append(bytes()) + data.append(chain_id) + data.append(bytes()) + data.append(bytes()) + return self.sign(bip32_path, data) + + def get_challenge(self) -> bytes: + return self._serialize(InsType.GET_CHALLENGE, 0x00, 0x00) + + def provide_trusted_name(self, name: str, key_id: int, algo_id: int, sig: bytes) -> bytes: + payload = bytearray() + payload.append(len(name)) + payload += name.encode() + payload.append(key_id) + payload.append(algo_id) + payload.append(len(sig)) + payload += sig + while len(payload) > 0: + yield self._serialize(InsType.PROVIDE_TRUSTED_NAME, + 0x00, + 0x00, + payload[:0xff]) + payload = payload[0xff:] diff --git a/tests/ragger/app/response_parser.py b/tests/ragger/app/response_parser.py index 681c18d7..5651797d 100644 --- a/tests/ragger/app/response_parser.py +++ b/tests/ragger/app/response_parser.py @@ -12,3 +12,7 @@ class EthereumRespParser: data = data[32:] return v, r, s + + def challenge(self, data: bytes) -> int: + assert len(data) == 4 + return int.from_bytes(data, "big") diff --git a/tests/ragger/app/setting.py b/tests/ragger/app/setting.py index a965fe3f..f9412860 100644 --- a/tests/ragger/app/setting.py +++ b/tests/ragger/app/setting.py @@ -5,6 +5,7 @@ class SettingType(IntEnum): BLIND_SIGNING = 0, DEBUG_DATA = auto() NONCE = auto() + VERBOSE_ENS = auto() VERBOSE_EIP712 = auto() class SettingImpl: diff --git a/tests/ragger/conftest.py b/tests/ragger/conftest.py index c1d58e9e..55fe750a 100644 --- a/tests/ragger/conftest.py +++ b/tests/ragger/conftest.py @@ -15,8 +15,9 @@ def pytest_addoption(parser): parser.addoption("--path", action="store", default="./elfs") parser.addoption("--model", action="store", required=True) parser.addoption("--display", action="store_true", default=False) + parser.addoption("--chainid", type=int, action="store", default=1) + parser.addoption("--gold", action="store_true", default=False) -# accessing the value of the "--backend" option as a fixture @pytest.fixture def arg_backend(pytestconfig) -> str: return pytestconfig.getoption("backend") @@ -33,6 +34,14 @@ def arg_model(pytestconfig) -> str: def arg_display(pytestconfig) -> bool: return pytestconfig.getoption("display") +@pytest.fixture +def arg_chainid(pytestconfig) -> int: + return pytestconfig.getoption("chainid") + +@pytest.fixture +def arg_gold(pytestconfig) -> bool: + return pytestconfig.getoption("gold") + # Providing the firmware as a fixture @pytest.fixture def firmware(arg_model: str) -> Firmware: @@ -41,6 +50,7 @@ def firmware(arg_model: str) -> Firmware: return fw raise ValueError("Unknown device model \"%s\"" % (arg_model)) + def get_speculos_args(arg_path: str, display: bool, fw: Firmware): extra_args = list() @@ -74,5 +84,7 @@ def backend_client(arg_backend: str, arg_path: str, arg_display: bool, firmware: # This final fixture will return the properly configured app client, to be used in tests @pytest.fixture -def app_client(backend_client: BackendInterface) -> EthereumClient: - return EthereumClient(backend_client) +def app_client(backend_client: BackendInterface, + arg_chainid: int, + arg_gold: bool) -> EthereumClient: + return EthereumClient(backend_client, arg_chainid, arg_gold)