diff --git a/tests/application_client/command_builder.py b/tests/application_client/command_builder.py index aeac3737..b993b36b 100644 --- a/tests/application_client/command_builder.py +++ b/tests/application_client/command_builder.py @@ -16,18 +16,62 @@ from input_files.cvote import MAX_CIP36_PAYLOAD_SIZE, CVoteTestCase from input_files.signOpCert import OpCertTestCase from input_files.signMsg import SignMsgTestCase, MessageAddressFieldType +from input_files.signTx import SignTxTestCase, TxInput, TxOutput, Certificate +from input_files.signTx import TxAuxiliaryDataType, TxAuxiliaryData, TxAuxiliaryDataHash, Withdrawal +from input_files.signTx import TxOutputBabbage, ThirdPartyAddressParams, CertificateType, CredentialType from application_client.app_def import InsType, AddressType, StakingDataSourceType class P1Type(IntEnum): + # Derive Address P1_RETURN = 0x01 P1_DISPLAY = 0x02 + # Sign CIP36 Vote P1_INIT = 0x01 P1_CHUNK = 0x02 P1_CONFIRM = 0x03 P1_WITNESS = 0x04 - + # SignTx + P1_INPUTS = 0x02 + P1_OUTPUTS = 0x03 + P1_FEE = 0x04 + P1_TTL = 0x05 + P1_CERTIFICATES = 0x06 + P1_WITHDRAWALS = 0x07 + P1_AUX_DATA = 0x08 + P1_VALIDITY_INTERVAL_START = 0x09 + P1_TX_CONFIRM = 0x0a + P1_MINT = 0x0b + P1_SCRIPT_DATA_HASH = 0x0c + P1_COLLATERAL_INPUTS = 0x0d + P1_REQUIRED_SIGNERS = 0x0e + P1_TX_WITNESSES = 0x0f + P1_TOTAL_COLLATERAL = 0x10 + P1_REFERENCE_INPUTS = 0x11 + P1_COLLATERAL_OUTPUT = 0x12 + P1_VOTING_PROCEDURES = 0x13 + P1_TREASURY = 0x15 + P1_DONATION = 0x16 + + +class P2Type(IntEnum): + # SignTx Outputs + P2_BASIC_DATA = 0x30 + P2_DATUM = 0x34 + P2_DATUM_CHUNK = 0x35 + P2_SCRIPT = 0x36 + P2_SCRIPT_CHUNK = 0x37 + P2_CONFIRM = 0x33 + # SignTx Aux Data + P2_INIT = 0x36 + P2_VOTE_KEY = 0x30 + P2_DELEGATION = 0x37 + P2_STAKING_KEY = 0x31 + P2_PAYMENT_ADDRESS = 0x32 + P2_NONCE = 0x33 + P2_VOTING_PURPOSE = 0x35 + P2_AUX_CONFIRM = 0x34 class CommandBuilder: _CLA: int = 0xd7 @@ -270,6 +314,271 @@ def sign_msg_confirm(self) -> bytes: return self._serialize(InsType.SIGN_MSG, P1Type.P1_CONFIRM, 0x00) + def sign_tx_init(self, testCase: SignTxTestCase) -> bytes: + """APDU Builder for Sign TX - INIT step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + # Options (1B) + # NetworkId (1B) + # ProtocolMagic (4B) + # TTL option flag (1B) + # auxiliary Data option flag (1B) + # validityIntervalStart option flag (1B) + # mint + # scriptDataHash + # includeNetworkId + # collateralOutput + # totalCollateral + # treasury + # donation + # signingMode + # TX inputs length (4B) + # TX outputs length (4B) + # TX certificates length (4B) + # TX withdrawals length (4B) + # witnessLegacy + # collateralInputs + # requiredSigners + # referenceInputs + # votingProcedures + # witnessBabbage + + data = bytes() + data += testCase.options.to_bytes(8, "big") + data += testCase.tx.network.networkId.to_bytes(1, "big") + data += testCase.tx.network.protocol.to_bytes(4, "big") + data += self._serializeOptionFlags(testCase.tx.ttl is not None) + data += self._serializeOptionFlags(testCase.tx.auxiliaryData is not None) + data += self._serializeOptionFlags(testCase.tx.validityIntervalStart is not None) + data += self._serializeOptionFlags(len(testCase.tx.mint) > 0) + data += self._serializeOptionFlags(testCase.tx.scriptDataHash is not None) + data += self._serializeOptionFlags(testCase.tx.includeNetworkId is not None) + data += self._serializeOptionFlags(testCase.tx.collateralOutput is not None) + data += self._serializeOptionFlags(testCase.tx.totalCollateral is not None) + data += self._serializeOptionFlags(testCase.tx.treasury is not None) + data += self._serializeOptionFlags(testCase.tx.donation is not None) + data += testCase.signingMode.to_bytes(1, "big") + data += len(testCase.tx.inputs).to_bytes(4, "big") + data += len(testCase.tx.outputs).to_bytes(4, "big") + data += len(testCase.tx.certificates).to_bytes(4, "big") + data += len(testCase.tx.withdrawals).to_bytes(4, "big") + data += len(testCase.tx.collateralInputs).to_bytes(4, "big") + data += len(testCase.tx.requiredSigners).to_bytes(4, "big") + data += len(testCase.tx.referenceInputs).to_bytes(4, "big") + data += len(testCase.tx.votingProcedures).to_bytes(4, "big") + # Compute the number of Babbage Witnesses Buffer + nbPaths = sum(1 for input in testCase.tx.inputs if input.path) + data += nbPaths.to_bytes(4, "big") # number of Babbage Witnesses Buffer + return self._serialize(InsType.SIGN_TX, P1Type.P1_INIT, 0x00, data) + + + def sign_tx_aux_data(self, auxData: TxAuxiliaryData) -> bytes: + """APDU Builder for Sign TX - AUX DATA step + + Args: + auxData (TxAuxiliaryData): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + if isinstance(auxData, TxAuxiliaryDataHash): + data += TxAuxiliaryDataType.ARBITRARY_HASH.to_bytes(1, "big") + data += bytes.fromhex(auxData.hashHex) + else: + data += TxAuxiliaryDataType.CIP36_REGISTRATION.to_bytes(1, "big") + return self._serialize(InsType.SIGN_TX, P1Type.P1_AUX_DATA, 0x00, data) + + + def sign_tx_inputs(self, txInput: TxInput) -> bytes: + """APDU Builder for Sign TX - INPUTS step + + Args: + txInput (TxInput): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + data += bytes.fromhex(txInput.txHashHex) + data += txInput.outputIndex.to_bytes(4, "big") + return self._serialize(InsType.SIGN_TX, P1Type.P1_INPUTS, 0x00, data) + + + def sign_tx_outputs_basic(self, txOutput: TxOutput) -> bytes: + """APDU Builder for Sign TX - OUTPUTS step - BASIC DATA level + + Args: + txOutput (TxOutput): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + # Format (1B) + # Tx Output destination + # Coin (8B) + # TokenBundle Length (4B) + # datum option flag (1B) + # referenceScriptHex option flag (1B) + + data = bytes() + data += txOutput.format.to_bytes(1, "big") + data += txOutput.destinationType.to_bytes(1, "big") + if isinstance(txOutput.destination, ThirdPartyAddressParams): + data += int(len(txOutput.destination.addressHex) / 2).to_bytes(4, "big") + data += bytes.fromhex(txOutput.destination.addressHex) + else: + data += self._serializeAddressParams(txOutput.destination) + data += txOutput.amount.to_bytes(8, "big") + data += len(txOutput.tokenBundle).to_bytes(4, "big") + data += self._serializeOptionFlags(txOutput.datumHashHex is not None) + if isinstance(txOutput, TxOutputBabbage): + data += self._serializeOptionFlags(txOutput.referenceScriptHex is not None) + return self._serialize(InsType.SIGN_TX, P1Type.P1_OUTPUTS, P2Type.P2_BASIC_DATA, data) + + + def sign_tx_outputs_confirm(self) -> bytes: + """APDU Builder for Sign TX - OUTPUTS step - CONFIRM level + + Returns: + Serial data APDU + """ + + return self._serialize(InsType.SIGN_TX, P1Type.P1_OUTPUTS, P2Type.P2_CONFIRM) + + + def sign_tx_fee(self, testCase: SignTxTestCase) -> bytes: + """APDU Builder for Sign TX - FEE step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + data += testCase.tx.fee.to_bytes(8, "big") + return self._serialize(InsType.SIGN_TX, P1Type.P1_FEE, 0x00, data) + + + def sign_tx_ttl(self, testCase: SignTxTestCase) -> bytes: + """APDU Builder for Sign TX - TTL step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + assert testCase.tx.ttl is not None + data += testCase.tx.ttl.to_bytes(8, "big") + return self._serialize(InsType.SIGN_TX, P1Type.P1_TTL, 0x00, data) + + + def sign_tx_withdrawal(self, withdrawal: Withdrawal) -> bytes: + """APDU Builder for Sign TX - WITHDRAWAL step + + Args: + withdrawal (Withdrawal): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + data += withdrawal.amount.to_bytes(8, "big") + data += withdrawal.type.to_bytes(1, "big") + if withdrawal.stakeCredential.keyValue.startswith("m/"): + data += pack_derivation_path(withdrawal.stakeCredential.keyValue) + else: + data += bytes.fromhex(withdrawal.stakeCredential.keyValue) + return self._serialize(InsType.SIGN_TX, P1Type.P1_WITHDRAWALS, 0x00, data) + + + def sign_tx_certificate(self, certificate: Certificate) -> bytes: + """APDU Builder for Sign TX - WITHDRAWAL step + + Args: + certificate (Certificate): Test parameters + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + if certificate.type in (CertificateType.STAKE_REGISTRATION, CertificateType.STAKE_DEREGISTRATION): + data += certificate.type.to_bytes(1, "big") + else: + raise NotImplementedError("Not implemented yet") + if certificate.stakeCredential.keyValue.startswith("m/"): + data += CredentialType.KEY_PATH.to_bytes(1, "big") + data += pack_derivation_path(certificate.stakeCredential.keyValue) + else: + data += CredentialType.KEY_HASH.to_bytes(1, "big") + data += bytes.fromhex(certificate.stakeCredential.keyValue) + return self._serialize(InsType.SIGN_TX, P1Type.P1_CERTIFICATES, 0x00, data) + + + def sign_tx_confirm(self) -> bytes: + """APDU Builder for Sign TX - CONFIRM step + + Returns: + Serial data APDU + """ + + # Serialization format: + return self._serialize(InsType.SIGN_TX, P1Type.P1_TX_CONFIRM) + + + def sign_tx_witness(self, path: str) -> bytes: + """APDU Builder for Sign TX - WITNESS step + + Args: + path (str): Input Test path + + Returns: + Serial data APDU + """ + + # Serialization format: + data = bytes() + data += pack_derivation_path(path) + return self._serialize(InsType.SIGN_TX, P1Type.P1_TX_WITNESSES, 0x00, data) + + + def _serializeOptionFlags(self, included: bool) -> bytes: + """Serialize Flag option value + + Args: + included (boolean): Flag value + + Returns: + Serial data APDU + """ + value = 0x02 if included else 0x01 + return value.to_bytes(1, "big") + + def _serializeAddressParams(self, testCase: DeriveAddressTestCase) -> bytes: """Serialize address parameters diff --git a/tests/application_client/command_sender.py b/tests/application_client/command_sender.py index f6617442..9414ab17 100644 --- a/tests/application_client/command_sender.py +++ b/tests/application_client/command_sender.py @@ -16,6 +16,8 @@ from input_files.cvote import CVoteTestCase from input_files.signOpCert import OpCertTestCase from input_files.signMsg import SignMsgTestCase +from input_files.signTx import SignTxTestCase, TxInput, TxOutput, TxAuxiliaryData +from input_files.signTx import Withdrawal, Certificate from application_client.command_builder import CommandBuilder, P1Type from application_client.app_def import Errors @@ -280,3 +282,154 @@ def sign_msg_confirm(self) -> Generator[None, None, None]: with self._exchange_async(self._cmd_builder.sign_msg_confirm()): yield + + + @contextmanager + def sign_tx_init(self, testCase: SignTxTestCase) -> Generator[None, None, None]: + """APDU Sign TX - INIT step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_init(testCase)): + yield + + + def sign_tx_aux_data(self, auxData: TxAuxiliaryData) -> RAPDU: + """APDU Sign TX - INPUTS step + + Args: + auxData (TxAuxiliaryData): Test parameters + + Returns: + Response APDU + """ + + return self._exchange(self._cmd_builder.sign_tx_aux_data(auxData)) + + + def sign_tx_inputs(self, txInput: TxInput) -> RAPDU: + """APDU Sign TX - INPUTS step + + Args: + txInput (TxInput): Test parameters + + Returns: + Response APDU + """ + + return self._exchange(self._cmd_builder.sign_tx_inputs(txInput)) + + + @contextmanager + def sign_tx_outputs_basic(self, txOutput: TxOutput) -> Generator[None, None, None]: + """APDU Sign TX - OUTPUTS step - BASIC DATA level + + Args: + txOutput (TxOutput): Test parameters + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_outputs_basic(txOutput)): + yield + + + def sign_tx_outputs_confirm(self) -> RAPDU: + """APDU Sign TX - OUTPUTS step -CONFIRM level + + Returns: + Response APDU + """ + + return self._exchange(self._cmd_builder.sign_tx_outputs_confirm()) + + + @contextmanager + def sign_tx_fee(self, testCase: SignTxTestCase) -> Generator[None, None, None]: + """APDU Sign TX - FEE step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_fee(testCase)): + yield + + + def sign_tx_ttl(self, testCase: SignTxTestCase) -> RAPDU: + """APDU Sign TX - TTL step + + Args: + testCase (SignTxTestCase): Test parameters + + Returns: + Response APDU + """ + + return self._exchange(self._cmd_builder.sign_tx_ttl(testCase)) + + + @contextmanager + def sign_tx_withdrawal(self, withdrawal: Withdrawal) -> Generator[None, None, None]: + """APDU Sign TX - WITNESS step + + Args: + withdrawal (Withdrawal): Input Test path + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_withdrawal(withdrawal)): + yield + + + @contextmanager + def sign_tx_certificate(self, certificate: Certificate) -> Generator[None, None, None]: + """APDU Sign TX - CERTIFICATE step + + Args: + withdrawal (Withdrawal): Input Test path + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_certificate(certificate)): + yield + + + @contextmanager + def sign_tx_confirm(self) -> Generator[None, None, None]: + """APDU Sign TX - CONFIRM step + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_confirm()): + yield + + + @contextmanager + def sign_tx_witness(self, path: str) -> Generator[None, None, None]: + """APDU Sign TX - WITNESS step + + Args: + path (str): Input Test path + + Returns: + Generator + """ + + with self._exchange_async(self._cmd_builder.sign_tx_witness(path)): + yield diff --git a/tests/input_files/signTx.py b/tests/input_files/signTx.py new file mode 100644 index 00000000..1005ffa1 --- /dev/null +++ b/tests/input_files/signTx.py @@ -0,0 +1,489 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2024 Ledger SAS +# SPDX-License-Identifier: LicenseRef-LEDGER +""" +This module provides Ragger tests for Sign TX check +""" + +from enum import IntEnum +from typing import List,Optional, Union +from dataclasses import dataclass, field +import base58 + +from application_client.app_def import NetworkDesc, Mainnet, Testnet +from input_files.derive_address import DeriveAddressTestCase, AddressType, pointer_to_str + +class TransactionSigningMode(IntEnum): + ORDINARY_TRANSACTION = 0x03 + POOL_REGISTRATION_AS_OWNER = 0x04 + POOL_REGISTRATION_AS_OPERATOR = 0x05 + MULTISIG_TRANSACTION = 0x06 + PLUTUS_TRANSACTION = 0x07 + +class TxAuxiliaryDataType(IntEnum): + ARBITRARY_HASH = 0x00 + CIP36_REGISTRATION = 0x01 + +class CredentialType(IntEnum): + KEY_PATH = 0x00 + SCRIPT_HASH = 0x01 + KEY_HASH = 0x02 + +class TxOutputFormat(IntEnum): + ARRAY_LEGACY = 0x00 + MAP_BABBAGE = 0x01 + +class TxOutputDestinationType(IntEnum): + THIRD_PARTY = 0x01 + DEVICE_OWNED = 0x02 + +class VoteOption(IntEnum): + NO = 0x00 + YES = 0x01 + ABSTAIN = 0x02 + +class VoterType(IntEnum): + COMMITTEE_KEY_HASH = 0 + COMMITTEE_KEY_PATH = 100 + COMMITTEE_SCRIPT_HASH = 1 + DREP_KEY_HASH = 2 + DREP_KEY_PATH = 102 + DREP_SCRIPT_HASH = 3 + STAKE_POOL_KEY_HASH = 4 + STAKE_POOL_KEY_PATH = 104 + +class CertificateType(IntEnum): + STAKE_REGISTRATION = 0 + STAKE_DEREGISTRATION = 1 + STAKE_DELEGATION = 2 + STAKE_POOL_REGISTRATION = 3 + STAKE_POOL_RETIREMENT = 4 + STAKE_POOL_UPDATE = 7 + STAKE_DEREGISTRATION_CONWAY = 8 + VOTE_DELEGATION = 9 + AUTHORIZE_COMMITTEE_HOT = 14 + RESIGN_COMMITTEE_COLD = 15 + DREP_REGISTRATION = 16 + DREP_DEREGISTRATION = 17 + DREP_UPDATE = 18 + + +@dataclass +class TxInput: + txHashHex: str + path: Optional[str] = None + outputIndex: int = 0 + +@dataclass +class Token: + assetNameHex: str + amount: int + +@dataclass +class AssetGroup: + policyIdHex: str + tokens: List[Token] + +@dataclass +class TxAuxiliaryDataHash: + hashHex: str + +@dataclass +class TxAuxiliaryDataCIP36: + voteKeyHex: str + # To Be Completed :) + +TxAuxiliaryData = Union[TxAuxiliaryDataHash, TxAuxiliaryDataCIP36] + +@dataclass +class ThirdPartyAddressParams: + addressHex: str + +@dataclass +class TxOutputAlonzo: + destination: Union[ThirdPartyAddressParams, DeriveAddressTestCase] + amount: int + format: TxOutputFormat + destinationType: TxOutputDestinationType + tokenBundle: List[AssetGroup] = field(default_factory=list) + datumHashHex: Optional[str] = None + +@dataclass +class TxOutputBabbage: + destination: Union[ThirdPartyAddressParams, DeriveAddressTestCase] + amount: int + format: TxOutputFormat + destinationType: TxOutputDestinationType + tokenBundle: List[AssetGroup] = field(default_factory=list) + datumHashHex: Optional[str] = None + referenceScriptHex: Optional[str] = None + +TxOutput = Union[TxOutputAlonzo, TxOutputBabbage] + +@dataclass +class RequiredSigner: + addressHex: str # signerPath or signerHash + +@dataclass +class CredentialParams: + keyValue: str # keyPath, keyHash or scriptHash + +@dataclass +class Withdrawal: + stakeCredential: CredentialParams + type: CredentialType + amount: int + +@dataclass +class Certificate: + type: CertificateType + stakeCredential: CredentialParams + hotCredential: Optional[CredentialParams] = None + deposit: Optional[int] = None + hash: Optional[str] = None + # To Be Completed :) + + +@dataclass +class GovActionId: + txHashHex: str + govActionIndex: int + +@dataclass +class AnchorParams: + url: str + hashHex: str + +@dataclass +class VotingProcedure: + vote: VoteOption + anchor: Optional[AnchorParams] + +@dataclass +class Voter: + type: VoterType + keyValue: str # keyPath, keyHash or scriptHash + +@dataclass +class Vote: + govActionId: GovActionId + votingProcedure: VotingProcedure + +@dataclass +class VoterVotes: + voter: Voter + votes: List[Vote] + +@dataclass +class Transaction: + network: NetworkDesc + inputs: List[TxInput] + outputs: List[TxOutput] + fee: int + ttl: Optional[int] = None + certificates: List[Certificate] = field(default_factory=list) + withdrawals: List[Withdrawal] = field(default_factory=list) + mint: List[AssetGroup] = field(default_factory=list) + collateralInputs: List[TxInput] = field(default_factory=list) + requiredSigners: List[RequiredSigner] = field(default_factory=list) + referenceInputs: List[TxInput] = field(default_factory=list) + votingProcedures: List[VoterVotes] = field(default_factory=list) + auxiliaryData: Optional[TxAuxiliaryData] = None + validityIntervalStart: Optional[int] = None + scriptDataHash: Optional[str] = None + includeNetworkId: Optional[bool] = None + collateralOutput: Optional[TxOutput] = None + totalCollateral: Optional[int] = None + treasury: Optional[int] = None + donation: Optional[int] = None + + +@dataclass +class Witness: + path: str + witnessSignatureHex: str + + +@dataclass +class TxAuxiliaryDataSupplement: + auxiliaryDataHashHex: str + cip36VoteRegistrationSignatureHex: str + + +@dataclass +class SignedTransactionData: + witnesses: List[Witness] + auxiliaryDataSupplement: Optional[TxAuxiliaryDataSupplement] = None + + +@dataclass +class SignTxTestCase: + name: str + tx: Transaction + signingMode: TransactionSigningMode + txBody: str + expected: SignedTransactionData + options: bool = False + additionalWitnessPaths: Optional[str] = None + txAuxiliaryData: Optional[str] = None + +# pylint: disable=line-too-long + +# Inputs +utxoByron = TxInput("1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", "m/44'/1815'/0'/0/0") +utxoShelley = TxInput("3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", "m/1852'/1815'/0'/0/0") +utxoNonReasonable = TxInput("3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", "m/1852'/1815'/456'/0/0") +utxoMultisig = TxInput("3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7") + +# Outputs +externalByronMainnet = TxOutputBabbage(ThirdPartyAddressParams(base58.b58decode("Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2").hex()), + 3003112, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.THIRD_PARTY) +externalByronDaedalusMainnet = TxOutputBabbage(ThirdPartyAddressParams(base58.b58decode("DdzFFzCqrht7HGoJ87gznLktJGywK1LbAJT2sbd4txmgS7FcYLMQFhawb18ojS9Hx55mrbsHPr7PTraKh14TSQbGBPJHbDZ9QVh6Z6Di").hex()), + 3003112, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.THIRD_PARTY) +externalByronTestnet = TxOutputBabbage(ThirdPartyAddressParams(base58.b58decode("2657WMsDfac6Cmfg4Varph2qyLKGi2K9E8jrtvjHVzfSjmbTMGy5sY3HpxCKsmtDA").hex()), + 3003112, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.THIRD_PARTY) + +# TODO: DEBUG bech32_decode +# externalShelleyBaseKeyhashKeyhash = TxOutputBabbage(ThirdPartyAddressParams(bech32.decode("addr", "addr1q97tqh7wzy8mnx0sr2a57c4ug40zzl222877jz06nt49g4zr43fuq3k0dfpqjh3uvqcsl2qzwuwsvuhclck3scgn3vys6wkj5d")[1].hex()), +# 1, +# TxOutputFormat.ARRAY_LEGACY, +# TxOutputDestinationType.THIRD_PARTY) +internalBaseWithStakingPath = TxOutputBabbage(DeriveAddressTestCase("", + Mainnet, + AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + "m/1852'/1815'/0'/0/0", + "m/1852'/1815'/0'/2/0"), + 7120787, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.DEVICE_OWNED) +internalBaseWithStakingKeyHash = TxOutputBabbage(DeriveAddressTestCase("", + Mainnet, + AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + "m/1852'/1815'/0'/0/0", + "122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), + 7120787, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.DEVICE_OWNED) +internalEnterprise = TxOutputBabbage(DeriveAddressTestCase("", + Mainnet, + AddressType.ENTERPRISE_KEY, + "m/1852'/1815'/0'/0/0"), + 7120787, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.DEVICE_OWNED) +internalPointer = TxOutputBabbage(DeriveAddressTestCase("", + Mainnet, + AddressType.POINTER_KEY, + "m/1852'/1815'/0'/0/0", + pointer_to_str(1, 2, 3)), + 7120787, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.DEVICE_OWNED) +internalBaseWithStakingPathNonReasonable = TxOutputBabbage(DeriveAddressTestCase("", + Mainnet, + AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + "m/1852'/1815'/456'/0/5000000", + "m/1852'/1815'/456'/2/0"), + 7120787, + TxOutputFormat.ARRAY_LEGACY, + TxOutputDestinationType.DEVICE_OWNED) + +testsByron = [ + SignTxTestCase("Sign tx with third-party Byron mainnet output", + Transaction(Mainnet, + [utxoByron], + [externalByronMainnet], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018182582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e802182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "9c12b678a047bf3148e867d969fba4f9295042c4fff8410782425a356820c79e7549de798f930480ba83615a5e2a19389c795a3281a59077b7d37cd5a071a606")])), + SignTxTestCase("Sign tx with third-party Byron Daedalus mainnet output", + Transaction(Mainnet, + [utxoByron], + [externalByronDaedalusMainnet], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018182584c82d818584283581cd2348b8ef7b8a6d1c922efa499c669b151eeef99e4ce3521e88223f8a101581e581cf281e648a89015a9861bd9e992414d1145ddaf80690be53235b0e2e5001a199834651a002dd2e802182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "fdca7969a3e8bc091c9ee32c04732f79bb7c0091f1796fd2c0e1de8aa8547a00457d50d0576f4dd421baf754499cf0e77584e848e3547addd5d5b7167597a307")])), + SignTxTestCase("Sign tx with third-party Byron testnet output", + Transaction(Testnet, + [utxoByron], + [externalByronTestnet], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018182582f82d818582583581c709bfb5d9733cbdd72f520cd2c8b9f8f942da5e6cd0b6994e1803b0aa10242182a001aef14e76d1a002dd2e802182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "224d103185f4709f7b749339ff7ba432d50ca5cb742678847f5e574858cf7dda7ed402399a9ddba81ecd731b6f939ba07a247cd570dcd543f83a9aeadc4f9603")])), +] + +testsShelleyNoCertificates = [ + SignTxTestCase("Sign tx without outputs", + Transaction(Mainnet, + [utxoShelley], + [], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018002182a030a", + SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + "190dcee0cc7125fd0ec104cf685674f1ad77f3e439a4a249e596a3306f9eb110ced8fb8ec59da15b721203c8973bd341d88e6a60b85c1e9f2623152fee8dc00a")])), + SignTxTestCase("Sign tx with 258 tag on inputs", + Transaction(Mainnet, + [utxoShelley], + [], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400d90102818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018002182a030a", + SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + "b842908ce71f3ad1e1a1e2261c3bfdbfdb48c3fe58484c3e0521588e94e48fdb001f30908b0cd041e6c1b9d9400739ea52d0ca7289b3d807d26d06d73961f609")]), + True), + + # # TODO: DEBUG bech32_decode in externalShelleyBaseKeyhashKeyhash + # SignTxTestCase("Sign tx without change address", + # Transaction(Mainnet, + # [utxoShelley], + # [externalShelleyBaseKeyhashKeyhash], + # 42, + # 10), + # TransactionSigningMode.ORDINARY_TRANSACTION, + # "a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7000181825839017cb05fce110fb999f01abb4f62bc455e217d4a51fde909fa9aea545443ac53c046cf6a42095e3c60310fa802771d0672f8fe2d1861138b090102182a030a", + # SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + # "ef73b0838e831dc86278e450ef36fecf4b7ad712dabb901f0d8470b6046ced8246cd086a15ad4c723c0cf01b685d8113e72a01511a5ceba374ebb8f4417afd0a")])), + + + SignTxTestCase("Sign tx with change base address with staking path", + Transaction(Mainnet, + [utxoByron], + [externalByronMainnet, + internalBaseWithStakingPath], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e88258390114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c1a006ca79302182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "4d29b3a66a152819baf9eb4866ab13ff6c5279ed80157463b96e2fd55aed14fa01d9df1de2a32560354da3db4f34cad79772804356401fa22523aabfd0363f03")])), + SignTxTestCase("Sign tx with change base address with staking key hash", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet, + internalBaseWithStakingKeyHash], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e88258390114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f1124122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "4ac5017c014886406a38a45417a165156280be63ca6975a5acffcabc0cc842ca603248b8a7ebfa729d7affce34518f4ca94fe797420a4d7aa0ef8c2b0ddfba0b")])), + SignTxTestCase("Sign tx with enterprise change address", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet, + internalEnterprise], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e882581d6114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241a006ca79302182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "70559415746a9646dc492b7758e18cb367c005ab0479558b3d540be2310eb1bb1dd0839081e22c0b4727e8bd8e163cfbfe9def99a8506fb4a6787a200862e00f")])), + SignTxTestCase("Sign tx with pointer change address", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet, + internalPointer], + 42, + 10), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e88258204114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11240102031a006ca79302182a030a", + SignedTransactionData([Witness("m/44'/1815'/0'/0/0", + "1838884e08cf6966ebe6b3e775191c4f08d90834723421779efd6aa96e52ffc91a24e5073abe6db94c74fe080d008258b3d989c159d9b87a9c778a51404abc08")])), + SignTxTestCase("Sign tx with non-reasonable account and address", + Transaction(Mainnet, + [utxoNonReasonable], + [internalBaseWithStakingPathNonReasonable], + 42, + 10, + auxiliaryData=TxAuxiliaryDataHash(f"{'deadbeef'*8}")), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182583901f90b0dfcace47bf03e88f7469a2f4fb3a7918461aa4765bfaf55f0dae260546c20562e598fb761f419dad27edcd49f4ee4f0540b8e40d4d51a006ca79302182a030a075820deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + SignedTransactionData([Witness("m/1852'/1815'/456'/0/0", + "bb1a035acf4a7b5dd68914f0007dfc4d1cc7b4d88748c0ad24326fd06597542ce0352075ed861b3ae012ab976cacd3dbbc58802cdf82409917ebf9a8bb182e04")])), + SignTxTestCase("Sign tx with path based withdrawal", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet], + 42, + 10, + withdrawals=[Withdrawal(CredentialParams("m/1852'/1815'/0'/2/0"), + CredentialType.KEY_PATH, + 111)]), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e802182a030a05a1581de11d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c186f", + SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + "22ef3b54a54a1f5390436911b23328225f92c660eb251189fceab2fa428187a2cec584ea5f6f9c9fcdf7f19bc496b3b2b9bb416ad07a3d31d73fbc0c05bec10c"), + Witness("m/1852'/1815'/0'/2/0", + "04b995979c2072b469c1e0ace5331c3d188e3e65d5a6f06aa4e608fb18a3588621370ee1b5d39d55afe0744aa4906785baa07210dc4cb49594eba507f7215102")])), + SignTxTestCase("Sign tx with auxiliary data hash", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet], + 42, + 10, + auxiliaryData=TxAuxiliaryDataHash(f"{'deadbeef'*8}")), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e802182a030a075820deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + "953c5243ba09570dd4e52642236834c138ad4abbbb21796a90540a11e8dc96e47043401d370cdaed70ebc332dd4db80c9b167fd7f20971c4f142875cea57200c")])), +] + +testsShelleyWithCertificates = [ + SignTxTestCase("Sign tx with a stake registration path certificate --- pre-Conway", + Transaction(Mainnet, + [utxoShelley], + [externalByronMainnet], + 42, + 10, + certificates=[Certificate(CertificateType.STAKE_REGISTRATION, + CredentialParams("m/1852'/1815'/0'/2/0"))]), + TransactionSigningMode.ORDINARY_TRANSACTION, + "a500818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e802182a030a048182008200581c1d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c", + SignedTransactionData([Witness("m/1852'/1815'/0'/0/0", + "9825594e5a91333b9f5762665ba316af34c2208bd7ef073178af5e48f2aae8673d50436045e292d5bb9be7492eeeda475a04e58621a326c91049a2ef26a33200")])), +] + +# testsConwayWithCertificates = [ +# ] + +# testsMultisig = [ +# ] + +# testsAllegra = [ +# ] + +# testsMary = [ +# ] + +# testsAlonzoTrezorComparison = [ +# ] + +# testsBabbageTrezorComparison = [ +# ] + +# testsMultidelegation = [ +# ] + +# testsConwayWithoutCertificates = [ +# ] + +# testsConwayVotingProcedures = [ +# ] diff --git a/tests/test_signTx.py b/tests/test_signTx.py new file mode 100644 index 00000000..b673f283 --- /dev/null +++ b/tests/test_signTx.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2024 Ledger SAS +# SPDX-License-Identifier: LicenseRef-LEDGER +""" +This module provides Ragger tests for Sign TX check +""" + +import pytest + +from ragger.backend import BackendInterface +from ragger.firmware import Firmware +from ragger.navigator import Navigator, NavInsID +from ragger.navigator.navigation_scenario import NavigateWithScenario + +from application_client.app_def import Errors, NetworkIds +from application_client.command_sender import CommandSender + +from input_files.signTx import testsByron, testsShelleyNoCertificates, SignTxTestCase +from input_files.signTx import ThirdPartyAddressParams, DeriveAddressTestCase +from input_files.derive_address import AddressType +from input_files.signTx import testsShelleyWithCertificates + +from utils import idTestFunc + + +@pytest.mark.parametrize( + "testCase", + testsByron + testsShelleyNoCertificates + testsShelleyWithCertificates, + ids=idTestFunc +) +def test_signTx(firmware: Firmware, + backend: BackendInterface, + navigator: Navigator, + scenario_navigator: NavigateWithScenario, + testCase: SignTxTestCase, + appFlags: dict) -> None: + """Check Sign TX""" + if appFlags['isAppXS']: + pytest.skip("Not supported by 'AppXS' version") + + # Use the app interface instead of raw interface + client = CommandSender(backend) + + # Send the INIT APDU + # ------------------------------------------- + moves = [] + if firmware.is_nano: + moves += [NavInsID.BOTH_CLICK] + if testCase.tx.network.networkId == NetworkIds.TESTNET: + moves += [NavInsID.BOTH_CLICK] + if len(testCase.tx.outputs) == 0: + moves += [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] + else: + moves += [NavInsID.SWIPE_CENTER_TO_LEFT] + if len(testCase.tx.outputs) == 0: + moves += [NavInsID.SWIPE_CENTER_TO_LEFT] + with client.sign_tx_init(testCase): + navigator.navigate(moves) + # Check the status (Asynchronous) + response = client.get_async_response() + assert response and response.status == Errors.SW_SUCCESS + + # Send the AUX DATA APDUs + # ------------------------------------------- + auxData: bool = False + if testCase.tx.auxiliaryData is not None: + response = client.sign_tx_aux_data(testCase.tx.auxiliaryData) + # Check the status + assert response.status == Errors.SW_SUCCESS + auxData = True + + # Send the INPUTS APDUs + # ------------------------------------------- + for txInput in testCase.tx.inputs: + response = client.sign_tx_inputs(txInput) + # Check the status + assert response.status == Errors.SW_SUCCESS + + # Send the OUTPUTS APDUs + # ------------------------------------------- + for txOutput in testCase.tx.outputs: + # Send Basic DATA + moves = [] + if isinstance(txOutput.destination, ThirdPartyAddressParams): + if firmware.is_nano: + moves = [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] * 2 + elif testCase.tx.network.networkId == NetworkIds.TESTNET: + moves = [NavInsID.TAPPABLE_CENTER_TAP] + elif isinstance(txOutput.destination, DeriveAddressTestCase): + if txOutput.destination.addrType == AddressType.POINTER_KEY: + if firmware.is_nano: + moves = [NavInsID.BOTH_CLICK] * 3 + [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] * 2 + else: + moves = [NavInsID.TAPPABLE_CENTER_TAP] + [NavInsID.SWIPE_CENTER_TO_LEFT] + [NavInsID.TAPPABLE_CENTER_TAP] * 2 + elif txOutput.destination.addrType == AddressType.ENTERPRISE_KEY: + if firmware.is_nano: + moves = [NavInsID.BOTH_CLICK] * 5 + else: + moves = [NavInsID.TAPPABLE_CENTER_TAP] + [NavInsID.SWIPE_CENTER_TO_LEFT] + [NavInsID.TAPPABLE_CENTER_TAP] * 2 + elif auxData: + if firmware.is_nano: + moves = [NavInsID.BOTH_CLICK] * 3 + [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] * 2 + else: + moves = [NavInsID.SWIPE_CENTER_TO_LEFT] + [NavInsID.TAPPABLE_CENTER_TAP] + [NavInsID.TAPPABLE_CENTER_TAP] + elif not txOutput.destination.stakingValue.startswith("m/"): + if firmware.is_nano: + moves = [NavInsID.BOTH_CLICK] * 2 + [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] + moves += [NavInsID.RIGHT_CLICK] + [NavInsID.BOTH_CLICK] * 2 + else: + moves = [NavInsID.TAPPABLE_CENTER_TAP] + [NavInsID.SWIPE_CENTER_TO_LEFT] + [NavInsID.TAPPABLE_CENTER_TAP] * 2 + + with client.sign_tx_outputs_basic(txOutput): + if len(moves) > 0: + navigator.navigate(moves) + else: + pass + # Check the status (Asynchronous) + response = client.get_async_response() + # Check the status (Asynchronous) + assert response and response.status == Errors.SW_SUCCESS + + # Send CONFIRM + response = client.sign_tx_outputs_confirm() + # Check the status + assert response.status == Errors.SW_SUCCESS + + # Send the FEE APDUs + # ------------------------------------------- + with client.sign_tx_fee(testCase): + moves = [NavInsID.BOTH_CLICK] if firmware.is_nano else [NavInsID.TAPPABLE_CENTER_TAP] + navigator.navigate(moves) + # Check the status (Asynchronous) + response = client.get_async_response() + assert response and response.status == Errors.SW_SUCCESS + + # Send the TTL APDUs + # ------------------------------------------- + if testCase.tx.ttl is not None: + response = client.sign_tx_ttl(testCase) + # Check the status + assert response.status == Errors.SW_SUCCESS + + # Send the CERTIFICATES APDUs + # ------------------------------------------- + for certificate in testCase.tx.certificates: + with client.sign_tx_certificate(certificate): + if firmware.is_nano: + navigator.navigate([NavInsID.BOTH_CLICK] * 3) + else: + scenario_navigator.address_review_approve(do_comparison=False) + # Check the status (Asynchronous) + response = client.get_async_response() + # Check the status (Asynchronous) + assert response and response.status == Errors.SW_SUCCESS + + # Send the WITHDRAWALS APDUs + # ------------------------------------------- + for withdrawal in testCase.tx.withdrawals: + with client.sign_tx_withdrawal(withdrawal): + pass + # Check the status (Asynchronous) + response = client.get_async_response() + # Check the status (Asynchronous) + assert response and response.status == Errors.SW_SUCCESS + + # Send the CONFIRM APDUs + # ------------------------------------------- + with client.sign_tx_confirm(): + if firmware.is_nano: + navigator.navigate([NavInsID.BOTH_CLICK]) + else: + scenario_navigator.review_approve(do_comparison=False) + # Check the status (Asynchronous) + response = client.get_async_response() + assert response and response.status == Errors.SW_SUCCESS + + # Send the WITNESS APDUs + # ------------------------------------------- + for idx, txInput in enumerate(testCase.tx.inputs): + with client.sign_tx_witness(txInput.path): + if auxData: + if txInput.path.startswith("m/185"): + # Shelley Address + pass + elif firmware.is_nano: + navigator.navigate([NavInsID.BOTH_CLICK] * 3) + else: + scenario_navigator.address_review_approve(do_comparison=False) + else: + pass + # Check the status (Asynchronous) + response = client.get_async_response() + # Check the status (Asynchronous) + assert response and response.status == Errors.SW_SUCCESS + # Check the response + assert response.data.hex() == testCase.expected.witnesses[idx].witnessSignatureHex