Skip to content

Commit

Permalink
Merge pull request thesis#18 from blooo-io/test/LDG-490-app-write-tests
Browse files Browse the repository at this point in the history
Test/ldg 490 app write tests
  • Loading branch information
Z4karia authored Oct 15, 2024
2 parents dd4a031 + ce6c212 commit 402feb3
Show file tree
Hide file tree
Showing 110 changed files with 696 additions and 37 deletions.
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ PATH_APP_LOAD_PARAMS = "44'/0'" "44'/1'" "48'/0'" "48'/1'" "49'/0'" "49'/1'" "84

# Application version
APPVERSION_M = 1
APPVERSION_N = 0
APPVERSION_P = 4
APPVERSION_N = 1
APPVERSION_P = 0
APPVERSION_SUFFIX = # if not empty, appended at the end. Do not add a dash.

ifeq ($(APPVERSION_SUFFIX),)
Expand All @@ -70,6 +70,16 @@ ifndef COIN
COIN=acre_testnet
endif

ifeq ($(COIN),acre)
COIN_VARIANT=1
else ifeq ($(COIN),acre_testnet)
COIN_VARIANT=2
else
$(error Unsupported COIN value: $(COIN))
endif

DEFINES += COIN_VARIANT=$(COIN_VARIANT)

########################################
# Application custom permissions #
########################################
Expand Down
36 changes: 27 additions & 9 deletions bitcoin_client/ledger_bitcoin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .embit.networks import NETWORKS

from .command_builder import BitcoinCommandBuilder, BitcoinInsType
from .common import Chain, read_uint, read_varint
from .common import Chain, read_uint, read_varint, SW_OK, SW_INTERRUPTED_EXECUTION
from .client_command import ClientCommandInterpreter
from .client_base import Client, TransportClient, PartialSignature
from .client_legacy import LegacyClient
Expand Down Expand Up @@ -72,7 +72,7 @@ def _make_request(
) -> Tuple[int, bytes]:
sw, response = self._apdu_exchange(apdu)

while sw == 0xE000:
while sw == SW_INTERRUPTED_EXECUTION:
if not client_intepreter:
raise RuntimeError("Unexpected SW_INTERRUPTED_EXECUTION received.")

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY)

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.REGISTER_WALLET)

if len(response) != 64:
Expand Down Expand Up @@ -152,7 +152,7 @@ def get_wallet_address(
client_intepreter,
)

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS)

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_PSBT)

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY)

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

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_MESSAGE)

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

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_WITHDRAW)

return base64.b64encode(response).decode('utf-8')

def sign_erc4361_message(self, message: Union[str, bytes], bip32_path: str) -> str:
if isinstance(message, str):
message_bytes = message.encode("utf-8")
else:
message_bytes = message

chunks = [message_bytes[64 * i: 64 * i + 64] for i in range((len(message_bytes) + 63) // 64)]

client_intepreter = ClientCommandInterpreter()
client_intepreter.add_known_list(chunks)

sw, response = self._make_request(self.builder.sign_erc4361_message(message_bytes, bip32_path), client_intepreter)

if sw != SW_OK:
raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_ERC4361_MESSAGE)

return base64.b64encode(response).decode('utf-8')

def _derive_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> Optional[str]:
desc_str = wallet.get_descriptor(change)
Expand Down
10 changes: 5 additions & 5 deletions bitcoin_client/ledger_bitcoin/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from .transport import Transport

from .common import Chain
from .common import Chain, SW_OK

from .command_builder import DefaultInsType
from .exception import DeviceException
Expand Down Expand Up @@ -35,7 +35,7 @@ def apdu_exchange(
) -> bytes:
sw, data = self.transport.exchange(cla, ins, p1, p2, None, data)

if sw != 0x9000:
if sw != SW_OK:
raise ApduException(sw, data)

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

response = self.transport_client.apdu_exchange(**apdu)
if self.debug:
print_response(0x9000, response)
print_response(SW_OK, response)

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

if sw != 0x9000:
if sw != SW_OK:
raise DeviceException(
error_code=sw, ins=DefaultInsType.GET_VERSION)

Expand Down
24 changes: 23 additions & 1 deletion bitcoin_client/ledger_bitcoin/command_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class BitcoinInsType(enum.IntEnum):
GET_MASTER_FINGERPRINT = 0x05
SIGN_MESSAGE = 0x10
SIGN_WITHDRAW = 0x11

SIGN_ERC4361_MESSAGE = 0x12
class FrameworkInsType(enum.IntEnum):
CONTINUE_INTERRUPTED = 0x01

Expand Down Expand Up @@ -236,6 +236,28 @@ def sign_withdraw(self, data_bytes: AcreWithdrawalDataBytes, bip32_path: str):
ins=BitcoinInsType.SIGN_WITHDRAW,
cdata=bytes(cdata)
)

