From a851c093b78aff77d8e5d911bd2ba2eec459f76a Mon Sep 17 00:00:00 2001 From: Alexis Grojean Date: Wed, 11 Oct 2023 10:52:52 +0200 Subject: [PATCH] Add 'dump' option to ledgerctl CLI install command. --- ledgerwallet/client.py | 9 ++++--- ledgerwallet/crypto/scp.py | 15 +++++++++++ ledgerwallet/ledgerctl.py | 22 ++++++++++++++-- ledgerwallet/manifest.py | 4 +++ ledgerwallet/transport/__init__.py | 5 ++++ ledgerwallet/transport/file.py | 41 ++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 ledgerwallet/transport/file.py diff --git a/ledgerwallet/client.py b/ledgerwallet/client.py index 4106844..2555278 100644 --- a/ledgerwallet/client.py +++ b/ledgerwallet/client.py @@ -22,14 +22,14 @@ from intelhex import IntelHex from ledgerwallet.crypto.ecc import PrivateKey -from ledgerwallet.crypto.scp import SCP +from ledgerwallet.crypto.scp import SCP, FakeSCP from ledgerwallet.hsmscript import HsmScript from ledgerwallet.hsmserver import HsmServer from ledgerwallet.ledgerserver import LedgerServer from ledgerwallet.manifest import AppManifest from ledgerwallet.proto.listApps_pb2 import AppList from ledgerwallet.simpleserver import SimpleServer -from ledgerwallet.transport import enumerate_devices +from ledgerwallet.transport import FileDevice, enumerate_devices from ledgerwallet.utils import serialize @@ -164,15 +164,18 @@ class NoLedgerDeviceException(Exception): class LedgerClient(object): def __init__(self, device=None, cla=0xE0, private_key=None): + self.scp = None if device is None: devices = enumerate_devices() if len(devices) == 0: raise NoLedgerDeviceException("No Ledger device has been found.") device = devices[0] + elif type(device) == FileDevice: + self.scp = FakeSCP() + self.device = device self.cla = cla self._target_id = None - self.scp = None if private_key is None: self.private_key = PrivateKey() else: diff --git a/ledgerwallet/crypto/scp.py b/ledgerwallet/crypto/scp.py index 692138a..b73f52b 100644 --- a/ledgerwallet/crypto/scp.py +++ b/ledgerwallet/crypto/scp.py @@ -112,3 +112,18 @@ def unwrap(self, data: bytes) -> bytes: raise Exception("Invalid SCP MAC") data = self._decrypt_data(encrypted_data) return iso9797_unpad(data) + + +class FakeSCP: + def __init__(self): + pass + + @staticmethod + def identity_wrap(data: bytes) -> bytes: + return data + + def wrap(self, data): + return self.identity_wrap(data) + + def unwrap(self, data): + return self.identity_wrap(data) diff --git a/ledgerwallet/ledgerctl.py b/ledgerwallet/ledgerctl.py index 1ec50eb..ea2d66f 100644 --- a/ledgerwallet/ledgerctl.py +++ b/ledgerwallet/ledgerctl.py @@ -25,6 +25,7 @@ from ledgerwallet.manifest import AppManifest from ledgerwallet.manifest_json import AppManifestJson from ledgerwallet.manifest_toml import AppManifestToml +from ledgerwallet.transport import FileDevice class ManifestFormatError(Exception): @@ -87,6 +88,14 @@ def get_private_key() -> bytes: return private_key +def get_file_device(target_id, output_file): + try: + return LedgerClient(FileDevice(target_id, out=output_file)) + except NoLedgerDeviceException as exception: + click.echo(exception) + sys.exit(0) + + @click.group() @click.option("-v", "--verbose", is_flag=True, help="Display exchanged APDU.") @click.pass_context @@ -162,8 +171,15 @@ def list_apps(get_client, remote, url, key): help="Delete using application hash instead of application name", is_flag=True, ) +@click.option( + "-d", + "--dump", + help="Dump APDU installation file.", + is_flag=False, + flag_value="out.apdu", +) @click.pass_obj -def install_app(get_client, manifest: AppManifest, force): +def install_app(get_client, manifest: AppManifest, force, dump): client = get_client() try: app_manifest: AppManifest = AppManifestToml(manifest) @@ -177,7 +193,9 @@ def install_app(get_client, manifest: AppManifest, force): raise ManifestFormatError(toml_error, json_error) try: - if force: + if dump: + client = get_file_device(app_manifest.target_id, dump) + elif force: client.delete_app(app_manifest.app_name) client.close() client = get_client() diff --git a/ledgerwallet/manifest.py b/ledgerwallet/manifest.py index b38dada..7879ca4 100644 --- a/ledgerwallet/manifest.py +++ b/ledgerwallet/manifest.py @@ -105,6 +105,10 @@ class AppManifest(ABC): def app_name(self) -> str: return self.dic.get("name", "") + @property + def target_id(self) -> str: + return self.dic.get("target_id", "") + @abstractmethod def data_size(self, device: str) -> int: pass diff --git a/ledgerwallet/transport/__init__.py b/ledgerwallet/transport/__init__.py index 452cdf2..a7a5b0f 100644 --- a/ledgerwallet/transport/__init__.py +++ b/ledgerwallet/transport/__init__.py @@ -1,11 +1,16 @@ from contextlib import contextmanager from .device import Device +from .file import FileDevice from .hid import HidDevice from .tcp import TcpDevice DEVICE_CLASSES = [TcpDevice, HidDevice] +__all__ = [ + "FileDevice", +] + def enumerate_devices(): devices = [] diff --git a/ledgerwallet/transport/file.py b/ledgerwallet/transport/file.py new file mode 100644 index 0000000..c608c7c --- /dev/null +++ b/ledgerwallet/transport/file.py @@ -0,0 +1,41 @@ +import sys + +from ..client import LedgerIns, VersionInfo +from .device import Device + + +class FileDevice(Device): + def __init__(self, target_id, out=None): + if out is None: + out = sys.stdout + t_id = int(target_id, 16) + self.version_info = VersionInfo.build( + dict(target_id=t_id, se_version="0", flags=0, mcu_version="0") + ) + self.buffer = None + self.out = out + + @classmethod + def enumerate_devices(cls): + return None + + def open(self): + pass + + def write(self, data: bytes): + self.buffer = data + if not self.buffer[1] == LedgerIns.GET_VERSION: + print(data.hex(), file=self.out) + + def read(self, timeout: int = 0) -> bytes: + if self.buffer[1] == LedgerIns.GET_VERSION: + return self.version_info + b"\x90\x00" + return b"\x00\x00\x00\x02\x90\x00" + + def exchange(self, data: bytes, timeout: int = 0) -> bytes: + self.write(data) + return self.read() + + def close(self): + if self.out: + self.out.close()