Added a ragger ethereum client with EIP712 support
This commit is contained in:
2
tests/ragger/.gitignore
vendored
Normal file
2
tests/ragger/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
50
tests/ragger/conftest.py
Normal file
50
tests/ragger/conftest.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
from ragger import Firmware
|
||||
from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface
|
||||
from ethereum_client import EthereumClient
|
||||
|
||||
# This variable is needed for Speculos only (physical tests need the application to be already installed)
|
||||
APPLICATION = "../../bin/app.elf"
|
||||
# This variable will be useful in tests to implement different behavior depending on the firmware
|
||||
NANOX_FIRMWARE = Firmware("nanox", "2.0.2")
|
||||
NANOS_FIRMWARE = Firmware("nanos", "2.1")
|
||||
NANOSP_FIRMWARE = Firmware("nanosp", "1.0")
|
||||
|
||||
# adding a pytest CLI option "--backend"
|
||||
def pytest_addoption(parser):
|
||||
print(help(parser.addoption))
|
||||
parser.addoption("--backend", action="store", default="speculos")
|
||||
|
||||
# accessing the value of the "--backend" option as a fixture
|
||||
@pytest.fixture(scope="session")
|
||||
def backend_name(pytestconfig) -> str:
|
||||
return pytestconfig.getoption("backend")
|
||||
|
||||
# Providing the firmware as a fixture
|
||||
@pytest.fixture
|
||||
def firmware() -> Firmware:
|
||||
return NANOX_FIRMWARE
|
||||
|
||||
# Depending on the "--backend" option value, a different backend is
|
||||
# instantiated, and the tests will either run on Speculos or on a physical
|
||||
# device depending on the backend
|
||||
def create_backend(backend: str, firmware: Firmware) -> BackendInterface:
|
||||
if backend.lower() == "ledgercomm":
|
||||
return LedgerCommBackend(firmware, interface="hid")
|
||||
elif backend.lower() == "ledgerwallet":
|
||||
return LedgerWalletBackend(firmware)
|
||||
elif backend.lower() == "speculos":
|
||||
return SpeculosBackend(APPLICATION, firmware)
|
||||
else:
|
||||
raise ValueError(f"Backend '{backend}' is unknown. Valid backends are: {BACKENDS}")
|
||||
|
||||
# This fixture will create and return the backend client
|
||||
@pytest.fixture
|
||||
def backend_client(backend_name: str, firmware: Firmware) -> BackendInterface:
|
||||
with create_backend(backend_name, firmware) as b:
|
||||
yield b
|
||||
|
||||
# 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)
|
||||
232
tests/ragger/ethereum_client.py
Normal file
232
tests/ragger/ethereum_client.py
Normal file
@@ -0,0 +1,232 @@
|
||||
from enum import IntEnum, auto
|
||||
from typing import Iterator
|
||||
from ragger.backend import BackendInterface
|
||||
from ragger.utils import RAPDU
|
||||
|
||||
class InsType(IntEnum):
|
||||
EIP712_SEND_STRUCT_DEF = 0x1a,
|
||||
EIP712_SEND_STRUCT_IMPL = 0x1c,
|
||||
EIP712_SEND_FILTERING = 0x1e,
|
||||
EIP712_SIGN = 0x0c
|
||||
|
||||
class P1Type(IntEnum):
|
||||
COMPLETE_SEND = 0x00,
|
||||
PARTIAL_SEND = 0x01
|
||||
|
||||
class P2Type(IntEnum):
|
||||
STRUCT_NAME = 0x00,
|
||||
STRUCT_FIELD = 0xff,
|
||||
ARRAY = 0x0f,
|
||||
LEGACY_IMPLEM = 0x00
|
||||
NEW_IMPLEM = 0x01,
|
||||
|
||||
class EIP712FieldType:
|
||||
CUSTOM = 0,
|
||||
INT = auto(),
|
||||
UINT = auto(),
|
||||
ADDRESS = auto(),
|
||||
BOOL = auto(),
|
||||
STRING = auto(),
|
||||
FIXED_BYTES = auto(),
|
||||
DYN_BYTES = auto()
|
||||
|
||||
|
||||
class EthereumClientCmdBuilder:
|
||||
_CLA: int = 0xE0
|
||||
|
||||
def _serialize(self,
|
||||
ins: InsType,
|
||||
p1: int,
|
||||
p2: int,
|
||||
cdata: bytearray = bytearray()) -> bytes:
|
||||
|
||||
header = bytearray()
|
||||
header.append(self._CLA)
|
||||
header.append(ins)
|
||||
header.append(p1)
|
||||
header.append(p2)
|
||||
header.append(len(cdata))
|
||||
return header + cdata
|
||||
|
||||
def eip712_send_struct_def_struct_name(self, name: str) -> bytes:
|
||||
data = bytearray()
|
||||
for char in name:
|
||||
data.append(ord(char))
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_DEF,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_NAME,
|
||||
data)
|
||||
|
||||
def eip712_send_struct_def_struct_field(self,
|
||||
field_type: EIP712FieldType,
|
||||
type_name: str,
|
||||
type_size: int,
|
||||
array_levels: [],
|
||||
key_name: str) -> bytes:
|
||||
data = bytearray()
|
||||
typedesc = 0
|
||||
typedesc |= (len(array_levels) > 0) << 7
|
||||
typedesc |= (type_size > 0) << 6
|
||||
typedesc |= field_type
|
||||
data.append(typedesc)
|
||||
if field_type == EIP712FieldType.CUSTOM:
|
||||
data.append(len(type_name))
|
||||
for char in type_name:
|
||||
data.append(ord(char))
|
||||
if type_size > 0:
|
||||
data.append(type_size)
|
||||
if len(array_levels) > 0:
|
||||
data.append(len(array_levels))
|
||||
for level in array_levels:
|
||||
data.append(0 if level == None else 1)
|
||||
if level != None:
|
||||
data.append(level)
|
||||
data.append(len(key_name))
|
||||
for char in key_name:
|
||||
data.append(ord(char))
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_DEF,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_FIELD,
|
||||
data)
|
||||
|
||||
def eip712_send_struct_impl_root_struct(self, name: str) -> bytes:
|
||||
data = bytearray()
|
||||
for char in name:
|
||||
data.append(ord(char))
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_NAME,
|
||||
data)
|
||||
|
||||
def eip712_send_struct_impl_array(self, size: int) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(size)
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.ARRAY,
|
||||
data)
|
||||
|
||||
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> Iterator[bytes]:
|
||||
while len(data > 0):
|
||||
yield self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_FIELD,
|
||||
data[:0xff])
|
||||
data = data[0xff:]
|
||||
|
||||
def _format_bip32(self, bip32, data = bytearray()) -> bytearray:
|
||||
data.append(len(bip32))
|
||||
for idx in bip32:
|
||||
data.append((idx & 0xff000000) >> 24)
|
||||
data.append((idx & 0x00ff0000) >> 16)
|
||||
data.append((idx & 0x0000ff00) >> 8)
|
||||
data.append((idx & 0x000000ff))
|
||||
return data
|
||||
|
||||
def eip712_sign_new(self, bip32) -> bytes:
|
||||
data = self._format_bip32(bip32)
|
||||
return self._serialize(InsType.EIP712_SIGN,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.NEW_IMPLEM,
|
||||
data)
|
||||
|
||||
def eip712_sign_legacy(self,
|
||||
bip32,
|
||||
domain_hash: bytes,
|
||||
message_hash: bytes) -> bytes:
|
||||
data = self._format_bip32(bip32)
|
||||
data += domain_hash
|
||||
data += message_hash
|
||||
return self._serialize(InsType.EIP712_SIGN,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.LEGACY_IMPLEM,
|
||||
data)
|
||||
|
||||
|
||||
class EthereumResponseParser:
|
||||
def sign(self, data: bytes):
|
||||
assert len(data) == (1 + 32 + 32)
|
||||
|
||||
v = data[0:1]
|
||||
data = data[1:]
|
||||
|
||||
r = data[0:32]
|
||||
data = data[32:]
|
||||
|
||||
s = data[0:32]
|
||||
data = data[32:]
|
||||
|
||||
return v, r, s
|
||||
|
||||
|
||||
class EthereumClient:
|
||||
def __init__(self, client: BackendInterface, debug: bool = False):
|
||||
self._client = client
|
||||
self._debug = debug
|
||||
self._cmd_builder = EthereumClientCmdBuilder()
|
||||
self._resp_parser = EthereumResponseParser()
|
||||
|
||||
def _send(self, payload: bytearray) -> None:
|
||||
self._client.send_raw(payload)
|
||||
|
||||
def _recv(self) -> RAPDU:
|
||||
return self._client.receive()
|
||||
|
||||
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()
|
||||
|
||||
def eip712_send_struct_def_struct_field(self,
|
||||
field_type: EIP712FieldType,
|
||||
type_name: str,
|
||||
type_size: int,
|
||||
array_levels: [],
|
||||
key_name: str):
|
||||
self._send(self._cmd_builder.eip712_send_struct_def_struct_field(
|
||||
field_type,
|
||||
type_name,
|
||||
type_size,
|
||||
array_levels,
|
||||
key_name))
|
||||
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))
|
||||
return self._recv()
|
||||
|
||||
def eip712_send_struct_impl_array(self, size: int):
|
||||
send._send(self._cmd_builder.eip712_send_struct_impl_array(size))
|
||||
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(
|
||||
InsType.EIP712_SEND_STRUCT_IMPL,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_FIELD,
|
||||
data[:0xff]):
|
||||
self._send(apdu)
|
||||
ret = self._recv()
|
||||
return ret
|
||||
|
||||
def eip712_sign_new(self, bip32):
|
||||
self._send(self._cmd_builder.eip712_sign_new(bip32))
|
||||
return self._recv()
|
||||
|
||||
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)
|
||||
for _ in range(2): # two 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)
|
||||
3
tests/ragger/requirements.txt
Normal file
3
tests/ragger/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
ragger
|
||||
pytest>=6.1.1,<7.0.0
|
||||
ecdsa>=0.16.1,<0.17.0
|
||||
22
tests/ragger/test_eip712.py
Normal file
22
tests/ragger/test_eip712.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
import fnmatch
|
||||
from ethereum_client import EthereumClient
|
||||
|
||||
def test_eip712_legacy(app_client: EthereumClient):
|
||||
bip32 = [
|
||||
0x8000002c,
|
||||
0x8000003c,
|
||||
0x80000000,
|
||||
0,
|
||||
0
|
||||
]
|
||||
|
||||
v, r, s = app_client.eip712_sign_legacy(
|
||||
bip32,
|
||||
bytes.fromhex('6137beb405d9ff777172aa879e33edb34a1460e701802746c5ef96e741710e59'),
|
||||
bytes.fromhex('eb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8')
|
||||
)
|
||||
|
||||
assert v == bytes.fromhex("1c")
|
||||
assert r == bytes.fromhex("ea66f747173762715751c889fea8722acac3fc35db2c226d37a2e58815398f64")
|
||||
assert s == bytes.fromhex("52d8ba9153de9255da220ffd36762c0b027701a3b5110f0a765f94b16a9dfb55")
|
||||
Reference in New Issue
Block a user