From ca04b5e21399d1d5dfae466c656960d4b46345e5 Mon Sep 17 00:00:00 2001 From: Alexandre Paillier Date: Tue, 2 Aug 2022 16:31:11 +0200 Subject: [PATCH] Now properly handles UI clicks in EIP712 automated tests --- tests/ragger/conftest.py | 4 +- tests/ragger/ethereum_client.py | 101 +++++++++++++++++++++++--------- tests/ragger/test_eip712.py | 36 ++++++++---- 3 files changed, 100 insertions(+), 41 deletions(-) diff --git a/tests/ragger/conftest.py b/tests/ragger/conftest.py index 5f9efae..09c6227 100644 --- a/tests/ragger/conftest.py +++ b/tests/ragger/conftest.py @@ -6,8 +6,8 @@ from ethereum_client import EthereumClient ELFS_DIR = (Path(__file__).parent.parent / "elfs").resolve() FWS = [ - Firmware("nanox", "2.0.2"), Firmware("nanos", "2.1"), + Firmware("nanox", "2.0.2"), Firmware("nanosp", "1.0.3") ] @@ -54,4 +54,4 @@ def backend_client(backend_name: str, firmware: Firmware) -> BackendInterface: # This final fixture will return the properly configured app client, to be used in tests @pytest.fixture def app_client(backend_client: BackendInterface) -> EthereumClient: - yield EthereumClient(backend_client) + return EthereumClient(backend_client) diff --git a/tests/ragger/ethereum_client.py b/tests/ragger/ethereum_client.py index 02a705e..fc3c4c5 100644 --- a/tests/ragger/ethereum_client.py +++ b/tests/ragger/ethereum_client.py @@ -1,7 +1,10 @@ +from contextlib import contextmanager +from typing import Generator from enum import IntEnum, auto from typing import Iterator from ragger.backend import BackendInterface from ragger.utils import RAPDU +import signal class InsType(IntEnum): EIP712_SEND_STRUCT_DEF = 0x1a, @@ -164,23 +167,38 @@ class EthereumResponseParser: return v, r, s - class EthereumClient: + _verbose_eip712 = False + def __init__(self, client: BackendInterface, debug: bool = False): self._client = client self._debug = debug self._cmd_builder = EthereumClientCmdBuilder() self._resp_parser = EthereumResponseParser() + self._click_delay = 1/4 + signal.signal(signal.SIGALRM, self._click_signal_timeout) - def _send(self, payload: bytearray) -> None: - self._client.send_raw(payload) + def _send(self, payload: bytearray): + return self._client.exchange_async_raw(payload) def _recv(self) -> RAPDU: - return self._client.receive() + return self._client._last_async_response + + def _click_signal_timeout(self, signum: int, frame): + self._client.right_click() + + def _enable_click_until_response(self): + signal.setitimer(signal.ITIMER_REAL, + self._click_delay, + self._click_delay) + + def _disable_click_until_response(self): + signal.setitimer(signal.ITIMER_REAL, 0, 0) def eip712_send_struct_def_struct_name(self, name: str): - self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name)) - return self._recv() + with self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name)): + pass + return self._recv().status == 0x9000 def eip712_send_struct_def_struct_field(self, field_type: EIP712FieldType, @@ -188,52 +206,77 @@ class EthereumClient: type_size: int, array_levels: [], key_name: str): - self._send(self._cmd_builder.eip712_send_struct_def_struct_field( + with self._send(self._cmd_builder.eip712_send_struct_def_struct_field( field_type, type_name, type_size, array_levels, - key_name)) + key_name)): + pass return self._recv() def eip712_send_struct_impl_root_struct(self, name: str): - self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name)) + with self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name)): + self._enable_click_until_response() + self._disable_click_until_response() return self._recv() def eip712_send_struct_impl_array(self, size: int): - self._send(self._cmd_builder.eip712_send_struct_impl_array(size)) + with self._send(self._cmd_builder.eip712_send_struct_impl_array(size)): + pass return self._recv() def eip712_send_struct_impl_struct_field(self, raw_value: bytes): - ret = None for apdu in self._cmd_builder.eip712_send_struct_impl_struct_field(raw_value): - self._send(apdu) - # TODO: Do clicks - ret = self._recv() - return ret + with self._send(apdu): + self._enable_click_until_response() + self._disable_click_until_response() + assert self._recv().status == 0x9000 def eip712_sign_new(self, bip32): - self._send(self._cmd_builder.eip712_sign_new(bip32)) - return self._recv() + with self._send(self._cmd_builder.eip712_sign_new(bip32)): + if not self._verbose_eip712: # need to skip the message hash + self._client.right_click() + self._client.right_click() + self._client.both_click() # approve signature + resp = self._recv() + assert resp.status == 0x9000 + return self._resp_parser.sign(resp.data) def eip712_sign_legacy(self, bip32, domain_hash: bytes, message_hash: bytes): - self._send(self._cmd_builder.eip712_sign_legacy(bip32, - domain_hash, - message_hash)) - self._client.right_click() # sign typed message screen - for _ in range(2): # two hashes (domain + message) - if self._client.firmware.device == "nanos": - screens_per_hash = 4 - else: - screens_per_hash = 2 - for _ in range(screens_per_hash): - self._client.right_click() - self._client.both_click() # approve signature + with self._send(self._cmd_builder.eip712_sign_legacy(bip32, + domain_hash, + message_hash)): + self._client.right_click() # sign typed message screen + for _ in range(2): # two hashes (domain + message) + if self._client.firmware.device == "nanos": + screens_per_hash = 4 + else: + screens_per_hash = 2 + for _ in range(screens_per_hash): + self._client.right_click() + self._client.both_click() # approve signature resp = self._recv() assert resp.status == 0x9000 return self._resp_parser.sign(resp.data) + + def setting_toggle_verbose_eip712(self): + # Go to settings + self._client.right_click() + self._client.right_click() + self._client.both_click() + # Go to verbose eip712 + self._client.right_click() + self._client.right_click() + self._client.right_click() + self._client.both_click() + # Go back + self._client.right_click() + self._client.both_click() + + self._verbose_eip712 = not self._verbose_eip712 diff --git a/tests/ragger/test_eip712.py b/tests/ragger/test_eip712.py index 9367dea..597989c 100644 --- a/tests/ragger/test_eip712.py +++ b/tests/ragger/test_eip712.py @@ -1,5 +1,7 @@ +import pytest import os import fnmatch +from typing import List from ethereum_client import EthereumClient from eip712 import InputData @@ -12,6 +14,22 @@ bip32 = [ ] +def input_files() -> List[str]: + files = [] + for file in os.scandir("./eip712/input_files"): + if fnmatch.fnmatch(file, "*-test.json"): + files.append(file.path) + return sorted(files) + +@pytest.fixture(params=input_files()) +def input_file(request) -> str: + return request.param + +@pytest.fixture(params=[True, False]) +def verbose(request) -> bool: + return request.param + + def test_eip712_legacy(app_client: EthereumClient): v, r, s = app_client.eip712_sign_legacy( bip32, @@ -24,14 +42,12 @@ def test_eip712_legacy(app_client: EthereumClient): assert s == bytes.fromhex("52d8ba9153de9255da220ffd36762c0b027701a3b5110f0a765f94b16a9dfb55") -def test_eip712_new(app_client: EthereumClient): - if app_client._client.firmware.device == "nanos": # not supported - return +def test_eip712_new(app_client: EthereumClient, input_file, verbose): + if app_client._client.firmware.device != "nanos": # not supported + print("=====> %s" % (input_file)) - # Loop through JSON files - for file in os.scandir("./eip712/input_files"): - if fnmatch.fnmatch(file, "*-test.json"): - print(file.path) - InputData.process_file(app_client, file.path, False) - app_client.eip712_sign_new(bip32) - assert 1 == 1 + if verbose: + app_client.setting_toggle_verbose_eip712() + InputData.process_file(app_client, input_file, False) + v, r, s = app_client.eip712_sign_new(bip32) + assert 1 == 1 # TODO: Replace by the actual v,r,s asserts