Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
fbeutin-ledger committed Dec 23, 2024
1 parent 1f53274 commit 8e1d889
Show file tree
Hide file tree
Showing 96 changed files with 429 additions and 120 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
2 changes: 2 additions & 0 deletions src/check_addresses_and_amounts.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ static uint16_t check_payout_or_refund_address(command_e ins,

// Depending on the current command, check either PAYOUT or REFUND
if (ins == CHECK_PAYOUT_ADDRESS) {
PRINTF("Preparing to run CHECK_PAYOUT_ADDRESS\n");
address_to_check = G_swap_ctx.swap_transaction.payout_address;
address_max_size = sizeof(G_swap_ctx.swap_transaction.payout_address);
extra_id_to_check = G_swap_ctx.swap_transaction.payout_extra_id;
} else {
PRINTF("Preparing to run CHECK_REFUND_ADDRESS\n");
address_to_check = G_swap_ctx.swap_transaction.refund_address;
address_max_size = sizeof(G_swap_ctx.swap_transaction.refund_address);
extra_id_to_check = G_swap_ctx.swap_transaction.refund_extra_id;
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
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
139 changes: 127 additions & 12 deletions test/python/apps/solana.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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

from .solana_tlv import FieldTag, format_tlv
from .solana_keychain import Key, sign_data

class INS(IntEnum):
# DEPRECATED - Use non "16" suffixed variants below
Expand All @@ -15,6 +19,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 +75,98 @@ 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 provide_trusted_name(self,
source_contract: bytes,
trusted_name: bytes,
address: bytes,
chain_id: int,
challenge: Optional[int] = None):

payload = format_tlv(FieldTag.TAG_STRUCTURE_TYPE, 3)
payload += format_tlv(FieldTag.TAG_VERSION, 2)
payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_TYPE, 0x06)
payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_SOURCE, 0x06)
payload += format_tlv(FieldTag.TAG_TRUSTED_NAME, trusted_name)
payload += format_tlv(FieldTag.TAG_CHAIN_ID, chain_id)
payload += format_tlv(FieldTag.TAG_ADDRESS, address)
payload += format_tlv(FieldTag.TAG_TRUSTED_NAME_SOURCE_CONTRACT, source_contract)
if challenge is not None:
payload += format_tlv(FieldTag.TAG_CHALLENGE, challenge)
payload += format_tlv(FieldTag.TAG_SIGNER_KEY_ID, 0) # test key
payload += format_tlv(FieldTag.TAG_SIGNER_ALGO, 1) # secp256k1
payload += format_tlv(FieldTag.TAG_DER_SIGNATURE,
sign_data(Key.TRUSTED_NAME, payload))

# send PKI certificate
if self._pki_client is None:
print(f"Ledger-PKI Not supported on '{self._client.firmware.name}'")
else:
# pylint: disable=line-too-long
if self._client.firmware == Firmware.NANOSP:
cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F6534010135010315473045022100D494B106E217B46BB90BF20A4E9285529C4C8382D9B80FF462F74942579785F802202D68D0F85CD7CA36BDF351FD41332F310E93163BD175F6A92446C14A3329CC8B" # noqa: E501
elif self._client.firmware == Firmware.NANOX:
cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F653401013501021546304402207FCD665B94B43A6E838E8CD68BE52403D38A7E6A98E2CE291AB1C5D24A41101D02207AB1863E5CB127D9E8A680AC63FF2F2CBEA79CE76652A72832EF154BF1AD6477" # noqa: E501
elif self._client.firmware == Firmware.STAX:
cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F65340101350104154730450221008F8FB0117C8D51F0D13A77680C18CA98B4B317C3D6C67F23BF9198410BEDF1A1022023B1052CA43E86E2411831990C64B1E027D85E142AD39F480948E3EF9517E55E" # noqa: E501
elif self._client.firmware == Firmware.FLEX:
cert_apdu = "01010102010211040000000212010013020002140101160400000000200C547275737465645F4E616D6530020004310104320121332102B91FBEC173E3BA4A714E014EBC827B6F899A9FA7F4AC769CDE284317A00F4F6534010135010515473045022100CEF28780DCAFA3A485D83406D519F9AC12FD9B9C3AA7AE798896013F07DD178D022020F01B1AB1D2AAEDA70357F615EAC55E17FE94EC36DF9DE850CEFACBC98D16C8" # noqa: E501
# pylint: enable=line-too-long

self._pki_client.send_certificate(bytes.fromhex(cert_apdu))

# send TLV trusted info
res: RAPDU = self._client.exchange(CLA, INS.INS_TRUSTED_INFO, P1_NON_CONFIRM, P2_NONE, payload)
assert res.status == StatusWord.OK

def get_challenge(self) -> bytes:
challenge: RAPDU = self._client.exchange(CLA, INS.INS_GET_CHALLENGE,P1_NON_CONFIRM, P2_NONE)

assert challenge.status == StatusWord.OK
return challenge.data

def get_public_key(self, derivation_path: bytes) -> bytes:
public_key: RAPDU = self._client.exchange(CLA, INS.INS_GET_PUBKEY,
Expand All @@ -85,24 +176,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 +210,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 8e1d889

Please sign in to comment.