Skip to content

Commit 402feb3

Browse files
authored
Merge pull request #18 from blooo-io/test/LDG-490-app-write-tests
Test/ldg 490 app write tests
2 parents dd4a031 + ce6c212 commit 402feb3

File tree

110 files changed

+696
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+696
-37
lines changed

Makefile

+12-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ PATH_APP_LOAD_PARAMS = "44'/0'" "44'/1'" "48'/0'" "48'/1'" "49'/0'" "49'/1'" "84
4444

4545
# Application version
4646
APPVERSION_M = 1
47-
APPVERSION_N = 0
48-
APPVERSION_P = 4
47+
APPVERSION_N = 1
48+
APPVERSION_P = 0
4949
APPVERSION_SUFFIX = # if not empty, appended at the end. Do not add a dash.
5050

5151
ifeq ($(APPVERSION_SUFFIX),)
@@ -70,6 +70,16 @@ ifndef COIN
7070
COIN=acre_testnet
7171
endif
7272

73+
ifeq ($(COIN),acre)
74+
COIN_VARIANT=1
75+
else ifeq ($(COIN),acre_testnet)
76+
COIN_VARIANT=2
77+
else
78+
$(error Unsupported COIN value: $(COIN))
79+
endif
80+
81+
DEFINES += COIN_VARIANT=$(COIN_VARIANT)
82+
7383
########################################
7484
# Application custom permissions #
7585
########################################

bitcoin_client/ledger_bitcoin/client.py

+27-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .embit.networks import NETWORKS
1010

1111
from .command_builder import BitcoinCommandBuilder, BitcoinInsType
12-
from .common import Chain, read_uint, read_varint
12+
from .common import Chain, read_uint, read_varint, SW_OK, SW_INTERRUPTED_EXECUTION
1313
from .client_command import ClientCommandInterpreter
1414
from .client_base import Client, TransportClient, PartialSignature
1515
from .client_legacy import LegacyClient
@@ -72,7 +72,7 @@ def _make_request(
7272
) -> Tuple[int, bytes]:
7373
sw, response = self._apdu_exchange(apdu)
7474

75-
while sw == 0xE000:
75+
while sw == SW_INTERRUPTED_EXECUTION:
7676
if not client_intepreter:
7777
raise RuntimeError("Unexpected SW_INTERRUPTED_EXECUTION received.")
7878

@@ -86,7 +86,7 @@ def _make_request(
8686
def get_extended_pubkey(self, path: str, display: bool = False) -> str:
8787
sw, response = self._make_request(self.builder.get_extended_pubkey(path, display))
8888

89-
if sw != 0x9000:
89+
if sw != SW_OK:
9090
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY)
9191

9292
return response.decode()
@@ -106,7 +106,7 @@ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]:
106106
self.builder.register_wallet(wallet), client_intepreter
107107
)
108108

109-
if sw != 0x9000:
109+
if sw != SW_OK:
110110
raise DeviceException(error_code=sw, ins=BitcoinInsType.REGISTER_WALLET)
111111

112112
if len(response) != 64:
@@ -152,7 +152,7 @@ def get_wallet_address(
152152
client_intepreter,
153153
)
154154

155-
if sw != 0x9000:
155+
if sw != SW_OK:
156156
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS)
157157

158158
result = response.decode()
@@ -224,7 +224,7 @@ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_
224224
client_intepreter,
225225
)
226226

227-
if sw != 0x9000:
227+
if sw != SW_OK:
228228
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_PSBT)
229229

230230
# parse results and return a structured version instead
@@ -250,7 +250,7 @@ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_
250250
def get_master_fingerprint(self) -> bytes:
251251
sw, response = self._make_request(self.builder.get_master_fingerprint())
252252

253-
if sw != 0x9000:
253+
if sw != SW_OK:
254254
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY)
255255

256256
return response
@@ -268,7 +268,7 @@ def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str:
268268

269269
sw, response = self._make_request(self.builder.sign_message(message_bytes, bip32_path), client_intepreter)
270270

271-
if sw != 0x9000:
271+
if sw != SW_OK:
272272
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_MESSAGE)
273273

274274
return base64.b64encode(response).decode('utf-8')
@@ -304,10 +304,28 @@ def sign_withdraw(self, data: AcreWithdrawalData, bip32_path: str) -> str:
304304

305305
sw, response = self._make_request(self.builder.sign_withdraw(data_bytes, bip32_path), client_intepreter)
306306

