25
doc/apdu.md
25
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<br/>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
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
@@ -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
|
||||
|
||||
<details>
|
||||
@@ -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|
|
||||
|---|---|--|--|--|--|
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
35
tests/speculos/docs/client.puml
Normal file
35
tests/speculos/docs/client.puml
Normal file
@@ -0,0 +1,35 @@
|
||||
@startuml Network
|
||||
|
||||
enum InsType <int> {
|
||||
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
|
||||
@@ -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'
|
||||
|
||||
13
tests/speculos/test_sign_cmd.py
Normal file
13
tests/speculos/test_sign_cmd.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user