Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'offline' option to ledgerctl CLI install command. #73

Merged
merged 6 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
57 changes: 50 additions & 7 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 @@ -162,9 +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",
agrojean-ledger marked this conversation as resolved.
Show resolved Hide resolved
help="Dump APDU installation file.",
is_flag=False,
flag_value="out.apdu",
xchapron-ledger marked this conversation as resolved.
Show resolved Hide resolved
)
@click.pass_obj
def install_app(get_client, manifest: AppManifest, force):
client = get_client()
def install_app(get_client, manifest: AppManifest, force, dump):
try:
app_manifest: AppManifest = AppManifestToml(manifest)
except TOMLDecodeError as toml_error:
Expand All @@ -177,10 +192,20 @@ 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 dump:
try:
dump_file = open(dump, "w")
except OSError:
click.echo("Unable to open file {} for dump.".format(dump))
sys.exit(1)
click.echo("Dumping APDU installation file to {}".format(dump))
client = get_file_device(dump_file, app_manifest.target_id)
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 +235,32 @@ 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(
"-d",
"--dump",
help="Dump APDU delete command file.",
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, dump):
if by_hash:
data = bytes.fromhex(app)
else:
data = app

if dump:
try:
dump_file = open(dump, "w")
except OSError:
click.echo("Unable to open file {} for dump.".format(dump))
sys.exit(1)
click.echo("Dumping APDU delete command file to {}".format(dump))
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()
67 changes: 66 additions & 1 deletion ledgerwallet/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import logging
from enum import Enum
from enum import Enum, IntEnum

from construct import (
Bytes,
Const,
FlagsEnum,
Hex,
Int8ub,
Int32ub,
Int32ul,
Optional,
PascalString,
Struct,
)


class DeviceNames(Enum):
Expand All @@ -9,6 +22,58 @@ class DeviceNames(Enum):
LEDGER_BLUE = "Ledger Blue"


class LedgerIns(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(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


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)),
)


def enable_apdu_log():
logger = logging.getLogger("ledgerwallet")
logger.setLevel(logging.DEBUG)
Expand Down