307-
if sw != 0x9000:
307+
if sw != SW_OK:
308308
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_WITHDRAW)
309309

310310
return base64.b64encode(response).decode('utf-8')
311+
312+
def sign_erc4361_message(self, message: Union[str, bytes], bip32_path: str) -> str:
313+
if isinstance(message, str):
314+
message_bytes = message.encode("utf-8")
315+
else:
316+
message_bytes = message
317+
318+
chunks = [message_bytes[64 * i: 64 * i + 64] for i in range((len(message_bytes) + 63) // 64)]
319+
320+
client_intepreter = ClientCommandInterpreter()
321+
client_intepreter.add_known_list(chunks)
322+
323+
sw, response = self._make_request(self.builder.sign_erc4361_message(message_bytes, bip32_path), client_intepreter)
324+
325+
if sw != SW_OK:
326+
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_ERC4361_MESSAGE)
327+
328+
return base64.b64encode(response).decode('utf-8')
311329

312330
def _derive_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> Optional[str]:
313331
desc_str = wallet.get_descriptor(change)

bitcoin_client/ledger_bitcoin/client_base.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from .transport import Transport
88

9-
from .common import Chain
9+
from .common import Chain, SW_OK
1010

1111
from .command_builder import DefaultInsType
1212
from .exception import DeviceException
@@ -35,7 +35,7 @@ def apdu_exchange(
3535
) -> bytes:
3636
sw, data = self.transport.exchange(cla, ins, p1, p2, None, data)
3737

38-
if sw != 0x9000:
38+
if sw != SW_OK:
3939
raise ApduException(sw, data)
4040

4141
return data
@@ -92,9 +92,9 @@ def _apdu_exchange(self, apdu: dict) -> Tuple[int, bytes]:
9292

9393
response = self.transport_client.apdu_exchange(**apdu)
9494
if self.debug:
95-
print_response(0x9000, response)
95+
print_response(SW_OK, response)
9696

97-
return 0x9000, response
97+
return SW_OK, response
9898
except ApduException as e:
9999
if self.debug:
100100
print_response(e.sw, e.data)
@@ -129,7 +129,7 @@ def get_version(self) -> Tuple[str, str, bytes]:
129129
sw, response = self._make_request(
130130
{"cla": 0xB0, "ins": DefaultInsType.GET_VERSION, "p1": 0, "p2": 0, "data": b''})
131131

132-
if sw != 0x9000:
132+
if sw != SW_OK:
133133
raise DeviceException(
134134
error_code=sw, ins=DefaultInsType.GET_VERSION)
135135

bitcoin_client/ledger_bitcoin/command_builder.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class BitcoinInsType(enum.IntEnum):
4040
GET_MASTER_FINGERPRINT = 0x05
4141
SIGN_MESSAGE = 0x10
4242
SIGN_WITHDRAW = 0x11
43-
43+
SIGN_ERC4361_MESSAGE = 0x12
4444
class FrameworkInsType(enum.IntEnum):
4545
CONTINUE_INTERRUPTED = 0x01
4646

@@ -236,6 +236,28 @@ def sign_withdraw(self, data_bytes: AcreWithdrawalDataBytes, bip32_path: str):
236236
ins=BitcoinInsType.SIGN_WITHDRAW,
237237
cdata=bytes(cdata)
238238
)
239+
240+
def sign_erc4361_message(self, message: bytes, bip32_path: str):
241+
cdata = bytearray()
242+
243+
bip32_path: List[bytes] = bip32_path_from_string(bip32_path)
244+
245+
# split message in 64-byte chunks (last chunk can be smaller)
246+
n_chunks = (len(message) + 63) // 64
247+
chunks = [message[64 * i: 64 * i + 64] for i in range(n_chunks)]
248+
249+
cdata += len(bip32_path).to_bytes(1, byteorder="big")
250+
cdata += b''.join(bip32_path)
251+
252+
cdata += write_varint(len(message))
253+
254+
cdata += MerkleTree(element_hash(c) for c in chunks).root
255+
256+
return self.serialize(
257+
cla=self.CLA_BITCOIN,
258+
ins=BitcoinInsType.SIGN_ERC4361_MESSAGE,
259+
cdata=bytes(cdata)
260+
)
239261

