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