From cebe7e89c0bea0cac0f0bbe473418e1b8c7227f0 Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Thu, 2 Jul 2026 13:05:17 +0200 Subject: [PATCH 1/4] migrate to ruff --- .github/workflows/python_client_checks.yml | 3 +-- tests/application_client/setup.cfg | 14 +------------- tests/setup.cfg | 10 ---------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/workflows/python_client_checks.yml b/.github/workflows/python_client_checks.yml index 0608443..f873e9b 100644 --- a/.github/workflows/python_client_checks.yml +++ b/.github/workflows/python_client_checks.yml @@ -18,8 +18,7 @@ jobs: name: Call Ledger Python linters uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_python_checks.yml@v1 with: - run_linter: pylint run_type_check: true src_directory: application_client setup_directory: tests - req_directory: tests \ No newline at end of file + req_directory: tests diff --git a/tests/application_client/setup.cfg b/tests/application_client/setup.cfg index 81c396a..ac17565 100644 --- a/tests/application_client/setup.cfg +++ b/tests/application_client/setup.cfg @@ -1,18 +1,6 @@ [tool:pytest] addopts = --strict-markers -[pylint] -disable = C0114, # missing-module-docstring - C0115, # missing-class-docstring - C0116, # missing-function-docstring - C0103, # invalid-name - R0801, # duplicate-code - R0913, # too-many-arguments - R0917 # too-many-positional-arguments - -max-line-length=120 -extension-pkg-whitelist=hid - [pycodestyle] max-line-length = 100 @@ -23,4 +11,4 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-ledgered.*] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True diff --git a/tests/setup.cfg b/tests/setup.cfg index 7d0d7e3..b698102 100644 --- a/tests/setup.cfg +++ b/tests/setup.cfg @@ -1,16 +1,6 @@ [tool:pytest] addopts = --strict-markers -[pylint] -disable = C0114, # missing-module-docstring - C0115, # missing-class-docstring - C0116, # missing-function-docstring - C0103, # invalid-name - R0801, # duplicate-code - R0913 # too-many-arguments -max-line-length=100 -extension-pkg-whitelist=hid - [pycodestyle] max-line-length = 100 From faa196b4697a42e3827da8a2f6036cdd09ff900f Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Thu, 2 Jul 2026 13:05:30 +0200 Subject: [PATCH 2/4] fix ruff linting defects --- tests/conftest.py | 1 - tests/test_sign_cmd.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 909ec8b..a9ea72c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -from ragger.conftest import configuration ########################### ### CONFIGURATION START ### diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py index 2332dc0..4050365 100644 --- a/tests/test_sign_cmd.py +++ b/tests/test_sign_cmd.py @@ -8,7 +8,6 @@ from aptos_sdk.transactions import RawTransaction, TransactionPayload, TransactionArgument, EntryFunction from aptos_sdk.account import AccountAddress -from aptos_sdk.type_tag import StructTag, TypeTag from aptos_sdk.bcs import Serializer From db5ce2efda477bdf9042832e41fa4eb3ce530266 Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Thu, 2 Jul 2026 13:05:45 +0200 Subject: [PATCH 3/4] format with ruff --- .../aptos_command_sender.py | 134 ++--- .../aptos_response_unpacker.py | 15 +- tests/application_client/aptos_utils.py | 21 +- tests/conftest.py | 3 +- tests/test_app_mainmenu.py | 12 +- tests/test_error_cmd.py | 30 +- tests/test_name_version.py | 4 +- tests/test_pubkey_cmd.py | 70 ++- tests/test_sign_cmd.py | 534 +++++++++++------- tests/test_version_cmd.py | 1 + tests/utils.py | 7 +- 11 files changed, 496 insertions(+), 335 deletions(-) diff --git a/tests/application_client/aptos_command_sender.py b/tests/application_client/aptos_command_sender.py index 5bfd111..e256834 100644 --- a/tests/application_client/aptos_command_sender.py +++ b/tests/application_client/aptos_command_sender.py @@ -10,119 +10,119 @@ CLA: int = 0x5B + class P1(IntEnum): # Parameter 1 for first APDU number. P1_START = 0x00 # Parameter 1 for maximum APDU number. - P1_MAX = 0x03 + P1_MAX = 0x03 # Parameter 1 for screen confirmation for GET_PUBLIC_KEY. P1_CONFIRM = 0x01 + class P2(IntEnum): # Parameter 2 for last APDU to receive. P2_LAST = 0x00 # Parameter 2 for more APDU to receive. P2_MORE = 0x80 + class InsType(IntEnum): - GET_VERSION = 0x03 - GET_APP_NAME = 0x04 + GET_VERSION = 0x03 + GET_APP_NAME = 0x04 GET_PUBLIC_KEY = 0x05 - SIGN_TX = 0x06 + SIGN_TX = 0x06 + class Errors(IntEnum): - SW_DENY = 0x6985 - SW_WRONG_P1P2 = 0x6A86 - SW_WRONG_DATA_LENGTH = 0x6A87 - SW_INS_NOT_SUPPORTED = 0x6D00 - SW_CLA_NOT_SUPPORTED = 0x6E00 - SW_WRONG_RESPONSE_LENGTH = 0xB000 + SW_DENY = 0x6985 + SW_WRONG_P1P2 = 0x6A86 + SW_WRONG_DATA_LENGTH = 0x6A87 + SW_INS_NOT_SUPPORTED = 0x6D00 + SW_CLA_NOT_SUPPORTED = 0x6E00 + SW_WRONG_RESPONSE_LENGTH = 0xB000 SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 - SW_DISPLAY_ADDRESS_FAIL = 0xB002 - SW_DISPLAY_AMOUNT_FAIL = 0xB003 - SW_WRONG_TX_LENGTH = 0xB004 - SW_TX_PARSING_FAIL = 0xB005 - SW_GET_PUB_KEY_FAIL = 0xB006 - SW_BAD_STATE = 0xB007 - SW_SIGNATURE_FAIL = 0xB008 - SW_DISPLAY_GAS_FEE_FAIL = 0xB009 - SW_SWAP_CHECKING_FAIL = 0xB00A + SW_DISPLAY_ADDRESS_FAIL = 0xB002 + SW_DISPLAY_AMOUNT_FAIL = 0xB003 + SW_WRONG_TX_LENGTH = 0xB004 + SW_TX_PARSING_FAIL = 0xB005 + SW_GET_PUB_KEY_FAIL = 0xB006 + SW_BAD_STATE = 0xB007 + SW_SIGNATURE_FAIL = 0xB008 + SW_DISPLAY_GAS_FEE_FAIL = 0xB009 + SW_SWAP_CHECKING_FAIL = 0xB00A def split_message(message: bytes, max_size: int) -> List[bytes]: - return [message[x:x + max_size] for x in range(0, len(message), max_size)] + return [message[x : x + max_size] for x in range(0, len(message), max_size)] class AptosCommandSender: def __init__(self, backend: BackendInterface) -> None: self.backend = backend - def get_app_and_version(self) -> RAPDU: - return self.backend.exchange(cla=0xB0, # specific CLA for BOLOS - ins=0x01, # specific INS for get_app_and_version - p1=P1.P1_START, - p2=P2.P2_LAST, - data=b"") - + return self.backend.exchange( + cla=0xB0, # specific CLA for BOLOS + ins=0x01, # specific INS for get_app_and_version + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"", + ) def get_version(self) -> RAPDU: - return self.backend.exchange(cla=CLA, - ins=InsType.GET_VERSION, - p1=P1.P1_START, - p2=P2.P2_LAST, - data=b"") - + return self.backend.exchange( + cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_LAST, data=b"" + ) def get_app_name(self) -> RAPDU: - return self.backend.exchange(cla=CLA, - ins=InsType.GET_APP_NAME, - p1=P1.P1_START, - p2=P2.P2_LAST, - data=b"") - + return self.backend.exchange( + cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START, p2=P2.P2_LAST, data=b"" + ) def get_public_key(self, path: str) -> RAPDU: - return self.backend.exchange(cla=CLA, - ins=InsType.GET_PUBLIC_KEY, - p1=P1.P1_START, - p2=P2.P2_LAST, - data=pack_derivation_path(path)) - + return self.backend.exchange( + cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=pack_derivation_path(path), + ) @contextmanager - def get_public_key_with_confirmation(self, path: str) -> Generator[None, None, None]: - with self.backend.exchange_async(cla=CLA, - ins=InsType.GET_PUBLIC_KEY, - p1=P1.P1_CONFIRM, - p2=P2.P2_LAST, - data=pack_derivation_path(path)) as response: + def get_public_key_with_confirmation( + self, path: str + ) -> Generator[None, None, None]: + with self.backend.exchange_async( + cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=pack_derivation_path(path), + ) as response: yield response - @contextmanager def sign_tx(self, path: str, transaction: bytes) -> Generator[None, None, None]: - self.backend.exchange(cla=CLA, - ins=InsType.SIGN_TX, - p1=P1.P1_START, - p2=P2.P2_MORE, - data=pack_derivation_path(path)) + self.backend.exchange( + cla=CLA, + ins=InsType.SIGN_TX, + p1=P1.P1_START, + p2=P2.P2_MORE, + data=pack_derivation_path(path), + ) messages = split_message(transaction, MAX_APDU_LEN) idx: int = P1.P1_START + 1 for msg in messages[:-1]: - self.backend.exchange(cla=CLA, - ins=InsType.SIGN_TX, - p1=idx, - p2=P2.P2_MORE, - data=msg) + self.backend.exchange( + cla=CLA, ins=InsType.SIGN_TX, p1=idx, p2=P2.P2_MORE, data=msg + ) idx += 1 - with self.backend.exchange_async(cla=CLA, - ins=InsType.SIGN_TX, - p1=idx, - p2=P2.P2_LAST, - data=messages[-1]) as response: + with self.backend.exchange_async( + cla=CLA, ins=InsType.SIGN_TX, p1=idx, p2=P2.P2_LAST, data=messages[-1] + ) as response: yield response def get_async_response(self) -> Optional[RAPDU]: diff --git a/tests/application_client/aptos_response_unpacker.py b/tests/application_client/aptos_response_unpacker.py index cc8bc2d..c96c192 100644 --- a/tests/application_client/aptos_response_unpacker.py +++ b/tests/application_client/aptos_response_unpacker.py @@ -1,20 +1,24 @@ from typing import Tuple from struct import unpack + # remainder, data_len, data -def pop_sized_buf_from_buffer(buffer:bytes, size:int) -> Tuple[bytes, bytes]: +def pop_sized_buf_from_buffer(buffer: bytes, size: int) -> Tuple[bytes, bytes]: return buffer[size:], buffer[0:size] + # remainder, data_len, data -def pop_size_prefixed_buf_from_buf(buffer:bytes) -> Tuple[bytes, int, bytes]: +def pop_size_prefixed_buf_from_buf(buffer: bytes) -> Tuple[bytes, int, bytes]: data_len = buffer[0] - return buffer[1+data_len:], data_len, buffer[1:data_len+1] + return buffer[1 + data_len :], data_len, buffer[1 : data_len + 1] + # Unpack from response: # response = app_name (var) def unpack_get_app_name_response(response: bytes) -> str: return response.decode("ascii") + # Unpack from response: # response = MAJOR (1) # MINOR (1) @@ -24,6 +28,7 @@ def unpack_get_version_response(response: bytes) -> Tuple[int, int, int]: major, minor, patch = unpack("BBB", response) return (major, minor, patch) + # Unpack from response: # response = format_id (1) # app_name_raw_len (1) @@ -42,6 +47,7 @@ def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: return app_name_raw.decode("ascii"), version_raw.decode("ascii") + # Unpack from response: # response = pub_key_len (1) # pub_key (var) @@ -57,6 +63,7 @@ def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, by return pub_key_len, pub_key, chain_code_len, chain_code + # Unpack from response: # response = der_sig_len (1) # der_sig (var) @@ -67,4 +74,4 @@ def unpack_sign_tx_response(response: bytes) -> Tuple[int, bytes, int]: assert len(response) == 0 - return sig_len, sig, int.from_bytes(v, byteorder='big') + return sig_len, sig, int.from_bytes(v, byteorder="big") diff --git a/tests/application_client/aptos_utils.py b/tests/application_client/aptos_utils.py index fd96e62..5701f81 100644 --- a/tests/application_client/aptos_utils.py +++ b/tests/application_client/aptos_utils.py @@ -2,9 +2,9 @@ from typing import Optional, Literal -UINT64_MAX: int = 2**64-1 -UINT32_MAX: int = 2**32-1 -UINT16_MAX: int = 2**16-1 +UINT64_MAX: int = 2**64 - 1 +UINT32_MAX: int = 2**32 - 1 +UINT16_MAX: int = 2**16 - 1 def write_varint(n: int) -> bytes: @@ -12,19 +12,18 @@ def write_varint(n: int) -> bytes: return n.to_bytes(1, byteorder="little") if n <= UINT16_MAX: - return b"\xFD" + n.to_bytes(2, byteorder="little") + return b"\xfd" + n.to_bytes(2, byteorder="little") if n <= UINT32_MAX: - return b"\xFE" + n.to_bytes(4, byteorder="little") + return b"\xfe" + n.to_bytes(4, byteorder="little") if n <= UINT64_MAX: - return b"\xFF" + n.to_bytes(8, byteorder="little") + return b"\xff" + n.to_bytes(8, byteorder="little") raise ValueError(f"Can't write to varint: '{n}'!") -def read_varint(buf: BytesIO, - prefix: Optional[bytes] = None) -> int: +def read_varint(buf: BytesIO, prefix: Optional[bytes] = None) -> int: b: bytes = prefix if prefix else buf.read(1) if not b: @@ -49,9 +48,9 @@ def read(buf: BytesIO, size: int) -> bytes: return b -def read_uint(buf: BytesIO, - bit_len: int, - byteorder: Literal['big', 'little'] = 'little') -> int: +def read_uint( + buf: BytesIO, bit_len: int, byteorder: Literal["big", "little"] = "little" +) -> int: size: int = bit_len // 8 b: bytes = buf.read(size) diff --git a/tests/conftest.py b/tests/conftest.py index a9ea72c..e0641cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ - ########################### ### CONFIGURATION START ### ########################### @@ -11,4 +10,4 @@ ######################### # Pull all features from the base ragger conftest using the overridden configuration -pytest_plugins = ("ragger.conftest.base_conftest", ) +pytest_plugins = ("ragger.conftest.base_conftest",) diff --git a/tests/test_app_mainmenu.py b/tests/test_app_mainmenu.py index 0d3a467..3089c20 100644 --- a/tests/test_app_mainmenu.py +++ b/tests/test_app_mainmenu.py @@ -10,7 +10,7 @@ def test_app_mainmenu(firmware, navigator, test_name): instructions = [ NavInsID.RIGHT_CLICK, NavInsID.RIGHT_CLICK, - NavInsID.RIGHT_CLICK + NavInsID.RIGHT_CLICK, ] else: num_info_pages = 2 if firmware.device == "flex" else 1 @@ -21,7 +21,11 @@ def test_app_mainmenu(firmware, navigator, test_name): NavIns(NavInsID.TOUCH, (200, 113)), *([NavInsID.USE_CASE_SETTINGS_NEXT] * num_info_pages), NavInsID.USE_CASE_SETTINGS_PREVIOUS, - NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT, ] - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, instructions, - screen_change_before_first_instruction=False) + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, + test_name, + instructions, + screen_change_before_first_instruction=False, + ) diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py index 5ad45a5..71d65b4 100644 --- a/tests/test_error_cmd.py +++ b/tests/test_error_cmd.py @@ -14,23 +14,31 @@ def test_bad_cla(backend): # Ensure the app returns an error when a bad INS is used def test_bad_ins(backend): with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, ins=0xff) + backend.exchange(cla=CLA, ins=0xFF) assert e.value.status == Errors.SW_INS_NOT_SUPPORTED # Ensure the app returns an error when a bad P1 or P2 is used def test_wrong_p1p2(backend): with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START + 1, p2=P2.P2_LAST) + backend.exchange( + cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START + 1, p2=P2.P2_LAST + ) assert e.value.status == Errors.SW_WRONG_P1P2 with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_MORE) + backend.exchange( + cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_MORE + ) assert e.value.status == Errors.SW_WRONG_P1P2 with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START + 1, p2=P2.P2_LAST) + backend.exchange( + cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START + 1, p2=P2.P2_LAST + ) assert e.value.status == Errors.SW_WRONG_P1P2 with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START, p2=P2.P2_MORE) + backend.exchange( + cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START, p2=P2.P2_MORE + ) assert e.value.status == Errors.SW_WRONG_P1P2 @@ -49,9 +57,11 @@ def test_wrong_data_length(backend): # Ensure there is no state confusion when trying wrong APDU sequences def test_invalid_state(backend): with pytest.raises(ExceptionRAPDU) as e: - backend.exchange(cla=CLA, - ins=InsType.SIGN_TX, - p1=P1.P1_START + 1, # Try to continue a flow instead of start a new one - p2=P2.P2_MORE, - data=b"abcde") # data is not parsed in this case + backend.exchange( + cla=CLA, + ins=InsType.SIGN_TX, + p1=P1.P1_START + 1, # Try to continue a flow instead of start a new one + p2=P2.P2_MORE, + data=b"abcde", + ) # data is not parsed in this case assert e.value.status == Errors.SW_BAD_STATE diff --git a/tests/test_name_version.py b/tests/test_name_version.py index 8fcc321..8a941e1 100644 --- a/tests/test_name_version.py +++ b/tests/test_name_version.py @@ -1,5 +1,7 @@ from application_client.aptos_command_sender import AptosCommandSender -from application_client.aptos_response_unpacker import unpack_get_app_and_version_response +from application_client.aptos_response_unpacker import ( + unpack_get_app_and_version_response, +) from utils import verify_version diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py index 02cd147..c692da5 100644 --- a/tests/test_pubkey_cmd.py +++ b/tests/test_pubkey_cmd.py @@ -10,14 +10,23 @@ # In this test we check that the GET_PUBLIC_KEY works in non-confirmation mode def test_get_public_key_no_confirm(backend): - for path in ["m/44'/637'/1'/0'/0'", "m/44'/637'/0'/0'/1'", "m/44'/637'/255'/255'/255'", "m/44'/637'/2147483647'/0'/0'/0'/0'/0'/0'/0'"]: + for path in [ + "m/44'/637'/1'/0'/0'", + "m/44'/637'/0'/0'/1'", + "m/44'/637'/255'/255'/255'", + "m/44'/637'/2147483647'/0'/0'/0'/0'/0'/0'/0'", + ]: client = AptosCommandSender(backend) response = client.get_public_key(path=path).data _, public_key, _, chain_code = unpack_get_public_key_response(response) - ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Ed25519Slip, path=path) + ref_public_key, ref_chain_code = calculate_public_key_and_chaincode( + CurveChoice.Ed25519Slip, path=path + ) ref_public_key_bytes = bytearray.fromhex(ref_public_key) - ref_public_key_bytes[0] = 0x04 # Set the first byte to 0x04 to indicate that it is uncompressed + ref_public_key_bytes[0] = ( + 0x04 # Set the first byte to 0x04 to indicate that it is uncompressed + ) assert public_key.hex() == ref_public_key_bytes.hex() assert chain_code.hex() == ref_chain_code @@ -29,11 +38,13 @@ def test_get_public_key_confirm_accepted(firmware, backend, navigator, test_name path = "m/44'/637'/1'/0'/0'" with client.get_public_key_with_confirmation(path=path): if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: if firmware.device == "flex": qr_coordinates = (80, 430) @@ -46,17 +57,21 @@ def test_get_public_key_confirm_accepted(firmware, backend, navigator, test_name NavIns(NavInsID.TOUCH, qr_coordinates), NavInsID.USE_CASE_ADDRESS_CONFIRMATION_EXIT_QR, NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS + NavInsID.USE_CASE_STATUS_DISMISS, ] - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, - test_name, - instructions) + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, instructions + ) response = client.get_async_response().data _, public_key, _, chain_code = unpack_get_public_key_response(response) - ref_public_key, ref_chain_code = calculate_public_key_and_chaincode(CurveChoice.Ed25519Slip, path=path) + ref_public_key, ref_chain_code = calculate_public_key_and_chaincode( + CurveChoice.Ed25519Slip, path=path + ) ref_public_key_bytes = bytearray.fromhex(ref_public_key) - ref_public_key_bytes[0] = 0x04 # Set the first byte to 0x04 to indicate that it is uncompressed + ref_public_key_bytes[0] = ( + 0x04 # Set the first byte to 0x04 to indicate that it is uncompressed + ) print(f"MUTABLE DATA: {ref_public_key_bytes.hex()}") print(f"Public key: {public_key.hex()}") @@ -75,32 +90,31 @@ def test_get_public_key_confirm_refused(firmware, backend, navigator, test_name) if firmware.device.startswith("nano"): with pytest.raises(ExceptionRAPDU) as e: with client.get_public_key_with_confirmation(path=path): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Reject", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name, + ) # Assert that we have received a refusal assert e.value.status == Errors.SW_DENY assert len(e.value.data) == 0 else: instructions_set = [ - [ - NavInsID.USE_CASE_REVIEW_REJECT, - NavInsID.USE_CASE_STATUS_DISMISS - ], + [NavInsID.USE_CASE_REVIEW_REJECT, NavInsID.USE_CASE_STATUS_DISMISS], [ NavInsID.USE_CASE_REVIEW_NEXT, NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CANCEL, - NavInsID.USE_CASE_STATUS_DISMISS - ] + NavInsID.USE_CASE_STATUS_DISMISS, + ], ] for i, instructions in enumerate(instructions_set): with pytest.raises(ExceptionRAPDU) as e: with client.get_public_key_with_confirmation(path=path): - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, - test_name + f"/part{i}", - instructions) + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name + f"/part{i}", instructions + ) # Assert that we have received a refusal assert e.value.status == Errors.SW_DENY assert len(e.value.data) == 0 diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py index 4050365..f626d25 100644 --- a/tests/test_sign_cmd.py +++ b/tests/test_sign_cmd.py @@ -1,18 +1,27 @@ import pytest from application_client.aptos_command_sender import AptosCommandSender, Errors -from application_client.aptos_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response +from application_client.aptos_response_unpacker import ( + unpack_get_public_key_response, + unpack_sign_tx_response, +) from ragger.error import ExceptionRAPDU from ragger.navigator import NavInsID, NavIns from utils import ROOT_SCREENSHOT_PATH, check_signature_validity -from aptos_sdk.transactions import RawTransaction, TransactionPayload, TransactionArgument, EntryFunction +from aptos_sdk.transactions import ( + RawTransaction, + TransactionPayload, + TransactionArgument, + EntryFunction, +) from aptos_sdk.account import AccountAddress from aptos_sdk.bcs import Serializer # In this tests we check the behavior of the device when asked to sign a transaction + # This fixture is used to disable the blind signing after a test that enabled it @pytest.fixture def disable_blind_signing(firmware, backend, navigator): @@ -31,7 +40,7 @@ def disable_blind_signing(firmware, backend, navigator): instructions = [ NavInsID.USE_CASE_HOME_SETTINGS, NavIns(NavInsID.TOUCH, (200, 113)), - NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT, ] navigator.navigate(instructions, screen_change_before_first_instruction=False) @@ -50,7 +59,9 @@ def test_sign_tx_short_tx(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee000000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde082a00000000000000204e0000000000006400000000000000565c51630000000022") + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee000000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde082a00000000000000204e0000000000006400000000000000565c51630000000022" + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -58,18 +69,21 @@ def test_sign_tx_short_tx(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data @@ -80,7 +94,9 @@ def test_sign_tx_short_tx(firmware, backend, navigator, test_name): # In this test we send to the device a transaction to sign and validate it on screen # The transaction will be sent in multiple chunks # Also, this transaction has a request for blind signing activation -def test_blind_sign_tx_long_tx(firmware, backend, navigator, test_name, disable_blind_signing): +def test_blind_sign_tx_long_tx( + firmware, backend, navigator, test_name, disable_blind_signing +): # Use the app interface instead of raw interface client = AptosCommandSender(backend) path: str = "m/44'/637'/1'/0'/0'" @@ -88,38 +104,52 @@ def test_blind_sign_tx_long_tx(firmware, backend, navigator, test_name, disable_ rapdu = client.get_public_key(path=path) _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b0000000000000002190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e120a736372697074735f76320473776170030700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e7304555344540007190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e12066375727665730c556e636f7272656c6174656400020800e1f5050000000008decbb30000000000480000000000000064000000000000008a9ba4640000000002") + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b0000000000000002190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e120a736372697074735f76320473776170030700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e7304555344540007190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e12066375727665730c556e636f7272656c6174656400020800e1f5050000000008decbb30000000000480000000000000064000000000000008a9ba4640000000002" + ) with client.sign_tx(path=path, transaction=transaction): if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Allow", - ROOT_SCREENSHOT_PATH, - test_name + "/part0", - screen_change_after_last_instruction=False) - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name + "/part1") + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Allow", + ROOT_SCREENSHOT_PATH, + test_name + "/part0", + screen_change_after_last_instruction=False, + ) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name + "/part1", + ) else: - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, - test_name + "/part0", - [NavInsID.USE_CASE_CHOICE_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS, - NavInsID.USE_CASE_CHOICE_REJECT, - NavInsID.INFO_HEADER_TAP, - NavInsID.NAVIGATION_HEADER_TAP], - screen_change_after_last_instruction=False) - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.INFO_HEADER_TAP, - NavInsID.NAVIGATION_HEADER_TAP, - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name + "/part1") + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, + test_name + "/part0", + [ + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS, + NavInsID.USE_CASE_CHOICE_REJECT, + NavInsID.INFO_HEADER_TAP, + NavInsID.NAVIGATION_HEADER_TAP, + ], + screen_change_after_last_instruction=False, + ) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [ + NavInsID.INFO_HEADER_TAP, + NavInsID.NAVIGATION_HEADER_TAP, + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS, + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name + "/part1", + ) response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) @@ -132,16 +162,20 @@ def test_sign_tx_refused(firmware, backend, navigator, test_name): client = AptosCommandSender(backend) path: str = "m/44'/637'/1'/0'/0'" - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e736665720002203835075df1bf469c336eabed8ac87052ee4485f3ec93380a5382fbf76b7a33070840420f000000000006000000000000006400000000000000c39aa4640000000002") + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e736665720002203835075df1bf469c336eabed8ac87052ee4485f3ec93380a5382fbf76b7a33070840420f000000000006000000000000006400000000000000c39aa4640000000002" + ) if firmware.device.startswith("nano"): with pytest.raises(ExceptionRAPDU) as e: with client.sign_tx(path=path, transaction=transaction): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Reject", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name, + ) # Assert that we have received a refusal assert e.value.status == Errors.SW_DENY @@ -149,18 +183,21 @@ def test_sign_tx_refused(firmware, backend, navigator, test_name): else: for i in range(4): instructions = [NavInsID.USE_CASE_VIEW_DETAILS_NEXT] * i - instructions += [NavInsID.USE_CASE_REVIEW_REJECT, - NavInsID.USE_CASE_CHOICE_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS] + instructions += [ + NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS, + ] with pytest.raises(ExceptionRAPDU) as e: with client.sign_tx(path=path, transaction=transaction): - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, - test_name + f"/part{i}", - instructions) + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name + f"/part{i}", instructions + ) # Assert that we have received a refusal assert e.value.status == Errors.SW_DENY assert len(e.value.data) == 0 + # In this test we send to the device a message to sign and validate it on screen # We will ensure that the displayed information is correct by using screenshots comparison def test_sign_tx_short_msg(firmware, backend, navigator, test_name): @@ -174,7 +211,7 @@ def test_sign_tx_short_msg(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the mes that will be sent to the device for signing - message = bytes("Hello Ledger!", 'utf-8') + message = bytes("Hello Ledger!", "utf-8") # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -182,24 +219,28 @@ def test_sign_tx_short_msg(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=message): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, message) + # In this test we send to the device a message to sign and validate it on screen # We will ensure that the displayed information is correct by using screenshots comparison def test_sign_short_raw_msg(firmware, backend, navigator, test_name): @@ -221,27 +262,33 @@ def test_sign_short_raw_msg(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=message): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, message) + # In this test we send to the device a message to sign and validate it on screen # We will ensure that the displayed information is correct by using screenshots comparison -def test_sign_long_raw_msg(firmware, backend, navigator, test_name, disable_blind_signing): +def test_sign_long_raw_msg( + firmware, backend, navigator, test_name, disable_blind_signing +): # Use the app interface instead of raw interface client = AptosCommandSender(backend) # The path used for this entire test @@ -252,44 +299,59 @@ def test_sign_long_raw_msg(firmware, backend, navigator, test_name, disable_blin _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the mes that will be sent to the device for signing - message = bytes.fromhex("bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b0000000000000002190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e120a736372697074735f76320473776170030700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e7304555344540007190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e12066375727665730c556e636f7272656c6174656400020800e1f5050000000008decbb30000000000480000000000000064000000000000008a9ba4640000000002") + message = bytes.fromhex( + "bc6f6693bddc1a9fec9e674a461eaa00b193094c6fc0d3b382a599c37e1aaa7618eff2c96a3586876082c4594c50c50d7dde1b0000000000000002190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e120a736372697074735f76320473776170030700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e7304555344540007190d44266241744264b964a37b8f09863167a12d3e70cda39376cfb4e3561e12066375727665730c556e636f7272656c6174656400020800e1f5050000000008decbb30000000000480000000000000064000000000000008a9ba4640000000002" + ) with client.sign_tx(path=path, transaction=message): if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Allow", - ROOT_SCREENSHOT_PATH, - test_name + "/part0", - screen_change_after_last_instruction=False) - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name + "/part1") + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Allow", + ROOT_SCREENSHOT_PATH, + test_name + "/part0", + screen_change_after_last_instruction=False, + ) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name + "/part1", + ) else: - navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, - test_name + "/part0", - [NavInsID.USE_CASE_CHOICE_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS, - NavInsID.USE_CASE_CHOICE_REJECT, - NavInsID.INFO_HEADER_TAP, - NavInsID.NAVIGATION_HEADER_TAP], - screen_change_after_last_instruction=False) - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.INFO_HEADER_TAP, - NavInsID.NAVIGATION_HEADER_TAP, - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name + "/part1") + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, + test_name + "/part0", + [ + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS, + NavInsID.USE_CASE_CHOICE_REJECT, + NavInsID.INFO_HEADER_TAP, + NavInsID.NAVIGATION_HEADER_TAP, + ], + screen_change_after_last_instruction=False, + ) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [ + NavInsID.INFO_HEADER_TAP, + NavInsID.NAVIGATION_HEADER_TAP, + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS, + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name + "/part1", + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, message) + # In this test we send to the device a transaction to sign and validate it on screen # The transaction is Legacy Tokens to be Clear Signed and is listed in the app def test_sign_listed_legacy_tokens(firmware, backend, navigator, test_name): @@ -303,7 +365,9 @@ def test_sign_listed_legacy_tokens(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1934e5e65d5c7a3191e4310ecd210e8f0ff53823189123b47086d928bd574a573d114000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107d11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b50b7374616b65645f636f696e0b5374616b65644170746f73000220a0d8abc262e3321f87d745bd5d687e8f3fb14c87d48f840b6b56867df0026ec808a0936c02000000000b0000000000000064000000000000005459d0") + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1934e5e65d5c7a3191e4310ecd210e8f0ff53823189123b47086d928bd574a573d114000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107d11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b50b7374616b65645f636f696e0b5374616b65644170746f73000220a0d8abc262e3321f87d745bd5d687e8f3fb14c87d48f840b6b56867df0026ec808a0936c02000000000b0000000000000064000000000000005459d0" + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -311,24 +375,28 @@ def test_sign_listed_legacy_tokens(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) + # In this test we send to the device a transaction to sign and validate it on screen # The transaction is Legacy Tokens to be Clear Signed and is whitelisted in the app def test_sign_unlisted_legacy_tokens(firmware, backend, navigator, test_name): @@ -342,7 +410,9 @@ def test_sign_unlisted_legacy_tokens(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1934e5e65d5c7a3191e4310ecd210e8f0ff53823189123b47086d928bd574a573d114000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107804cef4821e11c55e87f2e9ec7dfc0d31d297cd34d20bfb2ae166e5069b40fe20b6c65646765725f636f696e0b4c65646765724170746f73000220a0d8abc262e3321f87d745bd5d687e8f3fb14c87d48f840b6b56867df0026ec808a0936c02000000000b0000000000000064000000000000005459d0") + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1934e5e65d5c7a3191e4310ecd210e8f0ff53823189123b47086d928bd574a573d114000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107804cef4821e11c55e87f2e9ec7dfc0d31d297cd34d20bfb2ae166e5069b40fe20b6c65646765725f636f696e0b4c65646765724170746f73000220a0d8abc262e3321f87d745bd5d687e8f3fb14c87d48f840b6b56867df0026ec808a0936c02000000000b0000000000000064000000000000005459d0" + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -350,24 +420,28 @@ def test_sign_unlisted_legacy_tokens(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) + # # In this test we send to the device a transaction to sign and validate it on screen # # The transaction is Fungible Asset Tokens and should be Clear Signed def test_sign_fa_tx(firmware, backend, navigator, test_name): @@ -381,26 +455,31 @@ def test_sign_fa_tx(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1938f13f355f3af444bd356adeaaaf01235a7817d6a4417f5c9fa3d74a68f7b7afd0000000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b207be51d04d3a482fa056bc094bc5eadad005aaf823a95269410f08730f0d03cb40840420f000000000009000000000000006400000000000000000000000000000001") - + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b1938f13f355f3af444bd356adeaaaf01235a7817d6a4417f5c9fa3d74a68f7b7afd0000000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b207be51d04d3a482fa056bc094bc5eadad005aaf823a95269410f08730f0d03cb40840420f000000000009000000000000006400000000000000000000000000000001" + ) + # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. # It will yield the result when the navigation is done with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data @@ -421,8 +500,12 @@ def test_sign_staking_aptos(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - sender = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") - pool = AccountAddress.from_str("0xA651C7C52D64A2014379902BBC92439D196499BCC36D94FF0395AA45837C66DB") + sender = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) + pool = AccountAddress.from_str( + "0xA651C7C52D64A2014379902BBC92439D196499BCC36D94FF0395AA45837C66DB" + ) max_gas_amount = 100 fees = 39 * max_gas_amount @@ -440,7 +523,7 @@ def test_sign_staking_aptos(firmware, backend, navigator, test_name): ) # Create the raw transaction (TX_RAW) - gas_unit_price = int(fees/max_gas_amount) + gas_unit_price = int(fees / max_gas_amount) txn = RawTransaction( sender=sender, sequence_number=sequence_number, @@ -455,7 +538,10 @@ def test_sign_staking_aptos(firmware, backend, navigator, test_name): txn.serialize(serializer) transaction = serializer.output() # This is a salt required by the Nano App to make sure that the payload comes from Ledger Live host - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + transaction.hex()) + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + + transaction.hex() + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -463,24 +549,28 @@ def test_sign_staking_aptos(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) + # # In this test we send to the device a transaction to sign and validate it on screen # # The transaction is a Unlock stake transaction and should be Clear Signed def test_sign_unlocking_aptos(firmware, backend, navigator, test_name): @@ -494,8 +584,12 @@ def test_sign_unlocking_aptos(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - sender = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") - pool = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") + sender = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) + pool = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) max_gas_amount = 100 fees = 6 * max_gas_amount @@ -513,7 +607,7 @@ def test_sign_unlocking_aptos(firmware, backend, navigator, test_name): ) # Create the raw transaction (TX_RAW) - gas_unit_price = int(fees/max_gas_amount) + gas_unit_price = int(fees / max_gas_amount) txn = RawTransaction( sender=sender, sequence_number=sequence_number, @@ -528,7 +622,10 @@ def test_sign_unlocking_aptos(firmware, backend, navigator, test_name): txn.serialize(serializer) transaction = serializer.output() # This is a salt required by the Nano App to make sure that the payload comes from Ledger Live host - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + transaction.hex()) + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + + transaction.hex() + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -536,24 +633,28 @@ def test_sign_unlocking_aptos(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) + # # In this test we send to the device a transaction to sign and validate it on screen # # The transaction is a reactivate stake transaction and should be Clear Signed def test_sign_reactivate_aptos(firmware, backend, navigator, test_name): @@ -567,8 +668,12 @@ def test_sign_reactivate_aptos(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - sender = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") - pool = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") + sender = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) + pool = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) max_gas_amount = 100 fees = 6 * max_gas_amount @@ -586,7 +691,7 @@ def test_sign_reactivate_aptos(firmware, backend, navigator, test_name): ) # Create the raw transaction (TX_RAW) - gas_unit_price = int(fees/max_gas_amount) + gas_unit_price = int(fees / max_gas_amount) txn = RawTransaction( sender=sender, sequence_number=sequence_number, @@ -601,7 +706,10 @@ def test_sign_reactivate_aptos(firmware, backend, navigator, test_name): txn.serialize(serializer) transaction = serializer.output() # This is a salt required by the Nano App to make sure that the payload comes from Ledger Live host - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + transaction.hex()) + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + + transaction.hex() + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -609,24 +717,28 @@ def test_sign_reactivate_aptos(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) assert check_signature_validity(public_key, sig, transaction) + # # In this test we send to the device a transaction to sign and validate it on screen # # The transaction is a reactivate stake transaction and should be Clear Signed def test_sign_withdraw_aptos(firmware, backend, navigator, test_name): @@ -640,8 +752,12 @@ def test_sign_withdraw_aptos(firmware, backend, navigator, test_name): _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) # Create the transaction that will be sent to the device for signing - sender = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") - pool = AccountAddress.from_str("0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD") + sender = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) + pool = AccountAddress.from_str( + "0x8F13f355F3aF444BD356ADEAAAF01235A7817D6A4417F5c9FA3D74A68F7b7AFD" + ) max_gas_amount = 100 fees = 6 * max_gas_amount @@ -659,7 +775,7 @@ def test_sign_withdraw_aptos(firmware, backend, navigator, test_name): ) # Create the raw transaction (TX_RAW) - gas_unit_price = int(fees/max_gas_amount) + gas_unit_price = int(fees / max_gas_amount) txn = RawTransaction( sender=sender, sequence_number=sequence_number, @@ -674,7 +790,10 @@ def test_sign_withdraw_aptos(firmware, backend, navigator, test_name): txn.serialize(serializer) transaction = serializer.output() # This is a salt required by the Nano App to make sure that the payload comes from Ledger Live host - transaction = bytes.fromhex("b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + transaction.hex()) + transaction = bytes.fromhex( + "b5e97db07fa0bd0e5598aa3643a9bc6f6693bddc1a9fec9e674a461eaa00b193" + + transaction.hex() + ) # Send the sign device instruction. # As it requires on-screen validation, the function is asynchronous. @@ -682,20 +801,23 @@ def test_sign_withdraw_aptos(firmware, backend, navigator, test_name): with client.sign_tx(path=path, transaction=transaction): # Validate the on-screen request by performing the navigation appropriate for this device if firmware.device.startswith("nano"): - navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, - [NavInsID.BOTH_CLICK], - "Approve", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) else: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_VIEW_DETAILS_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, - NavInsID.USE_CASE_STATUS_DISMISS], - "Hold to sign", - ROOT_SCREENSHOT_PATH, - test_name) + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_VIEW_DETAILS_NEXT, + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name, + ) # The device as yielded the result, parse it and ensure that the signature is correct response = client.get_async_response().data _, sig, _ = unpack_sign_tx_response(response) - assert check_signature_validity(public_key, sig, transaction) \ No newline at end of file + assert check_signature_validity(public_key, sig, transaction) diff --git a/tests/test_version_cmd.py b/tests/test_version_cmd.py index 5b8ccd6..bf74e51 100644 --- a/tests/test_version_cmd.py +++ b/tests/test_version_cmd.py @@ -2,6 +2,7 @@ from application_client.aptos_response_unpacker import unpack_get_version_response from utils import verify_version + # In this test we check the behavior of the device when asked to provide the app version def test_version(backend): # Use the app interface instead of raw interface diff --git a/tests/utils.py b/tests/utils.py index e5b4d2b..ae86afd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,11 +9,14 @@ # Check if a signature of a given message is valid -def check_signature_validity(public_key: bytes, signature: bytes, message: bytes) -> bool: +def check_signature_validity( + public_key: bytes, signature: bytes, message: bytes +) -> bool: pk = VerifyKey(public_key[1:]) return pk.verify(signature=signature, smessage=message) + def verify_version(version: str) -> None: """Verify the app version, based on defines in Makefile Args: @@ -37,7 +40,7 @@ def verify_version(version: str) -> None: def _read_makefile() -> List[str]: - """Read lines from the parent Makefile """ + """Read lines from the parent Makefile""" parent = Path(__file__).parent.parent.resolve() makefile = f"{parent}/Makefile" From e08c6c2317ec874e424c5734bf8de704344e5a4a Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Fri, 3 Jul 2026 11:43:04 +0200 Subject: [PATCH 4/4] add .git-blame-ignore-revs --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..1457b9e --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +db5ce2efda477bdf9042832e41fa4eb3ce530266