Stax Ragger tests

This commit is contained in:
Alexandre Paillier
2023-05-09 15:16:03 +02:00
parent 683e410fee
commit fec9a80d27
9 changed files with 376 additions and 277 deletions

View File

@@ -2,14 +2,9 @@ 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 .command_builder import CommandBuilder
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
@@ -41,58 +36,19 @@ class DOMAIN_NAME_TAG(IntEnum):
ADDRESS = 0x22
class EthereumClient:
_settings: dict[SettingType, SettingImpl] = {
SettingType.BLIND_SIGNING: SettingImpl(
[ "nanos", "nanox", "nanosp" ]
),
SettingType.DEBUG_DATA: SettingImpl(
[ "nanos", "nanox", "nanosp" ]
),
SettingType.NONCE: SettingImpl(
[ "nanos", "nanox", "nanosp" ]
),
SettingType.VERBOSE_EIP712: SettingImpl(
[ "nanox", "nanosp" ]
),
SettingType.VERBOSE_ENS: SettingImpl(
[ "nanox", "nanosp" ]
)
}
_click_delay = 1/4
_eip712_filtering = False
def __init__(self, client: BackendInterface, golden_run: bool):
class EthAppClient:
def __init__(self, client: BackendInterface):
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
self._cmd_builder = CommandBuilder()
def _send(self, payload: bytearray):
return self._client.exchange_async_raw(payload)
def _recv(self) -> RAPDU:
def response(self) -> RAPDU:
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):
with self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name)):
pass
return self._recv().status == 0x9000
return self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name))
def eip712_send_struct_def_struct_field(self,
field_type: EIP712FieldType,
@@ -100,98 +56,45 @@ class EthereumClient:
type_size: int,
array_levels: [],
key_name: str):
with self._send(self._cmd_builder.eip712_send_struct_def_struct_field(
field_type,
type_name,
type_size,
array_levels,
key_name)):
pass
return self._recv()
return self._send(self._cmd_builder.eip712_send_struct_def_struct_field(
field_type,
type_name,
type_size,
array_levels,
key_name))
def eip712_send_struct_impl_root_struct(self, name: str):
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()
return self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name))
def eip712_send_struct_impl_array(self, size: int):
with self._send(self._cmd_builder.eip712_send_struct_impl_array(size)):
pass
return self._recv()
return self._send(self._cmd_builder.eip712_send_struct_impl_array(size))
def eip712_send_struct_impl_struct_field(self, raw_value: bytes):
for apdu in self._cmd_builder.eip712_send_struct_impl_struct_field(raw_value):
with self._send(apdu):
self._enable_click_until_response()
self._disable_click_until_response()
assert self._recv().status == 0x9000
chunks = self._cmd_builder.eip712_send_struct_impl_struct_field(raw_value)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])
def eip712_sign_new(self, bip32_path: str):
with self._send(self._cmd_builder.eip712_sign_new(bip32_path)):
time.sleep(0.5) # tight on timing, needed by the CI otherwise might fail sometimes
if not self._settings[SettingType.VERBOSE_EIP712].value and \
not self._eip712_filtering: # 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_new(self, bip32_path: str, verbose: bool):
return self._send(self._cmd_builder.eip712_sign_new(bip32_path))
def eip712_sign_legacy(self,
bip32_path: str,
domain_hash: bytes,
message_hash: bytes):
with self._send(self._cmd_builder.eip712_sign_legacy(bip32_path,
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 settings_set(self, new_values: dict[SettingType, bool]):
# Go to settings
for _ in range(2):
self._client.right_click()
self._client.both_click()
for enum in self._settings.keys():
if self._client.firmware.device in self._settings[enum].devices:
if enum in new_values.keys():
if new_values[enum] != self._settings[enum].value:
self._client.both_click()
self._settings[enum].value = new_values[enum]
self._client.right_click()
self._client.both_click()
return self._send(self._cmd_builder.eip712_sign_legacy(bip32_path,
domain_hash,
message_hash))
def eip712_filtering_activate(self):
with self._send(self._cmd_builder.eip712_filtering_activate()):
pass
self._eip712_filtering = True
assert self._recv().status == 0x9000
return self._send(self._cmd_builder.eip712_filtering_activate())
def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes):
with self._send(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig)):
self._enable_click_until_response()
self._disable_click_until_response()
assert self._recv().status == 0x9000
return self._send(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig))
def eip712_filtering_show_field(self, name: str, sig: bytes):
with self._send(self._cmd_builder.eip712_filtering_show_field(name, sig)):
pass
assert self._recv().status == 0x9000
return self._send(self._cmd_builder.eip712_filtering_show_field(name, sig))
def send_fund(self,
bip32_path: str,
@@ -200,8 +103,7 @@ class EthereumClient:
gas_limit: int,
to: bytes,
amount: float,
chain_id: int,
screenshot_collection: str = None):
chain_id: int):
data = list()
data.append(nonce)
data.append(gas_price)
@@ -213,27 +115,14 @@ class EthereumClient:
data.append(bytes())
data.append(bytes())
for chunk in self._cmd_builder.sign(bip32_path, rlp.encode(data)):
chunks = self._cmd_builder.sign(bip32_path, rlp.encode(data))
for chunk in chunks[:-1]:
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)
pass
return self._send(chunks[-1])
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 get_challenge(self):
return self._send(self._cmd_builder.get_challenge())
def provide_domain_name(self, challenge: int, name: str, addr: bytes):
payload = format_tlv(DOMAIN_NAME_TAG.STRUCTURE_TYPE, 3) # TrustedDomainName
@@ -247,6 +136,8 @@ class EthereumClient:
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):
chunks = self._cmd_builder.provide_domain_name(payload)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])

View File

