diff --git a/client/src/ledger_app_clients/ethereum/client.py b/client/src/ledger_app_clients/ethereum/client.py index 9b6a4de89e..dbc56ab0f5 100644 --- a/client/src/ledger_app_clients/ethereum/client.py +++ b/client/src/ledger_app_clients/ethereum/client.py @@ -12,6 +12,7 @@ from .keychain import sign_data, Key from .tlv import format_tlv +from hashlib import sha256 from web3 import Web3 @@ -43,21 +44,25 @@ class TrustedNameSource(IntEnum): DNS = 0x05 -class TrustedNameTag(IntEnum): +class FieldTag(IntEnum): STRUCT_TYPE = 0x01 STRUCT_VERSION = 0x02 NOT_VALID_AFTER = 0x10 CHALLENGE = 0x12 SIGNER_KEY_ID = 0x13 SIGNER_ALGO = 0x14 - SIGNATURE = 0x15 - NAME = 0x20 + DER_SIGNATURE = 0x15 + TRUSTED_NAME = 0x20 COIN_TYPE = 0x21 ADDRESS = 0x22 CHAIN_ID = 0x23 - NAME_TYPE = 0x70 - NAME_SOURCE = 0x71 - NFT_ID = 0x72 + TICKER = 0x24 + BLOCKCHAIN_FAMILY = 0x51 + NETWORK_NAME = 0x52 + NETWORK_ICON_HASH = 0x53 + TRUSTED_NAME_TYPE = 0x70 + TRUSTED_NAME_SOURCE = 0x71 + TRUSTED_NAME_NFT_ID = 0x72 class PKIPubKeyUsage(IntEnum): @@ -266,10 +271,10 @@ def _provide_trusted_name_common(self, payload: bytes) -> RAPDU: # pylint: enable=line-too-long self._pki_client.send_certificate(PKIPubKeyUsage.PUBKEY_USAGE_COIN_META, bytes.fromhex(cert_apdu)) - payload += format_tlv(TrustedNameTag.STRUCT_TYPE, 3) # TrustedName - payload += format_tlv(TrustedNameTag.SIGNER_KEY_ID, 0) # test key - payload += format_tlv(TrustedNameTag.SIGNER_ALGO, 1) # secp256k1 - payload += format_tlv(TrustedNameTag.SIGNATURE, + payload += format_tlv(FieldTag.STRUCT_TYPE, 3) # TrustedName + payload += format_tlv(FieldTag.SIGNER_KEY_ID, 0) # test key + payload += format_tlv(FieldTag.SIGNER_ALGO, 1) # secp256k1 + payload += format_tlv(FieldTag.DER_SIGNATURE, sign_data(Key.TRUSTED_NAME, payload)) chunks = self._cmd_builder.provide_trusted_name(payload) for chunk in chunks[:-1]: @@ -277,11 +282,11 @@ def _provide_trusted_name_common(self, payload: bytes) -> RAPDU: return self._exchange(chunks[-1]) def provide_trusted_name_v1(self, addr: bytes, name: str, challenge: int) -> RAPDU: - payload = format_tlv(TrustedNameTag.STRUCT_VERSION, 1) - payload += format_tlv(TrustedNameTag.CHALLENGE, challenge) - payload += format_tlv(TrustedNameTag.COIN_TYPE, 0x3c) # ETH in slip-44 - payload += format_tlv(TrustedNameTag.NAME, name) - payload += format_tlv(TrustedNameTag.ADDRESS, addr) + payload = format_tlv(FieldTag.STRUCT_VERSION, 1) + payload += format_tlv(FieldTag.CHALLENGE, challenge) + payload += format_tlv(FieldTag.COIN_TYPE, 0x3c) # ETH in slip-44 + payload += format_tlv(FieldTag.TRUSTED_NAME, name) + payload += format_tlv(FieldTag.ADDRESS, addr) return self._provide_trusted_name_common(payload) def provide_trusted_name_v2(self, @@ -293,19 +298,19 @@ def provide_trusted_name_v2(self, nft_id: Optional[int] = None, challenge: Optional[int] = None, not_valid_after: Optional[tuple[int]] = None) -> RAPDU: - payload = format_tlv(TrustedNameTag.STRUCT_VERSION, 2) - payload += format_tlv(TrustedNameTag.NAME, name) - payload += format_tlv(TrustedNameTag.ADDRESS, addr) - payload += format_tlv(TrustedNameTag.NAME_TYPE, name_type) - payload += format_tlv(TrustedNameTag.NAME_SOURCE, name_source) - payload += format_tlv(TrustedNameTag.CHAIN_ID, chain_id) + payload = format_tlv(FieldTag.STRUCT_VERSION, 2) + payload += format_tlv(FieldTag.TRUSTED_NAME, name) + payload += format_tlv(FieldTag.ADDRESS, addr) + payload += format_tlv(FieldTag.TRUSTED_NAME_TYPE, name_type) + payload += format_tlv(FieldTag.TRUSTED_NAME_SOURCE, name_source) + payload += format_tlv(FieldTag.CHAIN_ID, chain_id) if nft_id is not None: - payload += format_tlv(TrustedNameTag.NFT_ID, nft_id) + payload += format_tlv(FieldTag.TRUSTED_NAME_NFT_ID, nft_id) if challenge is not None: - payload += format_tlv(TrustedNameTag.CHALLENGE, challenge) + payload += format_tlv(FieldTag.CHALLENGE, challenge) if not_valid_after is not None: assert len(not_valid_after) == 3 - payload += format_tlv(TrustedNameTag.NOT_VALID_AFTER, struct.pack("BBB", *not_valid_after)) + payload += format_tlv(FieldTag.NOT_VALID_AFTER, struct.pack("BBB", *not_valid_after)) return self._provide_trusted_name_common(payload) def set_plugin(self, @@ -468,3 +473,66 @@ def provide_token_metadata(self, decimals, chain_id, sig)) + + def _prepare_network_info(self, + name: str, + ticker: str, + chain_id: int, + icon: Optional[bytes] = None) -> bytes: + + payload = format_tlv(FieldTag.STRUCT_TYPE, 8) + payload += format_tlv(FieldTag.STRUCT_VERSION, 1) + payload += format_tlv(FieldTag.BLOCKCHAIN_FAMILY, 1) + payload += format_tlv(FieldTag.CHAIN_ID, chain_id.to_bytes(8, 'big')) + payload += format_tlv(FieldTag.NETWORK_NAME, name.encode('utf-8')) + payload += format_tlv(FieldTag.TICKER, ticker.encode('utf-8')) + if icon: + # Network Icon + payload += format_tlv(FieldTag.NETWORK_ICON_HASH, sha256(icon).digest()) + # Append the data Signature + payload += format_tlv(FieldTag.DER_SIGNATURE, + sign_data(Key.CAL, payload)) + return payload + + def provide_network_information(self, + name: str, + ticker: str, + chain_id: int, + icon: Optional[bytes] = None) -> RAPDU: + + if self._pki_client is None: + print(f"Ledger-PKI Not supported on '{self._firmware.name}'") + else: + # pylint: disable=line-too-long + if self._firmware == Firmware.NANOSP: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200B45524332305F546F6B656E300200063101083201213321024CCA8FAD496AA5040A00A7EB2F5CC3B85376D88BA147A7D7054A99C64056188734010135010310040102000015473045022100C15795C2AE41E6FAE6B1362EE1AE216428507D7C1D6939B928559CC7A1F6425C02206139CF2E133DD62F3E00F183E42109C9853AC62B6B70C5079B9A80DBB9D54AB5" # noqa: E501 + elif self._firmware == Firmware.NANOX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200B45524332305F546F6B656E300200063101083201213321024CCA8FAD496AA5040A00A7EB2F5CC3B85376D88BA147A7D7054A99C64056188734010135010215473045022100E3B956F93FBFF0D41908483888F0F75D4714662A692F7A38DC6C41A13294F9370220471991BECB3CA4F43413CADC8FF738A8CC03568BFA832B4DCFE8C469080984E5" # noqa: E501 + elif self._firmware == Firmware.STAX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200B45524332305F546F6B656E300200063101083201213321024CCA8FAD496AA5040A00A7EB2F5CC3B85376D88BA147A7D7054A99C6405618873401013501041546304402206731FCD3E2432C5CA162381392FD17AD3A41EEF852E1D706F21A656AB165263602204B89FAE8DBAF191E2D79FB00EBA80D613CB7EDF0BE960CB6F6B29D96E1437F5F" # noqa: E501 + elif self._firmware == Firmware.FLEX: + cert_apdu = "01010102010211040000000212010013020002140101160400000000200B45524332305F546F6B656E300200063101083201213321024CCA8FAD496AA5040A00A7EB2F5CC3B85376D88BA147A7D7054A99C64056188734010135010515473045022100B59EA8B958AA40578A6FBE9BBFB761020ACD5DBD8AA863C11DA17F42B2AFDE790220186316059EFA58811337D47C7F815F772EA42BBBCEA4AE123D1118C80588F5CB" # noqa: E501 + # pylint: enable=line-too-long + + self._pki_client.send_certificate(PKIPubKeyUsage.PUBKEY_USAGE_COIN_META, bytes.fromhex(cert_apdu)) + + # Add the network info + payload = self._prepare_network_info(name, ticker, chain_id, icon) + chunks = self._cmd_builder.provide_network_information(payload, icon) + for chunk in chunks[:-1]: + response = self._exchange(chunk) + assert response.status == StatusWord.OK + response = self._exchange(chunks[-1]) + assert response.status == StatusWord.OK + + # Add fake (Boilerplate) glyphs to check the correct one will be used + # pylint: disable=line-too-long + fake_icon = "4000400001b30000b1001f8b08000000000002ff75d1bb0d03210c06609f228592116e856c70a31da37914a4146929292c1c83ff4404e5dc7c120f1b1ba2abb8c3f0312d32ccb0c03af9308568b3bdd088a29d0d4a74f2f0a6c3b46beab26abfa72f6d43fd2ab0c232b99bfd0986a7b3b07447d72aab1bd5d38d658115eb8273bddc01a3dff38d840d46ddbc5896f7c98f4ff4914fef8fa3f79b367fb0e5e6693e0df312ccaf2ef32dcbdcf9e27f967ffc1f6fe5d1a92100020000" # noqa: E501 + # pylint: enable=line-too-long + payload = self._prepare_network_info("Boilerplate", "BOL", chain_id + 1, bytes.fromhex(fake_icon)) + chunks = self._cmd_builder.provide_network_information(payload, bytes.fromhex(fake_icon)) + for chunk in chunks[:-1]: + response = self._exchange(chunk) + assert response.status == StatusWord.OK + response = self._exchange(chunks[-1]) + assert response.status == StatusWord.OK diff --git a/client/src/ledger_app_clients/ethereum/command_builder.py b/client/src/ledger_app_clients/ethereum/command_builder.py index ee59fb12e5..6673306457 100644 --- a/client/src/ledger_app_clients/ethereum/command_builder.py +++ b/client/src/ledger_app_clients/ethereum/command_builder.py @@ -3,7 +3,7 @@ import struct from enum import IntEnum -from typing import Optional +from typing import List, Optional from ragger.bip import pack_derivation_path from .eip712 import EIP712FieldType @@ -25,6 +25,7 @@ class InsType(IntEnum): GET_CHALLENGE = 0x20 PROVIDE_TRUSTED_NAME = 0x22 EXTERNAL_PLUGIN_SETUP = 0x12 + PROVIDE_NETWORK_INFORMATION = 0x30 class P1Type(IntEnum): @@ -32,6 +33,8 @@ class P1Type(IntEnum): PARTIAL_SEND = 0x01 SIGN_FIRST_CHUNK = 0x00 SIGN_SUBSQT_CHUNK = 0x80 + FIRST_CHUNK = 0x01 + FOLLOWING_CHUNK = 0x00 class P2Type(IntEnum): @@ -48,6 +51,8 @@ class P2Type(IntEnum): FILTERING_TOKEN_ADDR_CHECK = 0xfd FILTERING_AMOUNT_FIELD = 0xfe FILTERING_RAW = 0xff + NETWORK_CONFIG = 0x00 + NETWORK_ICON = 0x01 class CommandBuilder: @@ -408,3 +413,27 @@ def provide_erc20_token_information(self, 0x00, 0x00, payload) + + def provide_network_information(self, + tlv_payload: bytes, + icon: Optional[bytes] = None) -> list[bytes]: + chunks: List[bytes] = [] + + # Check if the TLV payload is larger than 0xff + assert len(tlv_payload) < 0xff, "Payload too large" + # Serialize the payload + chunks.append(self._serialize(InsType.PROVIDE_NETWORK_INFORMATION, + 0x00, + P2Type.NETWORK_CONFIG, + tlv_payload)) + + if icon: + p1 = P1Type.FIRST_CHUNK + while len(icon) > 0: + chunks.append(self._serialize(InsType.PROVIDE_NETWORK_INFORMATION, + p1, + P2Type.NETWORK_ICON, + icon[:0xff])) + icon = icon[0xff:] + p1 = P1Type.FOLLOWING_CHUNK + return chunks