Skip to content

Commit

Permalink
Merge pull request #73 from LedgerHQ/add-installation-dump-option
Browse files Browse the repository at this point in the history
Add 'offline' option to ledgerctl CLI install command.
  • Loading branch information
agrojean-ledger authored Dec 6, 2023
2 parents ca3debd + 33d6c6d commit b51a197
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 69 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2023-12-06

### Add

- offline mode : Add an option to allow dumping the APDU installation / delete file instead of trying to send it to a device.

## [0.3.0] - 2023-05-29

### Changed
Expand Down
67 changes: 7 additions & 60 deletions ledgerwallet/client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import enum
import logging
import struct
from typing import Union

from construct import (
Bytes,
Const,
FlagsEnum,
GreedyRange,
Hex,
Int8ub,
Int32ub,
Int32ul,
Optional,
PascalString,
Rebuild,
Struct,
Expand All @@ -22,49 +18,15 @@
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.utils import serialize


class LedgerIns(enum.IntEnum):
SECUINS = 0
GET_VERSION = 1
VALIDATE_TARGET_ID = 4
INITIALIZE_AUTHENTICATION = 0x50
VALIDATE_CERTIFICATE = 0x51
GET_CERTIFICATE = 0x52
MUTUAL_AUTHENTICATE = 0x53
ONBOARD = 0xD0
RUN_APP = 0xD8
# Commands for custom endorsement
ENDORSE_SET_START = 0xC0
ENDORSE_SET_COMMIT = 0xC2


class LedgerSecureIns(enum.IntEnum):
SET_LOAD_OFFSET = 5
LOAD = 6
FLUSH = 7
CRC = 8
COMMIT = 9
CREATE_APP = 11
DELETE_APP = 12
LIST_APPS = 14
LIST_APPS_CONTINUE = 15
GET_VERSION = 16
GET_MEMORY_INFORMATION = 17
SETUP_CUSTOM_CERTIFICATE = 18
RESET_CUSTOM_CERTIFICATE = 19
DELETE_APP_BY_HASH = 21
MCU_BOOTLOADER = 0xB0

from ledgerwallet.transport import FileDevice, enumerate_devices
from ledgerwallet.utils import LedgerIns, LedgerSecureIns, VersionInfo, serialize

LOAD_SEGMENT_CHUNK_HEADER_LENGTH = 3
MIN_PADDING_LENGTH = 1
Expand Down Expand Up @@ -93,24 +55,6 @@ class LedgerSecureIns(enum.IntEnum):
),
)

VersionInfo = Struct(
target_id=Hex(Int32ub),
se_version=PascalString(Int8ub, "utf-8"),
_flags_len=Const(b"\x04"),
flags=FlagsEnum(
Int32ul,
recovery_mode=1,
signed_mcu=2,
is_onboarded=4,
trust_issuer=8,
trust_custom_ca=16,
hsm_initialized=32,
pin_validated=128,
),
mcu_version=PascalString(Int8ub, "utf-8"),
mcu_hash=Optional(Bytes(32)),
)


class AppInfo(object):
def __init__(
Expand Down Expand Up @@ -164,15 +108,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:
Expand Down
15 changes: 15 additions & 0 deletions ledgerwallet/crypto/scp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
63 changes: 55 additions & 8 deletions ledgerwallet/ledgerctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -87,6 +88,14 @@ def get_private_key() -> bytes:
return private_key


def get_file_device(output_file, target_id="0x33000004"):
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
Expand Down Expand Up @@ -159,12 +168,18 @@ def list_apps(get_client, remote, url, key):
@click.option(
"-f",
"--force",
help="Delete using application hash instead of application name",
help="Delete the app with the same name before loading the provided one.",
is_flag=True,
)
@click.option(
"-o",
"--offline",
help="Dump APDU installation file, do not attempt to connect to a physical device.",
is_flag=False,
flag_value="out.apdu",
)
@click.pass_obj
def install_app(get_client, manifest: AppManifest, force):
client = get_client()
def install_app(get_client, manifest: AppManifest, force, offline):
try:
app_manifest: AppManifest = AppManifestToml(manifest)
except TOMLDecodeError as toml_error:
Expand All @@ -177,10 +192,22 @@ def install_app(get_client, manifest: AppManifest, force):
raise ManifestFormatError(toml_error, json_error)

try:
if force:
client.delete_app(app_manifest.app_name)
client.close()
if offline:
try:
dump_file = open(offline, "w")
except OSError:
click.echo("Unable to open file {} for dump.".format(offline))
sys.exit(1)
click.echo("Dumping APDU installation file to {}".format(offline))
client = get_file_device(dump_file, app_manifest.target_id)
if force:
client.delete_app(app_manifest.app_name)
else:
client = get_client()
if force:
client.delete_app(app_manifest.app_name)
client.close()
client = get_client()
client.install_app(app_manifest)
except CommException as e:
if e.sw == 0x6985:
Expand Down Expand Up @@ -210,14 +237,34 @@ def install_remote_app(get_client, app_path, key_path, url, key):
help="Delete using application hash instead of application name",
is_flag=True,
)
@click.option(
"-o",
"--offline",
help=(
"Dump APDU delete command file, do not attempt to connect to a physical device."
),
is_flag=False,
flag_value="out_delete.apdu",
)
@click.pass_obj
def delete_app(get_client, app, by_hash):
def delete_app(get_client, app, by_hash, offline):
if by_hash:
data = bytes.fromhex(app)
else:
data = app

if offline:
try:
dump_file = open(offline, "w")
except OSError:
click.echo("Unable to open file {} for dump.".format(offline))
sys.exit(1)
click.echo("Dumping APDU delete command file to {}".format(offline))
client = get_file_device(dump_file)
else:
client = get_client()
try:
get_client().delete_app(data)
client.delete_app(data)
except CommException as e:
if e.sw == 0x6985:
click.echo("Operation has been canceled by the user.")
Expand Down
4 changes: 4 additions & 0 deletions ledgerwallet/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("targetId", "")

@abstractmethod
def data_size(self, device: str) -> int:
pass
Expand Down
5 changes: 5 additions & 0 deletions ledgerwallet/transport/__init__.py
Original file line number Diff line number Diff line change
@@ -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 = []
Expand Down
41 changes: 41 additions & 0 deletions ledgerwallet/transport/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sys

from ..utils 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()
Loading

0 comments on commit b51a197

Please sign in to comment.