def sign_erc4361_message(self, message: bytes, bip32_path: str):
cdata = bytearray()

bip32_path: List[bytes] = bip32_path_from_string(bip32_path)

# split message in 64-byte chunks (last chunk can be smaller)
n_chunks = (len(message) + 63) // 64
chunks = [message[64 * i: 64 * i + 64] for i in range(n_chunks)]

cdata += len(bip32_path).to_bytes(1, byteorder="big")
cdata += b''.join(bip32_path)

cdata += write_varint(len(message))

cdata += MerkleTree(element_hash(c) for c in chunks).root

return self.serialize(
cla=self.CLA_BITCOIN,
ins=BitcoinInsType.SIGN_ERC4361_MESSAGE,
cdata=bytes(cdata)
)

def continue_interrupted(self, cdata: bytes):
"""Command builder for CONTINUE.
Expand Down
2 changes: 2 additions & 0 deletions bitcoin_client/ledger_bitcoin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
UINT32_MAX: int = 4294967295
UINT16_MAX: int = 65535

SW_OK = 0x9000
SW_INTERRUPTED_EXECUTION = 0xE000

# from bitcoin-core/HWI
class Chain(Enum):
Expand Down
62 changes: 61 additions & 1 deletion doc/acre.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The main commands use `CLA = 0xE1`.
| E1 | 04 | SIGN_PSBT | Sign a PSBT with a registered or default wallet |
| E1 | 05 | GET_MASTER_FINGERPRINT | Return the fingerprint of the master public key |
| E1 | 10 | SIGN_MESSAGE | Sign a message with a key from a BIP32 path (Bitcoin Message Signing) |
| E1 | 11 | SIGN_WITHDRAWAL | Signs a Withdrawal message. The message being signed is the hash of the Acre Withdrawal transaction. |
| E1 | 12 | SIGN_ERC4361_MESSAGE | Signs an Ethereum Sign-In message (ERC-4361) in Bitcoin format. |

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

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

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.

### SIGN_ERC4361_MESSAGE

Signs an Ethereum Sign-In message (ERC-4361) in Bitcoin format.

The device shows on its secure screen the following information:
- Domain
- Address
- URI
- Version
- Nonce
- Issued At
- Expiration Time

The user should verify this information carefully before approving the signature.

#### Encoding

**Command**

| *CLA* | *INS* |
|-------|-------|
| E1 | 12 |

**Input data**

| Length | Name | Description |
|---------|-------------------|-------------|
| `1` | `n` | Number of derivation steps (maximum 8) |
| `4` | `bip32_path[0]` | First derivation step (big endian) |
| `4` | `bip32_path[1]` | Second derivation step (big endian) |
| | ... | |
| `4` | `bip32_path[n-1]` | `n`-th derivation step (big endian) |
| `<var>` | `msg_length` | The byte length of the message to sign (Bitcoin-style varint) |
| `32` | `msg_merkle_root` | The Merkle root of the message, split in 64-byte chunks |

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.

**Output data**

| Length | Description |
|--------|-------------|
| `65` | The returned signature, encoded in the standard Bitcoin message signing format |

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).

#### Description

The digest being signed is the double-SHA256 of the ERC-4361 message, after prefixing the message with:

- the magic string `"\x18Bitcoin Signed Message:\n"` (equal to `18426974636f696e205369676e6564204d6573736167653a0a` in hexadecimal)
- the length of the message, encoded as a Bitcoin-style variable length integer.

Note: The message is restricted to maximum 128 character lines.

#### Client commands

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.

## Client commands reference

This section documents the commands that the Hardware Wallet can request to the client when returning with a `SW_INTERRUPTED_EXECUTION` status word.
Expand Down Expand Up @@ -490,4 +550,4 @@ All the current commands use a commit-and-reveal approach: the APDU that starts
- If a Merkle proof is asked via `GET_MERKLE_LEAF_PROOF`, the proof is verified.
- 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*.

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`).
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`).
20 changes: 19 additions & 1 deletion ragger_bitcoin/ragger_bitcoin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Tuple, List, Optional, Union
from pathlib import Path

from bitcoin_client.ledger_bitcoin.common import SW_INTERRUPTED_EXECUTION
from ledger_bitcoin.common import Chain
from ledger_bitcoin.client_command import ClientCommandInterpreter
from ledger_bitcoin.client_base import TransportClient, PartialSignature
Expand Down Expand Up @@ -114,7 +115,7 @@ def _make_request_with_navigation(self, navigator: Navigator, apdu: dict, client
sw, response, index = self.ragger_navigate(
navigator, apdu, instructions, testname, index)

while sw == 0xE000:
while sw == SW_INTERRUPTED_EXECUTION:
if not client_intepreter:
raise RuntimeError(
"Unexpected SW_INTERRUPTED_EXECUTION received.")
Expand Down Expand Up @@ -233,7 +234,24 @@ def sign_withdraw(self, data: AcreWithdrawalData, bip32_path: str, navigator:
self.navigate = False

return response

def sign_erc4361_message(self, message: Union[str, bytes], bip32_path: str, navigator:
Optional[Navigator] = None,
instructions: Instructions = None,
testname: str = ""
) -> str:

if navigator:
self.navigate = True
self.navigator = navigator
self.testname = testname
self.instructions = instructions

response = NewClient.sign_erc4361_message(self, message, bip32_path)

self.navigate = False

return response

def createRaggerClient(backend, chain: Chain = Chain.MAIN, debug: bool = False, screenshot_dir:
Path = TESTS_ROOT_DIR) -> RaggerClient:
Expand Down
4 changes: 4 additions & 0 deletions ragger_bitcoin/ragger_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def confirm_withdrawal(self, save_screenshot=True):
self.new_request("Approve", NavInsID.USE_CASE_REVIEW_TAP,
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)

def confirm_erc4361_message(self, save_screenshot=True):
self.new_request("Approve", NavInsID.USE_CASE_REVIEW_TAP,
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)

def confirm_message(self, save_screenshot=True):
self.same_request("Sign", NavInsID.USE_CASE_REVIEW_TAP,
NavInsID.USE_CASE_REVIEW_CONFIRM, save_screenshot=save_screenshot)
Expand Down
1 change: 1 addition & 0 deletions src/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ typedef enum {
GET_MASTER_FINGERPRINT = 0x05,
SIGN_MESSAGE = 0x10,
WITHDRAW = 0x11,
SIGN_ERC4361_MESSAGE = 0x12,
} command_e;
8 changes: 8 additions & 0 deletions src/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
*/
#define MAX_N_OUTPUTS_CAN_SIGN 512

// ERC4361 message constants
#define MESSAGE_CHUNK_SIZE 64
#define MAX_DOMAIN_LENGTH 64
#define MAX_URI_LENGTH 64
#define MAX_VERSION_LENGTH 5
#define MAX_NONCE_LENGTH 32
#define MAX_DATETIME_LENGTH 32

// SIGHASH flags
#define SIGHASH_DEFAULT 0x00000000
#define SIGHASH_ALL 0x00000001
Expand Down
1 change: 1 addition & 0 deletions src/handler/handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ void handler_register_wallet(dispatcher_context_t *dispatcher_context, uint8_t p
void handler_sign_message(dispatcher_context_t *dispatcher_context, uint8_t p2);
void handler_sign_psbt(dispatcher_context_t *dispatcher_context, uint8_t p2);
void handler_withdraw(dispatcher_context_t *dispatcher_context, uint8_t p2);
void handler_sign_erc4361_message(dispatcher_context_t *dispatcher_context, uint8_t p2);
Loading

0 comments on commit 402feb3

Please sign in to comment.