Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
fbeutin-ledger committed Jan 10, 2025
1 parent 1f53274 commit 291dba8
Show file tree
Hide file tree
Showing 104 changed files with 472 additions and 177 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ APPNAME = "Exchange"
APPVERSION_M = 4
APPVERSION_N = 1
APPVERSION_P = 0
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)-pkiv1"
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)-pkiv2"

# Application source files
APP_SOURCE_PATH += src
Expand Down
11 changes: 3 additions & 8 deletions src/tlv_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ bool parse_tlv(const tlv_handler_t *handlers,
const buf_t *payload,
tlv_out_t *tlv_out,
tlv_tag_t signature_tag,
uint8_t tlv_hash[INT256_LENGTH],
uint8_t hash[INT256_LENGTH],
uint32_t *received_tags_flags) {
tlv_step_t step = TLV_TAG;
tlv_data_t data;
Expand Down Expand Up @@ -277,13 +277,8 @@ bool parse_tlv(const tlv_handler_t *handlers,
}

// If the user requested the hash of the TLV, forward it to him
if (tlv_hash != NULL) {
CX_ASSERT(cx_hash_no_throw((cx_hash_t *) &hash_ctx,
CX_LAST,
NULL,
0,
tlv_hash,
INT256_LENGTH));
if (hash != NULL) {
CX_ASSERT(cx_hash_no_throw((cx_hash_t *) &hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH));
}

return true;
Expand Down
2 changes: 1 addition & 1 deletion src/tlv_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ bool parse_tlv(const tlv_handler_t *handlers,
const buf_t *payload,
tlv_out_t *tlv_out,
tlv_tag_t signature_tag,
uint8_t tlv_hash[INT256_LENGTH],
uint8_t hash[INT256_LENGTH],
uint32_t *received_tags_flags);

bool received_required_tags(uint32_t rcv_flags, const tlv_tag_t *tags, size_t tag_count);
Expand Down
3 changes: 2 additions & 1 deletion test/python/apps/cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .bitcoin import BTC_PACKED_DERIVATION_PATH, BTC_CONF
from .stellar import XLM_PACKED_DERIVATION_PATH, XLM_CONF
from .solana_utils import SOL_PACKED_DERIVATION_PATH, SOL_CONF
from .solana_utils import JUP_PACKED_DERIVATION_PATH, JUP_CONF
from .xrp import XRP_PACKED_DERIVATION_PATH, XRP_CONF
from .tezos import XTZ_PACKED_DERIVATION_PATH, XTZ_CONF
from .polkadot import DOT_PACKED_DERIVATION_PATH, DOT_CONF
Expand Down Expand Up @@ -42,6 +43,7 @@ def get_conf_for_ticker(self, overload_signer: Optional[SigningAuthority]=None)
LTC_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="LTC", conf=LTC_CONF, packed_derivation_path=LTC_PACKED_DERIVATION_PATH)
XLM_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="XLM", conf=XLM_CONF, packed_derivation_path=XLM_PACKED_DERIVATION_PATH)
SOL_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="SOL", conf=SOL_CONF, packed_derivation_path=SOL_PACKED_DERIVATION_PATH)
JUP_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="JUP", conf=JUP_CONF, packed_derivation_path=JUP_PACKED_DERIVATION_PATH)
XRP_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="XRP", conf=XRP_CONF, packed_derivation_path=XRP_PACKED_DERIVATION_PATH)
XTZ_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="XTZ", conf=XTZ_CONF, packed_derivation_path=XTZ_PACKED_DERIVATION_PATH)
BNB_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="BNB", conf=BSC_CONF, packed_derivation_path=BSC_PACKED_DERIVATION_PATH)
Expand All @@ -57,7 +59,6 @@ def get_conf_for_ticker(self, overload_signer: Optional[SigningAuthority]=None)
USDD_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="USDD", conf=TRX_USDD_CONF, packed_derivation_path=TRX_PACKED_DERIVATION_PATH)
ADA_BYRON_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="ADA", conf=ADA_CONF, packed_derivation_path=ADA_BYRON_PACKED_DERIVATION_PATH)
ADA_SHELLEY_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="ADA", conf=ADA_CONF, packed_derivation_path=ADA_SHELLEY_PACKED_DERIVATION_PATH)
# JUP_CURRENCY_CONFIGURATION = CurrencyConfiguration(ticker="JUP", conf=JUP_CONF, packed_derivation_path=JUP_PACKED_DERIVATION_PATH)


# Helper that can be called from outside if we want to generate errors easily
Expand Down
9 changes: 5 additions & 4 deletions test/python/apps/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

