diff --git a/doc/apdu.md b/doc/apdu.md index 9ee325d..a08b260 100644 --- a/doc/apdu.md +++ b/doc/apdu.md @@ -50,7 +50,7 @@ This command returns specific application configuration |CLA|INS|P1|P2|Lc|Le| |---|---|--|--|--|--| -|E0|06|00|00|00|04| +|E0|06|00|00|00|00| :inbox_tray: input data @@ -63,7 +63,23 @@ None |0x01 : arbitrary data signature enabled by user
0x02 : ERC 20 Token information needs to be provided externally|1| |Application major version|1| |Application minor version|1| -|Application patch version|1| +|Application patch version|1| + +Exemple: +CLA: E0 +INS: 06 +P1 : 00 +P2 : 00 +Lc : 00 +Le : 00 + +|CLA|INS|P1|P2|Lc|Le| +|---|---|--|--|--|--| +|E0|06|00|00|00|00| + +-> E0 06 00 00 00 00 + +
@@ -80,6 +96,7 @@ The address can be optionally checked on the device before being returned. Usefull link: - [HD Wallet by ledger](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets) - [BIP-044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +- [psd-application](https://developers.ledger.com/docs/nano-app/psd-applications/) |CLA|INS|P1 |P2 |Lc |Le | |---|---|-------------------------------------------------|--------------------------------|----------|----------| @@ -170,6 +187,8 @@ This command has been supported since firmware version 1.6.0 ## SIGN +- [RLP encoding](https://medium.com/coinmonks/data-structure-in-ethereum-episode-1-recursive-length-prefix-rlp-encoding-decoding-d1016832f919) + ### SIGN ETH TRANSACTION
@@ -353,7 +372,7 @@ The signature is computed on len(pluginName) || pluginName || contractAddress || methodSelector -signed by the following secp256k1 public key 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 +signed by the following secp256k1 public key `0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353` |CLA|INS|P1|P2|Lc|Le| |---|---|--|--|--|--| diff --git a/tests/speculos/README.md b/tests/speculos/README.md index 0a33a8a..8ff6e84 100644 --- a/tests/speculos/README.md +++ b/tests/speculos/README.md @@ -28,12 +28,12 @@ pytest tests/speculos/ you will find the list of apdu [here](../../doc/apdu.md) - Get - - GET APP CONFIGURATIOn - - [X] Simple test - - GET ETH PUBLIC ADDRESS - - [X] Test get key of coin (Ether, Dai) - - [ ] Test get key of coin (Ether, Dai) with display - - [ ] Test without chain code + - GET APP CONFIGURATION ( 1 test ) + - Get the configuration + - GET ETH PUBLIC ADDRESS ( 3 tests ) + - Ether coin without display + - Dai coin with display + - Dai coin with display and reject - GET ETH2 PUBLIC KEY - [ ] Test get key - [ ] Test get key with display \ No newline at end of file diff --git a/tests/speculos/boilerplate_client/boilerplate_cmd.py b/tests/speculos/boilerplate_client/boilerplate_cmd.py index ec5f5d9..a7874ba 100644 --- a/tests/speculos/boilerplate_client/boilerplate_cmd.py +++ b/tests/speculos/boilerplate_client/boilerplate_cmd.py @@ -1,3 +1,6 @@ +from ast import List +from contextlib import contextmanager +from ctypes.wintypes import INT import struct from typing import Tuple @@ -24,7 +27,7 @@ class BoilerplateCommand: except ApduException as error: raise DeviceException(error_code=error.sw, ins=InsType.INS_GET_VERSION) - # response = MAJOR (1) || MINOR (1) || PATCH (1) + # response = FLAG (1) || MAJOR (1) || MINOR (1) || PATCH (1) assert len(response) == 4 info, major, minor, patch = struct.unpack( @@ -34,12 +37,17 @@ class BoilerplateCommand: return info, major, minor, patch - def get_public_key(self, bip32_path: str, display: bool = False) -> Tuple[bytes, bytes, bytes]: + @contextmanager + def get_public_key(self, bip32_path: str, display: bool = False, result: List = list()) -> Tuple[bytes, bytes, bytes]: try: - response = self.client._apdu_exchange( - self.builder.get_public_key(bip32_path=bip32_path, - display=display) - ) # type: int, bytes + chunk: bytes = self.builder.get_public_key(bip32_path=bip32_path, display=display) + + with self.client.apdu_exchange_nowait(cla=chunk[0], ins=chunk[1], + p1=chunk[2], p2=chunk[3], + data=chunk[5:]) as exchange: + yield exchange + response: bytes = exchange.receive() + except ApduException as error: raise DeviceException(error_code=error.sw, ins=InsType.INS_GET_PUBLIC_KEY) @@ -64,8 +72,36 @@ class BoilerplateCommand: chain_code: bytes = response[offset:] assert len(response) == 1 + pub_key_len + 1 + eth_addr_len + 32 # 32 -> chain_code_len + + result.append(uncompressed_addr_len) + result.append(eth_addr) + result.append(chain_code) - return uncompressed_addr_len, eth_addr, chain_code + def simple_sign_tx(self, bip32_path: str, transaction) -> Tuple[int, int, int]: + chunk: bytes = self.builder.simple_sign_tx(bip32_path=bip32_path, transaction=transaction) + response: bytes = b"" + + with self.client.apdu_exchange_nowait(cla=chunk[0], ins=chunk[1], + p1=chunk[2], p2=chunk[3], + data=chunk[5:]) as exchange: + # Review Transaction + self.client.press_and_release('right') + # Address 1/3, 2/3, 3/3 + self.client.press_and_release('right') + self.client.press_and_release('right') + self.client.press_and_release('right') + # Amount + self.client.press_and_release('right') + # Approve + self.client.press_and_release('both') + response = exchange.receive() + + # response = V (1) || R (32) || S (32) + assert len(response) == 65 + + v, r, s = struct.unpack("BII", response) + + return v, r, s def sign_tx(self, bip32_path: str, transaction: Transaction) -> Tuple[int, bytes]: sw: int diff --git a/tests/speculos/boilerplate_client/boilerplate_cmd_builder.py b/tests/speculos/boilerplate_client/boilerplate_cmd_builder.py index 619e09a..2cc54d6 100644 --- a/tests/speculos/boilerplate_client/boilerplate_cmd_builder.py +++ b/tests/speculos/boilerplate_client/boilerplate_cmd_builder.py @@ -3,6 +3,8 @@ import logging import struct from typing import List, Tuple, Union, Iterator, cast +import rlp + from boilerplate_client.transaction import Transaction from boilerplate_client.utils import bip32_path_from_string @@ -29,9 +31,17 @@ def chunkify(data: bytes, chunk_len: int) -> Iterator[Tuple[bool, bytes]]: class InsType(enum.IntEnum): - INS_GET_PUBLIC_KEY = 0x02 - INS_SIGN_TX = 0x04 - INS_GET_CONFIGURATION = 0x06 + INS_GET_PUBLIC_KEY = 0x02 + INS_SIGN_TX = 0x04 + INS_GET_CONFIGURATION = 0x06 + INS_SIGN_PERSONAL_TX = 0x08 + INS_PROVIDE_ERC20 = 0x0A + INS_SIGN_EIP712 = 0x0c + INS_ETH2_GET_PUBLIC_KEY = 0x0E + INS_SET_ETH2_WITHDRAWAL = 0x10 + INS_SET_EXTERNAL_PLUGIN = 0x12 + INS_PROVIDE_NFT_INFORMATION = 0x14 + INS_SET_PLUGIN = 0x16 class BoilerplateCommandBuilder: @@ -185,3 +195,35 @@ class BoilerplateCommandBuilder: p1=0x00, p2=0x00, cdata=chunk) + + def simple_sign_tx(self, bip32_path: str, transaction) -> bytes: + """Command builder for INS_SIGN_TX. + + Parameters + ---------- + bip32_path : str + String representation of BIP32 path. + transaction : Transaction + Representation of the transaction to be signed. + + Yields + ------- + bytes + APDU command chunk for INS_SIGN_TX. + + """ + bip32_paths: List[bytes] = bip32_path_from_string(bip32_path) + + cdata: bytes = b"".join([ + len(bip32_paths).to_bytes(1, byteorder="big"), + *bip32_paths, + rlp.encode(transaction) + ]) + + print(cdata) + + return self.serialize(cla=self.CLA, + ins=InsType.INS_SIGN_TX, + p1=0x00, + p2=0x00, + cdata=cdata) diff --git a/tests/speculos/docs/client.puml b/tests/speculos/docs/client.puml new file mode 100644 index 0000000..ccadea4 --- /dev/null +++ b/tests/speculos/docs/client.puml @@ -0,0 +1,35 @@ +@startuml Network + +enum InsType { + INS_GET_PUBLIC_KEY = 0x02 + INS_SIGN_TX = 0x04 + INS_GET_CONFIGURATION = 0x06 + INS_SIGN_PERSONAL_TX = 0x08 + INS_PROVIDE_ERC20 = 0x0A + INS_SIGN_EIP712 = 0x0c + INS_ETH2_GET_PUBLIC_KEY = 0x0E + INS_SET_ETH2_WITHDRAWAL = 0x10 + INS_SET_EXTERNAL_PLUGIN = 0x12 + INS_PROVIDE_NFT_INFORMATION = 0x14 + INS_SET_PLUGIN = 0x16 +} + +class BoilerPlateCommandBuilder { + +bytes serialize(cla int, ins InsType, p1 int, p2 int, cdata bytes) + ____ + .. APDU Builder.. + +get_configuration() -> bytes + +get_public_key(bip32_path str, display bool) -> bytes +} + +class BoilerplateCommand { + +get_configuration() -> Tuple[int, int, int] + +get_public_key(bip32_path str, diplay bool) -> Tuple[bytes, bytes, bytes] +} + +class Transaction { + +serialize() -> bytes + +from_bytes(cls, hexa: Union[bytes, BytesIO]) +} + +@enduml \ No newline at end of file diff --git a/tests/speculos/test_pubkey_cmd.py b/tests/speculos/test_pubkey_cmd.py index aee64cf..9a79429 100644 --- a/tests/speculos/test_pubkey_cmd.py +++ b/tests/speculos/test_pubkey_cmd.py @@ -1,16 +1,18 @@ +from cgitb import reset from pickle import TRUE from typing import Tuple +import boilerplate_client + def test_get_public_key(cmd): - # ETHER COIN - uncompressed_addr_len, eth_addr, chain_code = cmd.get_public_key( - bip32_path="44'/60'/1'/0/0", - display=False - ) # type: bytes, bytes, bytes + # ETHER COIN without display + result: list = [] + with cmd.get_public_key(bip32_path="44'/60'/1'/0/0", display=False, result=result) as exchange: + pass - print("HERE", uncompressed_addr_len) + uncompressed_addr_len, eth_addr, chain_code = result assert len(uncompressed_addr_len) == 65 assert len(eth_addr) == 40 @@ -20,14 +22,20 @@ def test_get_public_key(cmd): assert eth_addr == b'463e4e114AA57F54f2Fd2C3ec03572C6f75d84C2' assert chain_code == b'\xaf\x89\xcd)\xea${8I\xec\xc80\xc2\xc8\x94\\e1\xd6P\x87\x07?\x9f\xd09\x00\xa0\xea\xa7\x96\xc8' - # DAI COIN - uncompressed_addr_len, eth_addr, chain_code = cmd.get_public_key( - bip32_path="44'/700'/1'/0/0", - display=False - ) # type: bytes, bytes, bytes - - print("HERE2", uncompressed_addr_len) + # DAI COIN with display + result: list = [] + with cmd.get_public_key(bip32_path="44'/700'/1'/0/0", display=True, result=result) as exchange: + cmd.client.press_and_release('right') + # Verify address + cmd.client.press_and_release('right') + # Address 1/3, 2/3, 3/3 + cmd.client.press_and_release('right') + cmd.client.press_and_release('right') + cmd.client.press_and_release('right') + # Approved + cmd.client.press_and_release('both') + uncompressed_addr_len, eth_addr, chain_code = result assert len(uncompressed_addr_len) == 65 assert len(eth_addr) == 40 assert len(chain_code) == 32 @@ -35,3 +43,23 @@ def test_get_public_key(cmd): assert uncompressed_addr_len == b'\x04V\x8a\x15\xdc\xed\xc8[\x16\x17\x8d\xaf\xcax\x91v~{\x9c\x06\xba\xaa\xde\xf4\xe7\x9f\x86\x1d~\xed)\xdc\n8\x9c\x84\xf01@E\x13]\xd7~6\x8e\x8e\xabb-\xad\xcdo\xc3Fw\xb7\xc8y\xdbQ/\xc3\xe5\x18' assert eth_addr == b'Ba9A9aED0a1AbBE1da1155F64e73e57Af7995880' assert chain_code == b'4\xaa\x95\xf4\x02\x12\x12-T\x155\x86\xed\xc5\x0b\x1d8\x81\xae\xce\xbd\x1a\xbbv\x9a\xc7\xd5\x1a\xd0KT\xe4' + + +def test_reject_get_public_key(cmd): + try: + # DAI COIN with display + result: list = [] + with cmd.get_public_key(bip32_path="44'/700'/1'/0/0", display=True, result=result) as exchange: + cmd.client.press_and_release('right') + # Verify address + cmd.client.press_and_release('right') + # Address 1/3, 2/3, 3/3 + cmd.client.press_and_release('right') + cmd.client.press_and_release('right') + cmd.client.press_and_release('right') + # Reject + cmd.client.press_and_release('right') + cmd.client.press_and_release('both') + + except boilerplate_client.exception.errors.DenyError as error: + assert error.args[0] == '0x6985' diff --git a/tests/speculos/test_sign_cmd.py b/tests/speculos/test_sign_cmd.py new file mode 100644 index 0000000..21f0c40 --- /dev/null +++ b/tests/speculos/test_sign_cmd.py @@ -0,0 +1,13 @@ +from urllib import response +import boilerplate_client +import struct + + +def test_sign(cmd): + transaction = "dog" + + response = cmd.simple_sign_tx(bip32_path="44'/60'/1'/0/0", transaction=transaction) + + print(response) + + \ No newline at end of file