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
This commit is contained in:
Alexandre Paillier
2023-02-27 10:54:36 +01:00
parent 2b463e95aa
commit 37a6dab7be
6 changed files with 133 additions and 5 deletions
+2
View File
@@ -1,2 +1,4 @@
venv/
__pycache__/
snapshots-tmp/
elfs/
+55 -1
View File
@@ -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
+56 -1
View File
@@ -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:]
+4
View File
@@ -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")
+1
View File
@@ -5,6 +5,7 @@ class SettingType(IntEnum):
BLIND_SIGNING = 0,
DEBUG_DATA = auto()
NONCE = auto()
VERBOSE_ENS = auto()
VERBOSE_EIP712 = auto()
class SettingImpl:
+15 -3
View File
@@ -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)