@@ -29,7 +29,7 @@ class P2Type(IntEnum):
FILTERING_CONTRACT_NAME = 0x0f
FILTERING_FIELD_NAME = 0xff
class EthereumCmdBuilder:
class CommandBuilder:
_CLA: int = 0xE0
def _serialize(self,
@@ -103,6 +103,7 @@ class EthereumCmdBuilder:
data)
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> Iterator[bytes]:
chunks = list()
# Add a 16-bit integer with the data's byte length (network byte order)
data_w_length = bytearray()
data_w_length.append((len(data) & 0xff00) >> 8)
@@ -110,11 +111,12 @@ class EthereumCmdBuilder:
data_w_length += data
while len(data_w_length) > 0:
p1 = P1Type.PARTIAL_SEND if len(data_w_length) > 0xff else P1Type.COMPLETE_SEND
yield self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
p1,
P2Type.STRUCT_FIELD,
data_w_length[:0xff])
chunks.append(self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
p1,
P2Type.STRUCT_FIELD,
data_w_length[:0xff]))
data_w_length = data_w_length[0xff:]
return chunks
def eip712_sign_new(self, bip32_path: str) -> bytes:
data = pack_derivation_path(bip32_path)
@@ -167,29 +169,33 @@ class EthereumCmdBuilder:
P2Type.FILTERING_FIELD_NAME,
self._eip712_filtering_send_name(name, sig))
def sign(self, bip32_path: str, rlp_data: bytes) -> Iterator[bytes]:
def sign(self, bip32_path: str, rlp_data: bytes) -> list[bytes]:
apdus = list()
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])
apdus.append(self._serialize(InsType.SIGN,
p1,
0x00,
payload[:0xff]))
payload = payload[0xff:]
p1 = P1Type.SIGN_SUBSQT_CHUNK
return apdus
def get_challenge(self) -> bytes:
return self._serialize(InsType.GET_CHALLENGE, 0x00, 0x00)
def provide_domain_name(self, tlv_payload: bytes) -> bytes:
def provide_domain_name(self, tlv_payload: bytes) -> list[bytes]:
chunks = list()
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])
chunks.append(self._serialize(InsType.PROVIDE_DOMAIN_NAME,
p1,
0x00,
payload[:0xff]))
payload = payload[0xff:]
p1 = 0
return chunks

View File

@@ -1,18 +1,14 @@
class EthereumRespParser:
def sign(self, data: bytes):
assert len(data) == (1 + 32 + 32)
def signature(data: bytes) -> tuple[bytes, bytes, bytes]:
assert len(data) == (1 + 32 + 32)
v = data[0:1]
data = data[1:]
v = data[0:1]
data = data[1:]
r = data[0:32]
data = data[32:]
s = data[0:32]
r = data[0:32]
data = data[32:]
return v, r, s
s = data[0:32]
data = data[32:]
return v, r, s
def challenge(self, data: bytes) -> int:
assert len(data) == 4
return int.from_bytes(data, "big")
def challenge(data: bytes) -> int:
assert len(data) == 4
return int.from_bytes(data, "big")

View File

@@ -1,16 +0,0 @@
from enum import IntEnum, auto
from typing import List
class SettingType(IntEnum):
BLIND_SIGNING = 0,
DEBUG_DATA = auto()
NONCE = auto()
VERBOSE_EIP712 = auto()
VERBOSE_ENS = auto()
class SettingImpl:
devices: List[str]
value: bool
def __init__(self, devs: List[str]):
self.devices = devs

View File

@@ -0,0 +1,62 @@
from enum import Enum, auto
from typing import List
from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavInsID, NavIns
class SettingID(Enum):
BLIND_SIGNING = auto()
DEBUG_DATA = auto()
NONCE = auto()
VERBOSE_EIP712 = auto()
VERBOSE_ENS = auto()
def get_device_settings(device: str) -> list[SettingID]:
if device == "nanos":
return [
SettingID.BLIND_SIGNING,
SettingID.DEBUG_DATA,
SettingID.NONCE
]
if (device == "nanox") or (device == "nanosp") or (device == "stax"):
return [
SettingID.BLIND_SIGNING,
SettingID.DEBUG_DATA,
SettingID.NONCE,
SettingID.VERBOSE_EIP712,
SettingID.VERBOSE_ENS
]
return []
settings_per_page = 3
def get_setting_position(device: str, setting: NavInsID) -> tuple[int, int]:
screen_height = 672 # px
header_height = 85 # px
footer_height = 124 # px
usable_height = screen_height - (header_height + footer_height)
setting_height = usable_height // settings_per_page
index_in_page = get_device_settings(device).index(setting) % settings_per_page
return 350, header_height + (setting_height * index_in_page) + (setting_height // 2)
def settings_toggle(fw: Firmware, nav: Navigator, to_toggle: list[SettingID]):
moves = list()
settings = get_device_settings(fw.device)
# Assume the app is on the home page
if fw.device.startswith("nano"):
moves += [NavInsID.RIGHT_CLICK] * 2
moves += [NavInsID.BOTH_CLICK]
for setting in settings:
if setting in to_toggle:
moves += [NavInsID.BOTH_CLICK]
moves += [NavInsID.RIGHT_CLICK]
moves += [NavInsID.BOTH_CLICK] # Back
else:
moves += [NavInsID.USE_CASE_HOME_SETTINGS]
for setting in settings:
setting_idx = settings.index(setting)
if (setting_idx > 0) and (setting_idx % settings_per_page) == 0:
moves += [NavInsID.USE_CASE_SETTINGS_NEXT]
if setting in to_toggle:
moves += [NavIns(NavInsID.TOUCH, get_setting_position(fw.device, setting))]
moves += [NavInsID.EXIT_HEADER_TAP]
nav.navigate(moves)