Updated Ragger app client for domain names & sign APDUs
This commit is contained in:
2
tests/ragger/.gitignore
vendored
2
tests/ragger/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
snapshots-tmp/
|
||||
elfs/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -6,6 +6,7 @@ class SettingType(IntEnum):
|
||||
DEBUG_DATA = auto()
|
||||
NONCE = auto()
|
||||
VERBOSE_EIP712 = auto()
|
||||
VERBOSE_ENS = auto()
|
||||
|
||||
class SettingImpl:
|
||||
devices: List[str]
|
||||
|
||||
25
tests/ragger/app/tlv.py
Normal file
25
tests/ragger/app/tlv.py
Normal file
@@ -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
|
||||
@@ -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", )
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ragger[speculos]>=1.6.0,<1.7.0
|
||||
pytest
|
||||
ecdsa
|
||||
simple-rlp
|
||||
|
||||
Reference in New Issue
Block a user