diff --git a/tests/ragger/.gitignore b/tests/ragger/.gitignore index 93526df..a3fe09d 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 6904c87..dad04f6 100644 --- a/tests/ragger/app/client.py +++ b/tests/ragger/app/client.py @@ -2,12 +2,33 @@ 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 .command_builder import EthereumCmdBuilder from .setting import SettingType, SettingImpl from .eip712 import EIP712FieldType from .response_parser import EthereumRespParser +from .tlv import format_tlv import signal import time +from pathlib import Path +import keychain +import rlp + + +ROOT_SCREENSHOT_PATH = Path(__file__).parent.parent +WEI_IN_ETH = 1e+18 + + +class DOMAIN_NAME_TAG(IntEnum): + STRUCTURE_TYPE = 0x01 + STRUCTURE_VERSION = 0x02 + CHALLENGE = 0x12 + SIGNER_KEY_ID = 0x13 + SIGNER_ALGO = 0x14 + SIGNATURE = 0x15 + DOMAIN_NAME = 0x20 + COIN_TYPE = 0x21 + ADDRESS = 0x22 class EthereumClient: @@ -23,15 +44,20 @@ class EthereumClient: ), SettingType.VERBOSE_EIP712: SettingImpl( [ "nanox", "nanosp" ] + ), + SettingType.VERBOSE_ENS: SettingImpl( + [ "nanox", "nanosp" ] ) } _click_delay = 1/4 _eip712_filtering = False - def __init__(self, client: BackendInterface): + def __init__(self, client: BackendInterface, golden_run: bool): self._client = client + self._chain_id = 1 self._cmd_builder = EthereumCmdBuilder() self._resp_parser = EthereumRespParser() + self._nav = NanoNavigator(client, client.firmware, golden_run) signal.signal(signal.SIGALRM, self._click_signal_timeout) for setting in self._settings.values(): setting.value = False @@ -156,3 +182,61 @@ class EthereumClient: with self._send(self._cmd_builder.eip712_filtering_show_field(name, sig)): pass assert self._recv().status == 0x9000 + + def send_fund(self, + bip32_path: str, + nonce: int, + gas_price: int, + gas_limit: int, + to: bytes, + amount: float, + chain_id: int, + screenshot_collection: str = None): + data = list() + data.append(nonce) + data.append(gas_price) + data.append(gas_limit) + data.append(to) + data.append(int(amount * WEI_IN_ETH)) + data.append(bytes()) + data.append(chain_id) + data.append(bytes()) + data.append(bytes()) + + for chunk in self._cmd_builder.sign(bip32_path, rlp.encode(data)): + with self._send(chunk): + nav_ins = NavIns(NavInsID.RIGHT_CLICK) + final_ins = [ NavIns(NavInsID.BOTH_CLICK) ] + target_text = "and send" + if screenshot_collection: + self._nav.navigate_until_text_and_compare(nav_ins, + final_ins, + target_text, + ROOT_SCREENSHOT_PATH, + screenshot_collection) + else: + self._nav.navigate_until_text(nav_ins, + final_ins, + target_text) + + def get_challenge(self) -> int: + with self._send(self._cmd_builder.get_challenge()): + pass + resp = self._recv() + return self._resp_parser.challenge(resp.data) + + def provide_domain_name(self, challenge: int, name: str, addr: bytes): + payload = format_tlv(DOMAIN_NAME_TAG.STRUCTURE_TYPE, 3) # TrustedDomainName + payload += format_tlv(DOMAIN_NAME_TAG.STRUCTURE_VERSION, 1) + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_KEY_ID, 0) # test key + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_ALGO, 1) # secp256k1 + payload += format_tlv(DOMAIN_NAME_TAG.CHALLENGE, challenge) + payload += format_tlv(DOMAIN_NAME_TAG.COIN_TYPE, 0x3c) # ETH in slip-44 + payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name) + payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr) + payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE, + keychain.sign_data(keychain.Key.DOMAIN_NAME, payload)) + + for chunk in self._cmd_builder.provide_domain_name(payload): + with self._send(chunk): + pass diff --git a/tests/ragger/app/command_builder.py b/tests/ragger/app/command_builder.py index 9dea245..aac10d0 100644 --- a/tests/ragger/app/command_builder.py +++ b/tests/ragger/app/command_builder.py @@ -5,14 +5,19 @@ from ragger.bip import pack_derivation_path import struct class InsType(IntEnum): + SIGN = 0x04 EIP712_SEND_STRUCT_DEF = 0x1a EIP712_SEND_STRUCT_IMPL = 0x1c EIP712_SEND_FILTERING = 0x1e EIP712_SIGN = 0x0c + GET_CHALLENGE = 0x20 + PROVIDE_DOMAIN_NAME = 0x22 class P1Type(IntEnum): 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) @@ -161,3 +166,30 @@ class EthereumCmdBuilder: P1Type.COMPLETE_SEND, P2Type.FILTERING_FIELD_NAME, self._eip712_filtering_send_name(name, sig)) + + def sign(self, bip32_path: str, rlp_data: bytes) -> Iterator[bytes]: + payload = pack_derivation_path(bip32_path) + payload += rlp_data + p1 = P1Type.SIGN_FIRST_CHUNK + while len(payload) > 0: + yield self._serialize(InsType.SIGN, + p1, + 0x00, + payload[:0xff]) + payload = payload[0xff:] + p1 = P1Type.SIGN_SUBSQT_CHUNK + + def get_challenge(self) -> bytes: + return self._serialize(InsType.GET_CHALLENGE, 0x00, 0x00) + + def provide_domain_name(self, tlv_payload: bytes) -> bytes: + payload = struct.pack(">H", len(tlv_payload)) + payload += tlv_payload + p1 = 1 + while len(payload) > 0: + yield self._serialize(InsType.PROVIDE_DOMAIN_NAME, + p1, + 0x00, + payload[:0xff]) + payload = payload[0xff:] + p1 = 0 diff --git a/tests/ragger/app/response_parser.py b/tests/ragger/app/response_parser.py index 9d1c9bd..242f4cf 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 a51f988..7e79da7 100644 --- a/tests/ragger/app/setting.py +++ b/tests/ragger/app/setting.py @@ -6,6 +6,7 @@ class SettingType(IntEnum): DEBUG_DATA = auto() NONCE = auto() VERBOSE_EIP712 = auto() + VERBOSE_ENS = auto() class SettingImpl: devices: List[str] diff --git a/tests/ragger/app/tlv.py b/tests/ragger/app/tlv.py new file mode 100644 index 0000000..2ff4cef --- /dev/null +++ b/tests/ragger/app/tlv.py @@ -0,0 +1,25 @@ +from typing import Any + +def der_encode(value: int) -> bytes: + # max() to have minimum length of 1 + value_bytes = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big') + if value >= 0x80: + value_bytes = (0x80 | len(value_bytes)).to_bytes(1, 'big') + value_bytes + return value_bytes + +def format_tlv(tag: int, value: Any) -> bytes: + if isinstance(value, int): + # max() to have minimum length of 1 + value = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big') + elif isinstance(value, str): + value = value.encode() + + if not isinstance(value, bytes): + print("Unhandled TLV formatting for type : %s" % (type(value))) + return None + + tlv = bytearray() + tlv += der_encode(tag) + tlv += der_encode(len(value)) + tlv += value + return tlv diff --git a/tests/ragger/conftest.py b/tests/ragger/conftest.py index 35e3fd3..68799b2 100644 --- a/tests/ragger/conftest.py +++ b/tests/ragger/conftest.py @@ -5,8 +5,8 @@ from app.client import EthereumClient # This final fixture will return the properly configured app client, to be used in tests @pytest.fixture -def app_client(backend: BackendInterface) -> EthereumClient: - return EthereumClient(backend) +def app_client(backend: BackendInterface, golden_run: bool) -> EthereumClient: + return EthereumClient(backend, golden_run) # Pull all features from the base ragger conftest using the overridden configuration pytest_plugins = ("ragger.conftest.base_conftest", ) diff --git a/tests/ragger/requirements.txt b/tests/ragger/requirements.txt index b2c4a7b..e408ead 100644 --- a/tests/ragger/requirements.txt +++ b/tests/ragger/requirements.txt @@ -1,3 +1,4 @@ ragger[speculos]>=1.6.0,<1.7.0 pytest ecdsa +simple-rlp