240262
def continue_interrupted(self, cdata: bytes):
241263
"""Command builder for CONTINUE.

bitcoin_client/ledger_bitcoin/common.py

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
UINT32_MAX: int = 4294967295
1010
UINT16_MAX: int = 65535
1111

12+
SW_OK = 0x9000
13+
SW_INTERRUPTED_EXECUTION = 0xE000
1214

1315
# from bitcoin-core/HWI
1416
class Chain(Enum):

doc/acre.md

+61-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The main commands use `CLA = 0xE1`.
1616
| E1 | 04 | SIGN_PSBT | Sign a PSBT with a registered or default wallet |
1717
| E1 | 05 | GET_MASTER_FINGERPRINT | Return the fingerprint of the master public key |
1818
| E1 | 10 | SIGN_MESSAGE | Sign a message with a key from a BIP32 path (Bitcoin Message Signing) |
19+
| E1 | 11 | SIGN_WITHDRAWAL | Signs a Withdrawal message. The message being signed is the hash of the Acre Withdrawal transaction. |
20+
| E1 | 12 | SIGN_ERC4361_MESSAGE | Signs an Ethereum Sign-In message (ERC-4361) in Bitcoin format. |
1921

2022
The `CLA = 0xF8` is used for framework-specific (rather than app-specific) APDUs; at this time, only one command is present.
2123

@@ -392,6 +394,64 @@ The digest being signed is the double-SHA256 of the Withdrawal transaction hash,
392394

393395
The client must respond to the `GET_PREIMAGE`, `GET_MERKLE_LEAF_PROOF` and `GET_MERKLE_LEAF_INDEX` queries for the Merkle tree of the list of chunks in the Withdrawal data.
394396

397+
### SIGN_ERC4361_MESSAGE
398+
399+
Signs an Ethereum Sign-In message (ERC-4361) in Bitcoin format.
400+
401+
The device shows on its secure screen the following information:
402+
- Domain
403+
- Address
404+
- URI
405+
- Version
406+
- Nonce
407+
- Issued At
408+
- Expiration Time
409+
410+
The user should verify this information carefully before approving the signature.
411+
412+
#### Encoding
413+
414+
**Command**
415+
416+
| *CLA* | *INS* |
417+
|-------|-------|
418+
| E1 | 12 |
419+
420+
**Input data**
421+
422+
| Length | Name | Description |
423+
|---------|-------------------|-------------|
424+
| `1` | `n` | Number of derivation steps (maximum 8) |
425+
| `4` | `bip32_path[0]` | First derivation step (big endian) |
426+
| `4` | `bip32_path[1]` | Second derivation step (big endian) |
427+
| | ... | |
428+
| `4` | `bip32_path[n-1]` | `n`-th derivation step (big endian) |
429+
| `<var>` | `msg_length` | The byte length of the message to sign (Bitcoin-style varint) |
430+
| `32` | `msg_merkle_root` | The Merkle root of the message, split in 64-byte chunks |
431+
432+
The message to be signed is split into `ceil(msg_length/64)` chunks of 64 bytes (except the last chunk that could be smaller); `msg_merkle_root` is the root of the Merkle tree of the corresponding list of chunks.
433+
434+
**Output data**
435+
436+
| Length | Description |
437+
|--------|-------------|
438+
| `65` | The returned signature, encoded in the standard Bitcoin message signing format |
439+
440+
The signature is returned as a 65-byte binary string (1 byte equal to 32 or 33, followed by `r` and `s`, each of them represented as a 32-byte big-endian integer).
441+
442+
#### Description
443+
444+
The digest being signed is the double-SHA256 of the ERC-4361 message, after prefixing the message with:
445+
446+
- the magic string `"\x18Bitcoin Signed Message:\n"` (equal to `18426974636f696e205369676e6564204d6573736167653a0a` in hexadecimal)
447+
- the length of the message, encoded as a Bitcoin-style variable length integer.
448+
449+
Note: The message is restricted to maximum 128 character lines.
450+
451+
#### Client commands
452+
453+
The client must respond to the `GET_PREIMAGE`, `GET_MERKLE_LEAF_PROOF` and `GET_MERKLE_LEAF_INDEX` queries for the Merkle tree of the list of chunks in the message.
454+
395455
## Client commands reference
396456

397457
This section documents the commands that the Hardware Wallet can request to the client when returning with a `SW_INTERRUPTED_EXECUTION` status word.
@@ -490,4 +550,4 @@ All the current commands use a commit-and-reveal approach: the APDU that starts
490550
- If a Merkle proof is asked via `GET_MERKLE_LEAF_PROOF`, the proof is verified.
491551
- If the index of a leaf is asked `GET_MERKLE_LEAF_INDEX`, the proof for that element is requested via `GET_MERKLE_LEAF_PROOF` and the proof verified, *even if the leaf value is known*.
492552

493-
Care needs to be taken in designing protocols, as the client might lie by omission (for example, fail to reveal that a leaf of a Merkle tree is present during a call to `GET_MERKLE_LEAF_INDEX`).
553+
Care needs to be taken in designing protocols, as the client might lie by omission (for example, fail to reveal that a leaf of a Merkle tree is present during a call to `GET_MERKLE_LEAF_INDEX`).

ragger_bitcoin/ragger_bitcoin.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Tuple, List, Optional, Union
22
from pathlib import Path
33

4+
from bitcoin_client.ledger_bitcoin.common import SW_INTERRUPTED_EXECUTION
45
from ledger_bitcoin.common import Chain
56
from ledger_bitcoin.client_command import ClientCommandInterpreter
67
from ledger_bitcoin.client_base import TransportClient, PartialSignature
@@ -114,7 +115,7 @@ def _make_request_with_navigation(self, navigator: Navigator, apdu: dict, client
114115
sw, response, index = self.ragger_navigate(
115116
navigator, apdu, instructions, testname, index)
116117

117-
while sw == 0xE000:
118+
while sw == SW_INTERRUPTED_EXECUTION:
118119
if not client_intepreter:
119120
raise RuntimeError(
120121
"Unexpected SW_INTERRUPTED_EXECUTION received.")
@@ -233,7 +234,24 @@ def sign_withdraw(self, data: AcreWithdrawalData, bip32_path: str, navigator:
233234
self.navigate = False
234235

235236
return response
237+
238+
def sign_erc4361_message(self, message: Union[str, bytes], bip32_path: str, navigator:
239+
Optional[Navigator] = None,
240+
instructions: Instructions = None,
241+
testname: str = ""
242+
) -> str:
236243

244+
if navigator:
245+
self.navigate = True
246+
self.navigator = navigator
247+
self.testname = testname
248+
self.instructions = instructions
249+
250+
response = NewClient.sign_erc4361_message(self, message, bip32_path)
251+
252+
self.navigate = False
253+
254+
return response
237255

238256
def createRaggerClient(backend, chain: Chain = Chain.MAIN, debug: bool = False, screenshot_dir:
239257
Path = TESTS_ROOT_DIR) -> RaggerClient:

ragger_bitcoin/ragger_instructions.py

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def confirm_withdrawal(self, save_screenshot=True):
8282
self.new_request("Approve", NavInsID.USE_CASE_REVIEW_TAP,
8383
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)
8484

85+
def confirm_erc4361_message(self, save_screenshot=True):
86+
self.new_request("Approve", NavInsID.USE_CASE_REVIEW_TAP,
87+
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)
88+
8589
def confirm_message(self, save_screenshot=True):
8690
self.same_request("Sign", NavInsID.USE_CASE_REVIEW_TAP,
8791
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)

src/commands.h

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ typedef enum {
1111
GET_MASTER_FINGERPRINT = 0x05,
1212
SIGN_MESSAGE = 0x10,
1313
WITHDRAW = 0x11,
14+
SIGN_ERC4361_MESSAGE = 0x12,
1415
} command_e;

src/constants.h

+8
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
*/
5252
#define MAX_N_OUTPUTS_CAN_SIGN 512
5353

54+
// ERC4361 message constants
55+
#define MESSAGE_CHUNK_SIZE 64
56+
#define MAX_DOMAIN_LENGTH 64
57+
#define MAX_URI_LENGTH 64
58+
#define MAX_VERSION_LENGTH 5
59+
#define MAX_NONCE_LENGTH 32
60+
#define MAX_DATETIME_LENGTH 32
61+
5462
// SIGHASH flags
5563
#define SIGHASH_DEFAULT 0x00000000
5664
#define SIGHASH_ALL 0x00000001

src/handler/handlers.h

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ void handler_register_wallet(dispatcher_context_t *dispatcher_context, uint8_t p
99
void handler_sign_message(dispatcher_context_t *dispatcher_context, uint8_t p2);
1010
void handler_sign_psbt(dispatcher_context_t *dispatcher_context, uint8_t p2);
1111
void handler_withdraw(dispatcher_context_t *dispatcher_context, uint8_t p2);
12+
void handler_sign_erc4361_message(dispatcher_context_t *dispatcher_context, uint8_t p2);

0 commit comments

Comments
 (0)