from ..utils import handle_lib_call_start_or_stop, int_to_minimally_sized_bytes, prefix_with_len_custom, get_version_from_makefile
from .exchange_transaction_builder import SubCommand
from .solana_keychain import Key, sign_data
from .pki.pem_signer import KeySigner

from .solana_tlv import FieldTag, format_tlv
from .pki.tlv import FieldTag, format_tlv

MAX_CHUNK_SIZE = 255

Expand Down Expand Up @@ -114,6 +114,7 @@ def __init__(self,
self._rate = rate
self._subcommand = subcommand
self._pki_client = PKIClient(self._client)
self.trusted_name_key_signer = KeySigner("trusted_name.pem")

@property
def rate(self) -> Rate:
Expand Down Expand Up @@ -212,10 +213,10 @@ def send_trusted_name_descriptor(self,
if not skip_signature_field:
if fake_signature_field:
payload += format_tlv(FieldTag.TAG_DER_SIGNATURE,
sign_data(Key.TRUSTED_NAME, payload + b"0"))
self.trusted_name_key_signer.sign_data(payload + b"0"))
else:
payload += format_tlv(FieldTag.TAG_DER_SIGNATURE,
sign_data(Key.TRUSTED_NAME, payload))
self.trusted_name_key_signer.sign_data(payload))

return self._exchange_split(Command.SEND_TRUSTED_NAME_DESCRIPTOR, payload=payload)

Expand Down
33 changes: 28 additions & 5 deletions test/python/apps/exchange_test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class ExchangeTestRunner:
wrong_destination_error_code = None
wrong_amount_error_code = None

alias_address: Optional[bytes] = None
_alias_refund_address: Optional[bytes] = None
_alias_payout_address: Optional[bytes] = None

def __init__(self, backend, exchange_navigation_helper):
self.backend = backend
self.exchange_navigation_helper = exchange_navigation_helper
Expand Down Expand Up @@ -116,6 +120,13 @@ def _perform_valid_exchange(self, subcommand, tx_infos, from_currency_configurat
from_configuration = from_currency_configuration.get_conf_for_ticker()

if subcommand == SubCommand.SWAP_NG:
if self._alias_refund_address is not None:
challenge = ex.get_challenge().data
ex.send_pki_certificate_and_trusted_name_descriptor(challenge=challenge, trusted_name=tx_infos["refund_address"], address=self._alias_refund_address)
if self._alias_payout_address is not None:
challenge = ex.get_challenge().data
ex.send_pki_certificate_and_trusted_name_descriptor(challenge=challenge, trusted_name=tx_infos["payout_address"], address=self._alias_payout_address)

to_configuration = to_currency_configuration.get_conf_for_ticker()
ex.check_payout_address(to_configuration)

Expand All @@ -140,7 +151,7 @@ def _perform_valid_exchange(self, subcommand, tx_infos, from_currency_configurat

self.exchange_navigation_helper.wait_for_library_spinner()

def perform_valid_swap_from_custom(self, destination, send_amount, fees, destination_memo, refund_address=None, refund_memo=None, ui_validation=True):
def perform_valid_swap_from_custom(self, destination, send_amount, fees, destination_memo, refund_address=None, refund_memo=None, ui_validation=True, allow_alias=True):
# Refund data is almost always 'valid', make it optionnal to specify it
refund_address = self.valid_refund if refund_address is None else refund_address
refund_memo = self.valid_refund_memo if refund_memo is None else refund_memo
Expand All @@ -156,9 +167,11 @@ def perform_valid_swap_from_custom(self, destination, send_amount, fees, destina
"amount_to_provider": int_to_minimally_sized_bytes(send_amount),
"amount_to_wallet": b"\246\333t\233+\330\000", # Default
}
if allow_alias:
self._alias_refund_address = self.alias_address
self._perform_valid_exchange(SubCommand.SWAP_NG, tx_infos, self.currency_configuration, cal.ETH_CURRENCY_CONFIGURATION, fees, ui_validation=ui_validation)

def perform_valid_thorswap_from_custom(self, destination, send_amount, fees, payin_extra_data, refund_address=None, ui_validation=True):
def perform_valid_thorswap_from_custom(self, destination, send_amount, fees, payin_extra_data, refund_address=None, ui_validation=True, allow_alias=True):
refund_address = self.valid_refund if refund_address is None else refund_address
tx_infos = {
"payin_address": destination,
Expand All @@ -172,9 +185,11 @@ def perform_valid_thorswap_from_custom(self, destination, send_amount, fees, pay
"amount_to_provider": int_to_minimally_sized_bytes(send_amount),
"amount_to_wallet": b"\246\333t\233+\330\000", # Default
}
if allow_alias:
self._alias_refund_address = self.alias_address
self._perform_valid_exchange(SubCommand.SWAP_NG, tx_infos, self.currency_configuration, cal.ETH_CURRENCY_CONFIGURATION, fees, ui_validation=ui_validation)

def perform_valid_swap_to_custom(self, destination, send_amount, fees, destination_memo, ui_validation=True):
def perform_valid_swap_to_custom(self, destination, send_amount, fees, destination_memo, ui_validation=True, allow_alias=True):
tx_infos = {
"payin_address": "0xDad77910DbDFdE764fC21FCD4E74D71bBACA6D8D", # Default
"payin_extra_id": "", # Default
Expand All @@ -187,6 +202,8 @@ def perform_valid_swap_to_custom(self, destination, send_amount, fees, destinati
"amount_to_provider": int_to_minimally_sized_bytes(send_amount),
"amount_to_wallet": b"\246\333t\233+\330\000", # Default
}
if allow_alias:
self._alias_payout_address = self.alias_address
self._perform_valid_exchange(SubCommand.SWAP_NG, tx_infos, cal.ETH_CURRENCY_CONFIGURATION, self.currency_configuration, fees, ui_validation=ui_validation)

def perform_valid_fund_from_custom(self, destination, send_amount, fees):
Expand Down Expand Up @@ -245,13 +262,19 @@ def perform_test_swap_wrong_refund(self):
self.valid_destination_memo_1,
refund_address=self.fake_refund,
refund_memo=self.fake_refund_memo,
ui_validation=False)
ui_validation=False,
allow_alias=False)
assert e.value.status == Errors.INVALID_ADDRESS

# We test that the currency app returns a fail when checking an incorrect payout address
def perform_test_swap_wrong_payout(self):
with pytest.raises(ExceptionRAPDU) as e:
self.perform_valid_swap_to_custom(self.fake_payout, self.valid_send_amount_1, self.valid_fees_1, self.fake_payout_memo, ui_validation=False)
self.perform_valid_swap_to_custom(self.fake_payout,
self.valid_send_amount_1,
self.valid_fees_1,
self.fake_payout_memo,
ui_validation=False,
allow_alias=False)
assert e.value.status == Errors.INVALID_ADDRESS

# The absolute standard swap, using default values, user accepts on UI
Expand Down
28 changes: 28 additions & 0 deletions test/python/apps/pki/pem_signer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import hashlib
from ecdsa import SigningKey
from ecdsa.util import sigencode_der


class KeySigner:
def __init__(self, pem_name: str):
"""
Initialize the KeySigner instance with a specific PEM file.
:param pem_name: Name of the PEM file (without extension), e.g., 'trusted_name'
"""
pem_path = os.path.join(os.path.dirname(__file__), f"{pem_name}")
if not os.path.exists(pem_path):
raise FileNotFoundError(f"PEM file not found: {pem_path}")

with open(pem_path, "r") as pem_file:
self._signing_key = SigningKey.from_pem(pem_file.read(), hashlib.sha256)

def sign_data(self, data: bytes) -> bytes:
"""
Generate a SECP256K1 signature of the given data.
:param data: Data to sign as bytes.
:return: Signature as bytes in DER format.
"""
return self._signing_key.sign_deterministic(data, sigencode=sigencode_der)
File renamed without changes.
File renamed without changes.
91 changes: 77 additions & 14 deletions test/python/apps/solana.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import List, Generator
from typing import List, Generator, Optional
from enum import IntEnum
from contextlib import contextmanager

from ragger.backend.interface import BackendInterface, RAPDU

from ragger.firmware import Firmware
from ragger.error import ExceptionRAPDU

class INS(IntEnum):
# DEPRECATED - Use non "16" suffixed variants below
Expand All @@ -15,6 +16,8 @@ class INS(IntEnum):
INS_GET_PUBKEY = 0x05
INS_SIGN_MESSAGE = 0x06
INS_SIGN_OFFCHAIN_MESSAGE = 0x07
INS_GET_CHALLENGE = 0x20
INS_TRUSTED_INFO = 0x21


CLA = 0xE0
Expand Down Expand Up @@ -69,13 +72,49 @@ def _extend_and_serialize_multiple_derivations_paths(derivations_paths: List[byt
serialized += derivations_path
return serialized

class StatusWord(IntEnum):
OK = 0x9000
ERROR_NO_INFO = 0x6a00
INVALID_DATA = 0x6a80
INSUFFICIENT_MEMORY = 0x6a84
INVALID_INS = 0x6d00
INVALID_P1_P2 = 0x6b00
CONDITION_NOT_SATISFIED = 0x6985
REF_DATA_NOT_FOUND = 0x6a88
EXCEPTION_OVERFLOW = 0x6807
NOT_IMPLEMENTED = 0x911c

class PKIClient:
_CLA: int = 0xB0
_INS: int = 0x06

def __init__(self, client: BackendInterface) -> None:
self._client = client

def send_certificate(self, payload: bytes) -> RAPDU:
response = self.send_raw(payload)
assert response.status == StatusWord.OK


def send_raw(self, payload: bytes) -> RAPDU:
header = bytearray()
header.append(self._CLA)
header.append(self._INS)
header.append(0x04) # PubKeyUsage = 0x04
header.append(0x00)
header.append(len(payload))
return self._client.exchange_raw(header + payload)


class SolanaClient:
client: BackendInterface

def __init__(self, client: BackendInterface):
self._client = client

self._pki_client: Optional[PKIClient] = None
if self._client.firmware != Firmware.NANOS:
# LedgerPKI not supported on Nanos
self._pki_client = PKIClient(self._client)

def get_public_key(self, derivation_path: bytes) -> bytes:
public_key: RAPDU = self._client.exchange(CLA, INS.INS_GET_PUBKEY,
Expand All @@ -85,24 +124,32 @@ def get_public_key(self, derivation_path: bytes) -> bytes:
return public_key.data


def split_and_prefix_message(self, derivation_path : bytes, message: bytes) -> List[bytes]:
@contextmanager
def send_public_key_with_confirm(self, derivation_path: bytes) -> bytes:
with self._client.exchange_async(CLA, INS.INS_GET_PUBKEY,
P1_CONFIRM, P2_NONE,
derivation_path):
yield


def split_and_prefix_message(self, derivation_path: bytes, message: bytes) -> List[bytes]:
assert len(message) <= 65535, "Message to send is too long"
header: bytes = _extend_and_serialize_multiple_derivations_paths([derivation_path])
# Check to see if this data needs to be split up and sent in chunks.
max_size = MAX_CHUNK_SIZE - len(header)
max_size = MAX_CHUNK_SIZE
message_splited = [message[x:x + max_size] for x in range(0, len(message), max_size)]
# Add the header to every chunk
return [header + s for s in message_splited]
# The first chunk is the header, then all chunks with max size
return [header] + message_splited


def send_first_message_batch(self, messages: List[bytes], p1: int) -> RAPDU:
self._client.exchange(CLA, INS.INS_SIGN_MESSAGE, p1, P2_MORE, messages[0])
def send_first_message_batch(self, ins: INS, messages: List[bytes], p1: int) -> RAPDU:
self._client.exchange(CLA, ins, p1, P2_MORE, messages[0])
for m in messages[1:]:
self._client.exchange(CLA, INS.INS_SIGN_MESSAGE, p1, P2_MORE | P2_EXTEND, m)
self._client.exchange(CLA, ins, p1, P2_MORE | P2_EXTEND, m)


@contextmanager
def send_async_sign_message(self,
def send_async_sign_request(self,
ins: INS,
derivation_path : bytes,
message: bytes) -> Generator[None, None, None]:
message_splited_prefixed = self.split_and_prefix_message(derivation_path, message)
Expand All @@ -111,17 +158,33 @@ def send_async_sign_message(self,
# Send all chunks with P2_EXTEND except for the first chunk
if len(message_splited_prefixed) > 1:
final_p2 = P2_EXTEND
self.send_first_message_batch(message_splited_prefixed[:-1], P1_CONFIRM)
self.send_first_message_batch(ins, message_splited_prefixed[:-1], P1_CONFIRM)
else:
final_p2 = 0

with self._client.exchange_async(CLA,
INS.INS_SIGN_MESSAGE,
ins,
P1_CONFIRM,
final_p2,
message_splited_prefixed[-1]):
yield


@contextmanager
def send_async_sign_message(self,
derivation_path : bytes,
message: bytes) -> Generator[None, None, None]:
with self.send_async_sign_request(INS.INS_SIGN_MESSAGE, derivation_path, message):
yield


@contextmanager
def send_async_sign_offchain_message(self,
derivation_path : bytes,
message: bytes) -> Generator[None, None, None]:
with self.send_async_sign_request(INS.INS_SIGN_OFFCHAIN_MESSAGE, derivation_path, message):
yield


def get_async_response(self) -> RAPDU:
return self._client.last_async_response
Loading

0 comments on commit 291dba8

Please sign in to comment.