diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..86c2fed --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# Run this command to always ignore formatting commits in `git blame` +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Formatting commit +3d2c9960d987b6ff0be180cadc9f9a895ee70f37 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 51f1a12..df45088 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -26,7 +26,7 @@ jobs: - name: Generate the documentation run: (cd doc && make html) - name: Upload documentation bundle - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: documentation path: doc/build/html/ diff --git a/.gitignore b/.gitignore index 46b3991..294f822 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ build/ dist/ ledgerblue.egg-info/ __pycache__ +.python-version __version__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7becaa6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.3.7 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/doc/source/conf.py b/doc/source/conf.py index 7023e6b..7a6d6e7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -7,35 +7,37 @@ from ledgerblue.__version__ import __version__ + def setup(app): - app.add_css_file('theme_overrides.css') # Override wide tables in RTD theme + app.add_css_file("theme_overrides.css") # Override wide tables in RTD theme + # General Configuration # ===================== extensions = [] -source_suffix = ['.rst'] +source_suffix = [".rst"] -master_doc = 'index' +master_doc = "index" -project = u'BOLOS Python Loader' -copyright = u'2017, Ledger Team' -author = u'Ledger Team' +project = "BOLOS Python Loader" +copyright = "2017, Ledger Team" +author = "Ledger Team" version = __version__ release = __version__ -pygments_style = 'sphinx' +pygments_style = "sphinx" # Options for HTML Output # ======================= -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" -html_static_path = ['_static'] +html_static_path = ["_static"] # sphinxarg # ========= -extensions += ['sphinxarg.ext'] +extensions += ["sphinxarg.ext"] diff --git a/ledgerblue/BleComm.py b/ledgerblue/BleComm.py index d61b5e5..3b6d4f6 100644 --- a/ledgerblue/BleComm.py +++ b/ledgerblue/BleComm.py @@ -12,21 +12,34 @@ TAG_ID = b"\x05" + def get_argparser(): parser = argparse.ArgumentParser(description="Manage ledger ble devices.") - parser.add_argument("--show", help="Show currently selected ledger device.", action='store_true') - parser.add_argument("--demo", help="Get version demo (connect to ble device, send get version, print response and disconnect).", action='store_true') + parser.add_argument( + "--show", help="Show currently selected ledger device.", action="store_true" + ) + parser.add_argument( + "--demo", + help="Get version demo (connect to ble device, send get version, print response and disconnect).", + action="store_true", + ) return parser + class NoLedgerDeviceDetected(Exception): pass + class BleScanner(object): def __init__(self): self.devices = [] def __scan_callback(self, device: BLEDevice, advertisement_data: AdvertisementData): - if LEDGER_SERVICE_UUID_STAX in advertisement_data.service_uuids or LEDGER_SERVICE_UUID_NANOX in advertisement_data.service_uuids or LEDGER_SERVICE_UUID_EUROPA in advertisement_data.service_uuids: + if ( + LEDGER_SERVICE_UUID_STAX in advertisement_data.service_uuids + or LEDGER_SERVICE_UUID_NANOX in advertisement_data.service_uuids + or LEDGER_SERVICE_UUID_EUROPA in advertisement_data.service_uuids + ): device_is_in_list = False for dev in self.devices: if device.address == dev[0]: @@ -45,12 +58,15 @@ async def scan(self): counter += 1 await scanner.stop() + queue: asyncio.Queue = asyncio.Queue() + def callback(sender, data): response = bytes(data) queue.put_nowait(response) + async def _get_client(address: str) -> BleakClient: # Connect to client client = BleakClient(address) @@ -60,7 +76,11 @@ async def _get_client(address: str) -> BleakClient: characteristic_write_with_rsp = None characteristic_write_cmd = None for service in client.services: - if service.uuid in [LEDGER_SERVICE_UUID_NANOX, LEDGER_SERVICE_UUID_STAX, LEDGER_SERVICE_UUID_EUROPA]: + if service.uuid in [ + LEDGER_SERVICE_UUID_NANOX, + LEDGER_SERVICE_UUID_STAX, + LEDGER_SERVICE_UUID_EUROPA, + ]: for char in service.characteristics: if "0001" in char.uuid: characteristic_notify = char @@ -86,44 +106,45 @@ async def _get_client(address: str) -> BleakClient: # Get MTU value await client.write_gatt_char(characteristic_write.uuid, bytes.fromhex("0800000000")) response = await queue.get() - mtu = int.from_bytes(response[5:6], 'big') + mtu = int.from_bytes(response[5:6], "big") print("[BLE] MTU {:d}".format(mtu)) return client, mtu, characteristic_write + async def _read(mtu) -> bytes: response = await queue.get() assert len(response) >= 5 assert response[0] == TAG_ID[0] - sequence = int.from_bytes(response[1:3], 'big') + sequence = int.from_bytes(response[1:3], "big") assert sequence == 0 total_size = int.from_bytes(response[3:5], "big") total_size_bkup = total_size - if total_size >= (mtu-5): + if total_size >= (mtu - 5): apdu = response[5:mtu] - total_size -= (mtu-5) + total_size -= mtu - 5 response = await queue.get() assert response[0] == TAG_ID[0] assert len(response) >= 3 - next_sequence = int.from_bytes(response[1:3], 'big') - assert next_sequence == sequence+1 + next_sequence = int.from_bytes(response[1:3], "big") + assert next_sequence == sequence + 1 sequence = next_sequence - while total_size >= (mtu-3): + while total_size >= (mtu - 3): apdu += response[3:mtu] - total_size -= (mtu-3) + total_size -= mtu - 3 response = await queue.get() assert response[0] == TAG_ID[0] assert len(response) >= 3 - sequence = int.from_bytes(response[1:3], 'big') - assert next_sequence == sequence+1 + sequence = int.from_bytes(response[1:3], "big") + assert next_sequence == sequence + 1 sequence = next_sequence if total_size > 0: - apdu += response[3:3+total_size] + apdu += response[3 : 3 + total_size] else: apdu = response[5:] @@ -162,7 +183,9 @@ def __init__(self, address): def open(self): self.loop = asyncio.new_event_loop() - self.client, self.mtu, self.write_characteristic = self.loop.run_until_complete(_get_client(self.address)) + self.client, self.mtu, self.write_characteristic = self.loop.run_until_complete( + _get_client(self.address) + ) self.opened = True def close(self): @@ -172,7 +195,9 @@ def close(self): self.loop.close() def __write(self, data: bytes): - self.loop.run_until_complete(_write(self.client, data, self.write_characteristic, self.mtu)) + self.loop.run_until_complete( + _write(self.client, data, self.write_characteristic, self.mtu) + ) def __read(self) -> bytes: return self.loop.run_until_complete(_read(self.mtu)) @@ -181,18 +206,25 @@ def exchange(self, data: bytes, timeout=1000) -> bytes: self.__write(data) return self.__read() + if __name__ == "__main__": args = get_argparser().parse_args() try: if args.show: - print(f"Environment variable LEDGER_BLE_MAC currently set to '{os.environ['LEDGER_BLE_MAC']}'") + print( + f"Environment variable LEDGER_BLE_MAC currently set to '{os.environ['LEDGER_BLE_MAC']}'" + ) elif args.demo: try: - ble_device = BleDevice(os.environ['LEDGER_BLE_MAC']) + ble_device = BleDevice(os.environ["LEDGER_BLE_MAC"]) except Exception as ex: - print(f"Please run 'python -m ledgerblue.BleComm' to select wich device to connect to") + print( + f"Please run 'python -m ledgerblue.BleComm' to select wich device to connect to" + ) raise ex - print("-----------------------------Get version BLE demo------------------------------") + print( + "-----------------------------Get version BLE demo------------------------------" + ) ble_device.open() print(f"Connected to {ble_device.address}") get_version_apdu = bytes.fromhex("e001000000") @@ -201,7 +233,7 @@ def exchange(self, data: bytes, timeout=1000) -> bytes: print(f"[BLE] <= {result.hex()}") ble_device.close() print(f"Disconnected from {ble_device.address}") - print(79*"-") + print(79 * "-") else: scanner = BleScanner() asyncio.run(scanner.scan()) @@ -209,14 +241,17 @@ def exchange(self, data: bytes, timeout=1000) -> bytes: device_idx = 0 if len(scanner.devices): for device in scanner.devices: - devices_str += f" -{device_idx+1}- mac=\"{device[0]}\" name=\"{device[1]}\"\n" + devices_str += ( + f' -{device_idx + 1}- mac="{device[0]}" name="{device[1]}"\n' + ) device_idx += 1 index = int(input(f"Select device by index\n{devices_str}")) - print(f"Please run 'export LEDGER_BLE_MAC=\"{scanner.devices[index-1][0]}\"' to select which device to connect to") + print( + f"Please run 'export LEDGER_BLE_MAC=\"{scanner.devices[index - 1][0]}\"' to select which device to connect to" + ) else: raise NoLedgerDeviceDetected except NoLedgerDeviceDetected as ex: print(ex) except Exception as ex: raise ex - diff --git a/ledgerblue/BlueHSMServer_pb2.py b/ledgerblue/BlueHSMServer_pb2.py index 2b09fce..33ba34a 100644 --- a/ledgerblue/BlueHSMServer_pb2.py +++ b/ledgerblue/BlueHSMServer_pb2.py @@ -2,7 +2,8 @@ # source: BlueHSMServer.proto import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) + +_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -13,211 +14,355 @@ _sym_db = _symbol_database.Default() - - DESCRIPTOR = _descriptor.FileDescriptor( - name='BlueHSMServer.proto', - package='bluehsmserver', - serialized_pb=_b('\n\x13\x42lueHSMServer.proto\x12\rbluehsmserver\"7\n\tParameter\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\r\n\x05local\x18\x03 \x01(\x08\"\xa1\x01\n\x07Request\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nparameters\x18\x02 \x01(\x0c\x12\x11\n\treference\x18\x03 \x01(\t\x12\x0b\n\x03\x65lf\x18\x04 \x01(\x0c\x12\r\n\x05\x63lose\x18\x05 \x01(\x08\x12\x12\n\nlargeStack\x18\x06 \x01(\x08\x12\x33\n\x11remote_parameters\x18\x07 \x03(\x0b\x32\x18.bluehsmserver.Parameter\"L\n\x08Response\x12\n\n\x02id\x18\x01 \x02(\t\x12\x10\n\x08response\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x11\n\texception\x18\x04 \x01(\tB-\n\x1b\x63om.ledger.bluehsm.protobufB\x0c\x42lueHSMProtoH\x01') + name="BlueHSMServer.proto", + package="bluehsmserver", + serialized_pb=_b( + '\n\x13\x42lueHSMServer.proto\x12\rbluehsmserver"7\n\tParameter\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\r\n\x05local\x18\x03 \x01(\x08"\xa1\x01\n\x07Request\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nparameters\x18\x02 \x01(\x0c\x12\x11\n\treference\x18\x03 \x01(\t\x12\x0b\n\x03\x65lf\x18\x04 \x01(\x0c\x12\r\n\x05\x63lose\x18\x05 \x01(\x08\x12\x12\n\nlargeStack\x18\x06 \x01(\x08\x12\x33\n\x11remote_parameters\x18\x07 \x03(\x0b\x32\x18.bluehsmserver.Parameter"L\n\x08Response\x12\n\n\x02id\x18\x01 \x02(\t\x12\x10\n\x08response\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x11\n\texception\x18\x04 \x01(\tB-\n\x1b\x63om.ledger.bluehsm.protobufB\x0c\x42lueHSMProtoH\x01' + ), ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) - - _PARAMETER = _descriptor.Descriptor( - name='Parameter', - full_name='bluehsmserver.Parameter', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='bluehsmserver.Parameter.name', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='alias', full_name='bluehsmserver.Parameter.alias', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='local', full_name='bluehsmserver.Parameter.local', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=38, - serialized_end=93, + name="Parameter", + full_name="bluehsmserver.Parameter", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="name", + full_name="bluehsmserver.Parameter.name", + index=0, + number=1, + type=9, + cpp_type=9, + label=2, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="alias", + full_name="bluehsmserver.Parameter.alias", + index=1, + number=2, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="local", + full_name="bluehsmserver.Parameter.local", + index=2, + number=3, + type=8, + cpp_type=7, + label=1, + has_default_value=False, + default_value=False, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[], + serialized_start=38, + serialized_end=93, ) _REQUEST = _descriptor.Descriptor( - name='Request', - full_name='bluehsmserver.Request', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='id', full_name='bluehsmserver.Request.id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='parameters', full_name='bluehsmserver.Request.parameters', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='reference', full_name='bluehsmserver.Request.reference', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='elf', full_name='bluehsmserver.Request.elf', index=3, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='close', full_name='bluehsmserver.Request.close', index=4, - number=5, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='largeStack', full_name='bluehsmserver.Request.largeStack', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='remote_parameters', full_name='bluehsmserver.Request.remote_parameters', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=96, - serialized_end=257, + name="Request", + full_name="bluehsmserver.Request", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="id", + full_name="bluehsmserver.Request.id", + index=0, + number=1, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="parameters", + full_name="bluehsmserver.Request.parameters", + index=1, + number=2, + type=12, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b(""), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="reference", + full_name="bluehsmserver.Request.reference", + index=2, + number=3, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="elf", + full_name="bluehsmserver.Request.elf", + index=3, + number=4, + type=12, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b(""), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="close", + full_name="bluehsmserver.Request.close", + index=4, + number=5, + type=8, + cpp_type=7, + label=1, + has_default_value=False, + default_value=False, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="largeStack", + full_name="bluehsmserver.Request.largeStack", + index=5, + number=6, + type=8, + cpp_type=7, + label=1, + has_default_value=False, + default_value=False, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="remote_parameters", + full_name="bluehsmserver.Request.remote_parameters", + index=6, + number=7, + type=11, + cpp_type=10, + label=3, + has_default_value=False, + default_value=[], + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[], + serialized_start=96, + serialized_end=257, ) _RESPONSE = _descriptor.Descriptor( - name='Response', - full_name='bluehsmserver.Response', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='id', full_name='bluehsmserver.Response.id', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='response', full_name='bluehsmserver.Response.response', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='message', full_name='bluehsmserver.Response.message', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='exception', full_name='bluehsmserver.Response.exception', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=259, - serialized_end=335, + name="Response", + full_name="bluehsmserver.Response", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="id", + full_name="bluehsmserver.Response.id", + index=0, + number=1, + type=9, + cpp_type=9, + label=2, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="response", + full_name="bluehsmserver.Response.response", + index=1, + number=2, + type=12, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b(""), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="message", + full_name="bluehsmserver.Response.message", + index=2, + number=3, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + _descriptor.FieldDescriptor( + name="exception", + full_name="bluehsmserver.Response.exception", + index=3, + number=4, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=_b("").decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + options=None, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[], + serialized_start=259, + serialized_end=335, ) -_REQUEST.fields_by_name['remote_parameters'].message_type = _PARAMETER -DESCRIPTOR.message_types_by_name['Parameter'] = _PARAMETER -DESCRIPTOR.message_types_by_name['Request'] = _REQUEST -DESCRIPTOR.message_types_by_name['Response'] = _RESPONSE +_REQUEST.fields_by_name["remote_parameters"].message_type = _PARAMETER +DESCRIPTOR.message_types_by_name["Parameter"] = _PARAMETER +DESCRIPTOR.message_types_by_name["Request"] = _REQUEST +DESCRIPTOR.message_types_by_name["Response"] = _RESPONSE -Parameter = _reflection.GeneratedProtocolMessageType('Parameter', (_message.Message,), dict( - DESCRIPTOR = _PARAMETER, - __module__ = 'BlueHSMServer_pb2' - # @@protoc_insertion_point(class_scope:bluehsmserver.Parameter) - )) +Parameter = _reflection.GeneratedProtocolMessageType( + "Parameter", + (_message.Message,), + dict( + DESCRIPTOR=_PARAMETER, + __module__="BlueHSMServer_pb2", + # @@protoc_insertion_point(class_scope:bluehsmserver.Parameter) + ), +) _sym_db.RegisterMessage(Parameter) -Request = _reflection.GeneratedProtocolMessageType('Request', (_message.Message,), dict( - DESCRIPTOR = _REQUEST, - __module__ = 'BlueHSMServer_pb2' - # @@protoc_insertion_point(class_scope:bluehsmserver.Request) - )) +Request = _reflection.GeneratedProtocolMessageType( + "Request", + (_message.Message,), + dict( + DESCRIPTOR=_REQUEST, + __module__="BlueHSMServer_pb2", + # @@protoc_insertion_point(class_scope:bluehsmserver.Request) + ), +) _sym_db.RegisterMessage(Request) -Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), dict( - DESCRIPTOR = _RESPONSE, - __module__ = 'BlueHSMServer_pb2' - # @@protoc_insertion_point(class_scope:bluehsmserver.Response) - )) +Response = _reflection.GeneratedProtocolMessageType( + "Response", + (_message.Message,), + dict( + DESCRIPTOR=_RESPONSE, + __module__="BlueHSMServer_pb2", + # @@protoc_insertion_point(class_scope:bluehsmserver.Response) + ), +) _sym_db.RegisterMessage(Response) DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\033com.ledger.bluehsm.protobufB\014BlueHSMProtoH\001')) +DESCRIPTOR._options = _descriptor._ParseOptions( + descriptor_pb2.FileOptions(), + _b("\n\033com.ledger.bluehsm.protobufB\014BlueHSMProtoH\001"), +) # @@protoc_insertion_point(module_scope) diff --git a/ledgerblue/Dongle.py b/ledgerblue/Dongle.py index d64eed9..07d62e1 100644 --- a/ledgerblue/Dongle.py +++ b/ledgerblue/Dongle.py @@ -16,31 +16,34 @@ * limitations under the License. ******************************************************************************** """ + from abc import ABCMeta, abstractmethod -TIMEOUT=20000 +TIMEOUT = 20000 + class DongleWait(object): - __metaclass__ = ABCMeta + __metaclass__ = ABCMeta + + @abstractmethod + def waitFirstResponse(self, timeout): + pass - @abstractmethod - def waitFirstResponse(self, timeout): - pass class Dongle(object): - __metaclass__ = ABCMeta + __metaclass__ = ABCMeta - @abstractmethod - def exchange(self, apdu, timeout=TIMEOUT): - pass + @abstractmethod + def exchange(self, apdu, timeout=TIMEOUT): + pass - @abstractmethod - def apduMaxDataSize(self): - pass + @abstractmethod + def apduMaxDataSize(self): + pass - @abstractmethod - def close(self): - pass + @abstractmethod + def close(self): + pass - def setWaitImpl(self, waitImpl): - self.waitImpl = waitImpl \ No newline at end of file + def setWaitImpl(self, waitImpl): + self.waitImpl = waitImpl diff --git a/ledgerblue/__init__.py b/ledgerblue/__init__.py index 4fcf13e..0eb238a 100644 --- a/ledgerblue/__init__.py +++ b/ledgerblue/__init__.py @@ -16,6 +16,7 @@ * limitations under the License. ******************************************************************************** """ + try: from ledgerblue.__version__ import __version__ # noqa except ImportError: diff --git a/ledgerblue/checkGenuine.py b/ledgerblue/checkGenuine.py index 8644c79..b17cad5 100644 --- a/ledgerblue/checkGenuine.py +++ b/ledgerblue/checkGenuine.py @@ -23,42 +23,63 @@ def get_argparser(): - parser = argparse.ArgumentParser(description="""Use attestation to determine if the device is a genuine Ledger -device.""") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", - type=auto_int, default=0x31000002) - parser.add_argument("--issuerKey", help="Issuer key (hex encoded, default is batch 1)", default=DEFAULT_ISSUER_KEY) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') + parser = argparse.ArgumentParser( + description="""Use attestation to determine if the device is a genuine Ledger +device.""" + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--issuerKey", + help="Issuer key (hex encoded, default is batch 1)", + default=DEFAULT_ISSUER_KEY, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") return parser + def auto_int(x): return int(x, 0) + def getDeployedSecretV2(dongle, masterPrivate, targetId, issuerKey): testMaster = PrivateKey(bytes(masterPrivate)) testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) - targetid = bytearray(struct.pack('>I', targetId)) + targetid = bytearray(struct.pack(">I", targetId)) # identify - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid dongle.exchange(apdu) # walk the chain nonce = os.urandom(8) - apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce + apdu = bytearray([0xE0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce auth_info = dongle.exchange(apdu) batch_signer_serial = auth_info[0:4] deviceNonce = auth_info[4:12] # if not found, get another pair - #if cardKey != testMasterPublic: + # if cardKey != testMasterPublic: # raise Exception("Invalid batch public key") dataToSign = bytes(bytearray([0x01]) + testMasterPublic) signature = testMaster.ecdsa_sign(bytes(dataToSign)) signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate + certificate = ( + bytearray([len(testMasterPublic)]) + + testMasterPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x00, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) dongle.exchange(apdu) # provide the ephemeral certificate @@ -67,8 +88,17 @@ def getDeployedSecretV2(dongle, masterPrivate, targetId, issuerKey): dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic) signature = testMaster.ecdsa_sign(bytes(dataToSign)) signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate + certificate = ( + bytearray([len(ephemeralPublic)]) + + ephemeralPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) dongle.exchange(apdu) # walk the device certificates to retrieve the public key to use for authentication @@ -77,43 +107,54 @@ def getDeployedSecretV2(dongle, masterPrivate, targetId, issuerKey): devicePublicKey = None while True: if index == 0: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) elif index == 1: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) else: break offset = 1 - certificateHeader = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificatePublicKey = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificateSignatureArray = certificate[offset : offset + certificate[offset-1]] - certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray)) + certificateHeader = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificatePublicKey = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificateSignatureArray = certificate[ + offset : offset + certificate[offset - 1] + ] + certificateSignature = last_pub_key.ecdsa_deserialize( + bytes(certificateSignatureArray) + ) # first cert contains a header field which holds the certificate's public key role if index == 0: devicePublicKey = certificatePublicKey - certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey + certificateSignedData = ( + bytearray([0x02]) + certificateHeader + certificatePublicKey + ) # Could check if the device certificate is signed by the issuer public key # ephemeral key certificate else: - certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey - if not last_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature): + certificateSignedData = ( + bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey + ) + if not last_pub_key.ecdsa_verify( + bytes(certificateSignedData), certificateSignature + ): return None last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True) index = index + 1 # Commit device ECDH channel - dongle.exchange(bytearray.fromhex('E053000000')) + dongle.exchange(bytearray.fromhex("E053000000")) secret = last_pub_key.ecdh(binascii.unhexlify(ephemeralPrivate.serialize())) - if targetId&0xF == 0x2: + if targetId & 0xF == 0x2: return secret[0:16] - elif targetId&0xF >= 0x3: + elif targetId & 0xF >= 0x3: ret = {} - ret['ecdh_secret'] = secret - ret['devicePublicKey'] = devicePublicKey + ret["ecdh_secret"] = secret + ret["devicePublicKey"] = devicePublicKey return ret -if __name__ == '__main__': + +if __name__ == "__main__": from .ecWrapper import PrivateKey, PublicKey from .comm import getDongle from .hexLoader import HexLoader @@ -134,34 +175,36 @@ def getDeployedSecretV2(dongle, masterPrivate, targetId, issuerKey): dongle = getDongle(args.apdu) version = None - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey) + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey + ) if secret != None: try: - loader = HexLoader(dongle, 0xe0, True, secret) + loader = HexLoader(dongle, 0xE0, True, secret) version = loader.getVersion() genuine = True apps = loader.listApp() while len(apps) != 0: for app in apps: - if app['flags'] & 0x08: + if app["flags"] & 0x08: ui = True - if app['flags'] & 0x400: + if app["flags"] & 0x400: customCA = True apps = loader.listApp(False) except: genuine = False if genuine: if ui: - print ("WARNING : Product is genuine but has a UI application loaded") + print("WARNING : Product is genuine but has a UI application loaded") if customCA: - print ("WARNING : Product is genuine but has a Custom CA loaded") + print("WARNING : Product is genuine but has a Custom CA loaded") if not ui and not customCA: - print ("Product is genuine") - print ("SE Version " + version['osVersion']) - print ("MCU Version " + version['mcuVersion']) - if 'mcuHash' in version: - print ("MCU Hash " + binascii.hexlify(version['mcuHash']).decode('ascii')) + print("Product is genuine") + print("SE Version " + version["osVersion"]) + print("MCU Version " + version["mcuVersion"]) + if "mcuHash" in version: + print("MCU Hash " + binascii.hexlify(version["mcuHash"]).decode("ascii")) else: - print ("Product is NOT genuine") + print("Product is NOT genuine") - dongle.close() \ No newline at end of file + dongle.close() diff --git a/ledgerblue/checkGenuineRemote.py b/ledgerblue/checkGenuineRemote.py index d3ff4e7..1477a83 100644 --- a/ledgerblue/checkGenuineRemote.py +++ b/ledgerblue/checkGenuineRemote.py @@ -19,72 +19,93 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser("Update the firmware by using Ledger to open a Secure Channel.") - parser.add_argument("--url", help="Websocket URL", default="wss://scriptrunner.api.live.ledger.com/update/genuine") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific -Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - return parser + parser = argparse.ArgumentParser( + "Update the firmware by using Ledger to open a Secure Channel." + ) + parser.add_argument( + "--url", + help="Websocket URL", + default="wss://scriptrunner.api.live.ledger.com/update/genuine", + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--perso", + help="""A reference to the personalization key; this is a reference to the specific +Issuer keypair used by Ledger to sign the device's Issuer Certificate""", + default="perso_11", + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def process(dongle, request): - response = {} - apdusList = [] - try: - response['nonce'] = request['nonce'] - if request['query'] == "exchange": - apdusList.append(binascii.unhexlify(request['data'])) - elif request['query'] == "bulk": - for apdu in request['data']: - apdusList.append(binascii.unhexlify(apdu)) - else: - response['response'] = "unsupported" - except: - response['response'] = "parse error" - - if len(apdusList) != 0: - try: - for apdu in apdusList: - response['data'] = dongle.exchange(apdu).hex() - if len(response['data']) == 0: - response['data'] = "9000" - response['response'] = "success" - except: - response['response'] = "I/O" # or error, and SW in data - - return response - -if __name__ == '__main__': - import urllib.parse as urlparse - from .comm import getDongle - from websocket import create_connection - import json - import binascii - - args = get_argparser().parse_args() - - dongle = getDongle(args.apdu) - - url = args.url - queryParameters = {} - queryParameters['targetId'] = args.targetId - queryParameters['perso'] = args.perso - queryString = urlparse.urlencode(queryParameters) - ws = create_connection(args.url + '?' + queryString) - while True: - result = json.loads(ws.recv()) - if result['query'] == 'success': - break - if result['query'] == 'error': - raise Exception(result['data'] + " on " + result['uuid'] + "/" + result['session']) - response = process(dongle, result) - ws.send(json.dumps(response)) - ws.close() - - print("Product is genuine") - - dongle.close() + response = {} + apdusList = [] + try: + response["nonce"] = request["nonce"] + if request["query"] == "exchange": + apdusList.append(binascii.unhexlify(request["data"])) + elif request["query"] == "bulk": + for apdu in request["data"]: + apdusList.append(binascii.unhexlify(apdu)) + else: + response["response"] = "unsupported" + except: + response["response"] = "parse error" + + if len(apdusList) != 0: + try: + for apdu in apdusList: + response["data"] = dongle.exchange(apdu).hex() + if len(response["data"]) == 0: + response["data"] = "9000" + response["response"] = "success" + except: + response["response"] = "I/O" # or error, and SW in data + + return response + + +if __name__ == "__main__": + import urllib.parse as urlparse + from .comm import getDongle + from websocket import create_connection + import json + import binascii + + args = get_argparser().parse_args() + + dongle = getDongle(args.apdu) + + url = args.url + queryParameters = {} + queryParameters["targetId"] = args.targetId + queryParameters["perso"] = args.perso + queryString = urlparse.urlencode(queryParameters) + ws = create_connection(args.url + "?" + queryString) + while True: + result = json.loads(ws.recv()) + if result["query"] == "success": + break + if result["query"] == "error": + raise Exception( + result["data"] + " on " + result["uuid"] + "/" + result["session"] + ) + response = process(dongle, result) + ws.send(json.dumps(response)) + ws.close() + + print("Product is genuine") + + dongle.close() diff --git a/ledgerblue/comm.py b/ledgerblue/comm.py index 8fe7fcd..5164992 100644 --- a/ledgerblue/comm.py +++ b/ledgerblue/comm.py @@ -35,52 +35,60 @@ from .BleComm import BleDevice -APDUGEN=None +APDUGEN = None if "APDUGEN" in os.environ and len(os.environ["APDUGEN"]) != 0: - APDUGEN=os.environ["APDUGEN"] + APDUGEN = os.environ["APDUGEN"] # Force use of U2F if required -U2FKEY=None +U2FKEY = None if "U2FKEY" in os.environ and len(os.environ["U2FKEY"]) != 0: - U2FKEY=os.environ["U2FKEY"] + U2FKEY = os.environ["U2FKEY"] # Force use of MCUPROXY if required -MCUPROXY=None +MCUPROXY = None if "MCUPROXY" in os.environ and len(os.environ["MCUPROXY"]) != 0: - MCUPROXY=os.environ["MCUPROXY"] + MCUPROXY = os.environ["MCUPROXY"] # Force use of TCP PROXY if required -TCP_PROXY=None -if "LEDGER_PROXY_ADDRESS" in os.environ and len(os.environ["LEDGER_PROXY_ADDRESS"]) != 0 and \ - "LEDGER_PROXY_PORT" in os.environ and len(os.environ["LEDGER_PROXY_PORT"]) != 0: - TCP_PROXY=(os.environ["LEDGER_PROXY_ADDRESS"], int(os.environ["LEDGER_PROXY_PORT"])) -NFC_PROXY=None +TCP_PROXY = None +if ( + "LEDGER_PROXY_ADDRESS" in os.environ + and len(os.environ["LEDGER_PROXY_ADDRESS"]) != 0 + and "LEDGER_PROXY_PORT" in os.environ + and len(os.environ["LEDGER_PROXY_PORT"]) != 0 +): + TCP_PROXY = ( + os.environ["LEDGER_PROXY_ADDRESS"], + int(os.environ["LEDGER_PROXY_PORT"]), + ) +NFC_PROXY = None if "LEDGER_NFC_PROXY" in os.environ: - NFC_PROXY=True + NFC_PROXY = True -BLE_PROXY=None +BLE_PROXY = None if "LEDGER_BLE_PROXY" in os.environ: - BLE_PROXY=True + BLE_PROXY = True # Force use of MCUPROXY if required -PCSC=None +PCSC = None if "PCSC" in os.environ and len(os.environ["PCSC"]) != 0: - PCSC=os.environ["PCSC"] + PCSC = os.environ["PCSC"] if PCSC: - try: - from smartcard.Exceptions import NoCardException - from smartcard.System import readers - from smartcard.util import toHexString, toBytes - except ImportError: - PCSC = False + try: + from smartcard.Exceptions import NoCardException + from smartcard.System import readers + from smartcard.util import toHexString, toBytes + except ImportError: + PCSC = False + def get_possible_error_cause(sw): cause_map = { 0x6982: "Have you uninstalled the existing CA with resetCustomCA first?", 0x6985: "Condition of use not satisfied (denied by the user?)", - 0x6a84: "Not enough space?", - 0x6a85: "Not enough space?", - 0x6a83: "Maybe this app requires a library to be installed first?", + 0x6A84: "Not enough space?", + 0x6A85: "Not enough space?", + 0x6A83: "Maybe this app requires a library to be installed first?", 0x6484: "Are you using the correct targetId?", - 0x6d00: "Unexpected state of device: verify that the right application is opened?", - 0x6e00: "Unexpected state of device: verify that the right application is opened?", + 0x6D00: "Unexpected state of device: verify that the right application is opened?", + 0x6E00: "Unexpected state of device: verify that the right application is opened?", 0x5515: "Did you unlock the device?", 0x6814: "Unexpected target device: verify that you are using the right device?", 0x511F: "The OS version on your device does not seem compatible with the SDK version used to build the app", @@ -92,231 +100,244 @@ def get_possible_error_cause(sw): class HIDDongleHIDAPI(Dongle, DongleWait): - - def __init__(self, device, ledger=False, debug=False): - self.device = device - self.ledger = ledger - self.debug = debug - self.waitImpl = self - self.opened = True - - def exchange(self, apdu, timeout=TIMEOUT): - if APDUGEN: - print(apdu.hex()) - return b"" - - if self.debug: - print("HID => %s" % apdu.hex()) - if self.ledger: - apdu = wrapCommandAPDU(0x0101, apdu, 64) - padSize = len(apdu) % 64 - tmp = apdu - if padSize != 0: - tmp.extend([0] * (64 - padSize)) - offset = 0 - while offset != len(tmp): - data = tmp[offset:offset + 64] - data = bytearray([0]) + data - if self.device.write(data) < 0: - raise BaseException("Error while writing") - offset += 64 - dataLength = 0 - dataStart = 2 - result = self.waitImpl.waitFirstResponse(timeout) - if not self.ledger: - if result[0] == 0x61: # 61xx : data available - self.device.set_nonblocking(False) - dataLength = result[1] - dataLength += 2 - if dataLength > 62: - remaining = dataLength - 62 - while remaining != 0: - if remaining > 64: - blockLength = 64 - else: - blockLength = remaining - result.extend(bytearray(self.device.read(65))[0:blockLength]) - remaining -= blockLength - swOffset = dataLength - dataLength -= 2 - self.device.set_nonblocking(True) - else: - swOffset = 0 - else: - self.device.set_nonblocking(False) - while True: - response = unwrapResponseAPDU(0x0101, result, 64) - if response is not None: - result = response - dataStart = 0 - swOffset = len(response) - 2 - dataLength = len(response) - 2 - self.device.set_nonblocking(True) - break - result.extend(bytearray(self.device.read(65))) - sw = (result[swOffset] << 8) + result[swOffset + 1] - response = result[dataStart : dataLength + dataStart] - if self.debug: - print("HID <= %s%.2x" % (response.hex(), sw)) - if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: - possibleCause = get_possible_error_cause(sw) - raise CommException("Invalid status %04x (%s)" % (sw, possibleCause), sw, response) - return response - - def waitFirstResponse(self, timeout): - start = time.time() - data = "" - while len(data) == 0: - data = self.device.read(65) - if not len(data): - if time.time() - start > timeout: - raise CommException("Timeout") - time.sleep(0.0001) - return bytearray(data) - - def apduMaxDataSize(self): - return 255 - - def close(self): - if self.opened: - try: - self.device.close() - except: - pass - self.opened = False + def __init__(self, device, ledger=False, debug=False): + self.device = device + self.ledger = ledger + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=TIMEOUT): + if APDUGEN: + print(apdu.hex()) + return b"" + + if self.debug: + print("HID => %s" % apdu.hex()) + if self.ledger: + apdu = wrapCommandAPDU(0x0101, apdu, 64) + padSize = len(apdu) % 64 + tmp = apdu + if padSize != 0: + tmp.extend([0] * (64 - padSize)) + offset = 0 + while offset != len(tmp): + data = tmp[offset : offset + 64] + data = bytearray([0]) + data + if self.device.write(data) < 0: + raise BaseException("Error while writing") + offset += 64 + dataLength = 0 + dataStart = 2 + result = self.waitImpl.waitFirstResponse(timeout) + if not self.ledger: + if result[0] == 0x61: # 61xx : data available + self.device.set_nonblocking(False) + dataLength = result[1] + dataLength += 2 + if dataLength > 62: + remaining = dataLength - 62 + while remaining != 0: + if remaining > 64: + blockLength = 64 + else: + blockLength = remaining + result.extend(bytearray(self.device.read(65))[0:blockLength]) + remaining -= blockLength + swOffset = dataLength + dataLength -= 2 + self.device.set_nonblocking(True) + else: + swOffset = 0 + else: + self.device.set_nonblocking(False) + while True: + response = unwrapResponseAPDU(0x0101, result, 64) + if response is not None: + result = response + dataStart = 0 + swOffset = len(response) - 2 + dataLength = len(response) - 2 + self.device.set_nonblocking(True) + break + result.extend(bytearray(self.device.read(65))) + sw = (result[swOffset] << 8) + result[swOffset + 1] + response = result[dataStart : dataLength + dataStart] + if self.debug: + print("HID <= %s%.2x" % (response.hex(), sw)) + if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: + possibleCause = get_possible_error_cause(sw) + raise CommException( + "Invalid status %04x (%s)" % (sw, possibleCause), sw, response + ) + return response + + def waitFirstResponse(self, timeout): + start = time.time() + data = "" + while len(data) == 0: + data = self.device.read(65) + if not len(data): + if time.time() - start > timeout: + raise CommException("Timeout") + time.sleep(0.0001) + return bytearray(data) + + def apduMaxDataSize(self): + return 255 + + def close(self): + if self.opened: + try: + self.device.close() + except: + pass + self.opened = False class DongleNFC(Dongle, DongleWait): - def __init__(self, debug = False): - self.waitImpl = self - self.opened = True - self.debug = debug - self.clf = nfc.ContactlessFrontend('usb') - self.tag = self.clf.connect(rdwr={'on-connect': lambda tag: False}) - - def exchange(self, apdu, timeout=TIMEOUT): - if self.debug: - print(f"[NFC] => {apdu.hex()}") - response = self.tag.transceive(apdu, 5.0) - sw = (response[-2] << 8) + response[-1] - if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: - possibleCause = get_possible_error_cause(sw) - self.close() - raise CommException("Invalid status %04x (%s)" % (sw, possibleCause), sw, response) - if self.debug: - print(f"[NFC] <= {response.hex()}") - return response - - def apduMaxDataSize(self): - return 255 - - def close(self): - self.clf.close() - pass + def __init__(self, debug=False): + self.waitImpl = self + self.opened = True + self.debug = debug + self.clf = nfc.ContactlessFrontend("usb") + self.tag = self.clf.connect(rdwr={"on-connect": lambda tag: False}) + + def exchange(self, apdu, timeout=TIMEOUT): + if self.debug: + print(f"[NFC] => {apdu.hex()}") + response = self.tag.transceive(apdu, 5.0) + sw = (response[-2] << 8) + response[-1] + if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: + possibleCause = get_possible_error_cause(sw) + self.close() + raise CommException( + "Invalid status %04x (%s)" % (sw, possibleCause), sw, response + ) + if self.debug: + print(f"[NFC] <= {response.hex()}") + return response + + def apduMaxDataSize(self): + return 255 + + def close(self): + self.clf.close() + pass + class DongleBLE(Dongle, DongleWait): - def __init__(self, debug = False): - self.waitImpl = self - self.debug = debug - try: - self.device = BleDevice(os.environ['LEDGER_BLE_MAC']) - self.device.open() - except KeyError as ex: - sys.exit(f"Key Error\nPlease run 'python -m ledgerblue.BleComm' to select wich device to connect to") - self.opened = self.device.opened - - def exchange(self, apdu, timeout=TIMEOUT): - if self.debug: - print(f"[BLE] => {apdu.hex()}") - response = self.device.exchange(apdu, timeout) - sw = (response[-2] << 8) + response[-1] - response = response[0:-2] - if self.debug: - print("[BLE] <= %s%.2x" % (response.hex(), sw)) - if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: - possibleCause = get_possible_error_cause(sw) - self.close() - raise CommException("Invalid status %04x (%s)" % (sw, possibleCause), sw, response) - return response - - def apduMaxDataSize(self): - return 0x99 - - def close(self): - self.device.close() + def __init__(self, debug=False): + self.waitImpl = self + self.debug = debug + try: + self.device = BleDevice(os.environ["LEDGER_BLE_MAC"]) + self.device.open() + except KeyError as ex: + sys.exit( + f"Key Error\nPlease run 'python -m ledgerblue.BleComm' to select wich device to connect to" + ) + self.opened = self.device.opened + + def exchange(self, apdu, timeout=TIMEOUT): + if self.debug: + print(f"[BLE] => {apdu.hex()}") + response = self.device.exchange(apdu, timeout) + sw = (response[-2] << 8) + response[-1] + response = response[0:-2] + if self.debug: + print("[BLE] <= %s%.2x" % (response.hex(), sw)) + if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: + possibleCause = get_possible_error_cause(sw) + self.close() + raise CommException( + "Invalid status %04x (%s)" % (sw, possibleCause), sw, response + ) + return response + + def apduMaxDataSize(self): + return 0x99 + + def close(self): + self.device.close() + class DongleSmartcard(Dongle): + def __init__(self, device, debug=False): + self.device = device + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=TIMEOUT): + if self.debug: + print("SC => %s" % apdu.hex()) + response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) + sw = (sw1 << 8) | sw2 + if self.debug: + print("SC <= %s%.2x" % (response.hex(), sw)) + if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: + raise CommException("Invalid status %04x" % sw, sw, bytearray(response)) + return bytearray(response) + + def close(self): + if self.opened: + try: + self.device.disconnect() + except: + pass + self.opened = False - def __init__(self, device, debug=False): - self.device = device - self.debug = debug - self.waitImpl = self - self.opened = True - - def exchange(self, apdu, timeout=TIMEOUT): - if self.debug: - print("SC => %s" % apdu.hex()) - response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) - sw = (sw1 << 8) | sw2 - if self.debug: - print("SC <= %s%.2x" % (response.hex(), sw)) - if sw != 0x9000 and (sw & 0xFF00) != 0x6100 and (sw & 0xFF00) != 0x6C00: - raise CommException("Invalid status %04x" % sw, sw, bytearray(response)) - return bytearray(response) - - def close(self): - if self.opened: - try: - self.device.disconnect() - except: - pass - self.opened = False def getDongle(debug=False, selectCommand=None): - if APDUGEN: - return HIDDongleHIDAPI(None, True, debug) - - if not U2FKEY is None: - return getDongleU2F(scrambleKey=U2FKEY, debug=debug) - elif MCUPROXY is not None: - return getDongleHTTP(remote_host=MCUPROXY, debug=debug) - elif TCP_PROXY is not None: - return getDongleTCP(server=TCP_PROXY[0], port=TCP_PROXY[1], debug=debug) - elif NFC_PROXY: - return DongleNFC(debug) - elif BLE_PROXY: - return DongleBLE(debug) - dev = None - hidDevicePath = None - ledger = True - for hidDevice in hid.enumerate(0, 0): - if hidDevice['vendor_id'] == 0x2c97: - if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0): - hidDevicePath = hidDevice['path'] - if hidDevicePath is not None: - dev = hid.device() - dev.open_path(hidDevicePath) - dev.set_nonblocking(True) - return HIDDongleHIDAPI(dev, ledger, debug) - if PCSC: - connection = None - for reader in readers(): - try: - connection = reader.createConnection() - connection.connect() - if selectCommand != None: - response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031")) - sw = (sw1 << 8) | sw2 - if sw == 0x9000: - break - else: - connection.disconnect() - connection = None - else: - break - except: - connection = None - pass - if connection is not None: - return DongleSmartcard(connection, debug) - raise CommException("No dongle found") + if APDUGEN: + return HIDDongleHIDAPI(None, True, debug) + + if not U2FKEY is None: + return getDongleU2F(scrambleKey=U2FKEY, debug=debug) + elif MCUPROXY is not None: + return getDongleHTTP(remote_host=MCUPROXY, debug=debug) + elif TCP_PROXY is not None: + return getDongleTCP(server=TCP_PROXY[0], port=TCP_PROXY[1], debug=debug) + elif NFC_PROXY: + return DongleNFC(debug) + elif BLE_PROXY: + return DongleBLE(debug) + dev = None + hidDevicePath = None + ledger = True + for hidDevice in hid.enumerate(0, 0): + if hidDevice["vendor_id"] == 0x2C97: + if ( + "interface_number" in hidDevice and hidDevice["interface_number"] == 0 + ) or ("usage_page" in hidDevice and hidDevice["usage_page"] == 0xFFA0): + hidDevicePath = hidDevice["path"] + if hidDevicePath is not None: + dev = hid.device() + dev.open_path(hidDevicePath) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, debug) + if PCSC: + connection = None + for reader in readers(): + try: + connection = reader.createConnection() + connection.connect() + if selectCommand != None: + response, sw1, sw2 = connection.transmit( + toBytes("00A4040010FF4C4547522E57414C5430312E493031") + ) + sw = (sw1 << 8) | sw2 + if sw == 0x9000: + break + else: + connection.disconnect() + connection = None + else: + break + except: + connection = None + pass + if connection is not None: + return DongleSmartcard(connection, debug) + raise CommException("No dongle found") diff --git a/ledgerblue/commException.py b/ledgerblue/commException.py index 6f93558..b3b4fbb 100644 --- a/ledgerblue/commException.py +++ b/ledgerblue/commException.py @@ -17,13 +17,13 @@ ******************************************************************************** """ -class CommException(Exception): - def __init__(self, message, sw=0x6f00, data=None): - self.message = message - self.sw = sw - self.data = data +class CommException(Exception): + def __init__(self, message, sw=0x6F00, data=None): + self.message = message + self.sw = sw + self.data = data - def __str__(self): - buf = "Exception : " + self.message - return buf + def __str__(self): + buf = "Exception : " + self.message + return buf diff --git a/ledgerblue/commHTTP.py b/ledgerblue/commHTTP.py index 3661990..f81031c 100644 --- a/ledgerblue/commHTTP.py +++ b/ledgerblue/commHTTP.py @@ -21,19 +21,20 @@ import requests -class HTTPProxy(object): +class HTTPProxy(object): def __init__(self, remote_host="localhost:8081", debug=False): self.remote_host = "http://" + remote_host self.debug = debug - def exchange(self, apdu): if self.debug: print("=> %s" % apdu.hex()) try: - ret = requests.post(self.remote_host + "/send_apdu", params={"data": apdu.hex()}) + ret = requests.post( + self.remote_host + "/send_apdu", params={"data": apdu.hex()} + ) while True: ret = requests.post(self.remote_host + "/fetch_apdu") @@ -43,24 +44,23 @@ def exchange(self, apdu): else: time.sleep(0.1) - return bytearray(str(ret.text).decode("hex")) except Exception as e: print(e) - - def exchange_seph_event(self, event): if self.debug >= 3: print("=> %s" % event.hex()) try: - ret = requests.post(self.remote_host + "/send_seph_event", params={"data": event.encode("hex")}) + ret = requests.post( + self.remote_host + "/send_seph_event", + params={"data": event.encode("hex")}, + ) return ret.text except Exception as e: print(e) - def poll_status(self): if self.debug >= 5: print("=> Waiting for a status") @@ -77,7 +77,6 @@ def poll_status(self): except Exception as e: print(e) - def reset(self): if self.debug: print("=> Reset") @@ -87,5 +86,6 @@ def reset(self): except Exception as e: print(e) + def getDongle(remote_host="localhost", debug=False): return HTTPProxy(remote_host, debug) diff --git a/ledgerblue/commTCP.py b/ledgerblue/commTCP.py index 7586a6d..5abe9ba 100644 --- a/ledgerblue/commTCP.py +++ b/ledgerblue/commTCP.py @@ -22,69 +22,69 @@ import socket import struct -class DongleServer(object): - def __init__(self, server, port, debug=False): - self.server = server - self.port = port - self.debug = debug - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.opened = True - try: - self.socket.connect((self.server, self.port)) - except: - raise CommException("Proxy connection failed") - def exchange(self, apdu, timeout=20000): +class DongleServer(object): + def __init__(self, server, port, debug=False): + self.server = server + self.port = port + self.debug = debug + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.opened = True + try: + self.socket.connect((self.server, self.port)) + except: + raise CommException("Proxy connection failed") - def send_apdu(apdu): - if self.debug: - print("=> %s" % hexlify(apdu)) - self.socket.send(struct.pack(">I", len(apdu))) - self.socket.send(apdu) + def exchange(self, apdu, timeout=20000): + def send_apdu(apdu): + if self.debug: + print("=> %s" % hexlify(apdu)) + self.socket.send(struct.pack(">I", len(apdu))) + self.socket.send(apdu) - def get_data(): - size = struct.unpack(">I", self.socket.recv(4))[0] - response = self.socket.recv(size) - sw = struct.unpack(">H", self.socket.recv(2))[0] - if self.debug: - print("<= %s%.2x" % (hexlify(response), sw)) - return sw, response + def get_data(): + size = struct.unpack(">I", self.socket.recv(4))[0] + response = self.socket.recv(size) + sw = struct.unpack(">H", self.socket.recv(2))[0] + if self.debug: + print("<= %s%.2x" % (hexlify(response), sw)) + return sw, response + send_apdu(apdu) + (sw, response) = get_data() + if sw == 0x9000: + return bytearray(response) + else: + # handle the get response case: + # When more data is available, the chip sends 0x61XX + # So 0x61xx as a SW must not be interpreted as an error + if (sw & 0xFF00) != 0x6100: + raise CommException("Invalid status %04x" % sw, sw, data=response) + else: + while (sw & 0xFF00) == 0x6100: + send_apdu(bytes.fromhex("00c0000000")) # GET RESPONSE + (sw, data) = get_data() + response += data - send_apdu(apdu) - (sw, response) = get_data() - if sw == 0x9000: - return bytearray(response) - else: - # handle the get response case: - # When more data is available, the chip sends 0x61XX - # So 0x61xx as a SW must not be interpreted as an error - if (sw & 0xFF00) != 0x6100: - raise CommException("Invalid status %04x" % sw, sw, data=response) - else: - while (sw & 0xFF00) == 0x6100: - send_apdu(bytes.fromhex("00c0000000")) # GET RESPONSE - (sw, data) = get_data() - response += data + # Check that the last received SW is indeed 0x9000 + if sw == 0x9000: + return bytearray(response) - # Check that the last received SW is indeed 0x9000 - if sw == 0x9000: - return bytearray(response) + # In any other case return an exception + raise CommException("Invalid status %04x" % sw, sw, data=response) - # In any other case return an exception - raise CommException("Invalid status %04x" % sw, sw, data=response) + def apduMaxDataSize(self): + return 240 - def apduMaxDataSize(self): - return 240 + def close(self): + try: + self.socket.shutdown(socket.SHUT_RD) + self.socket.close() + self.socket = None + except: + pass + self.opened = False - def close(self): - try: - self.socket.shutdown(socket.SHUT_RD) - self.socket.close() - self.socket = None - except: - pass - self.opened = False def getDongle(server="127.0.0.1", port=9999, debug=False): return DongleServer(server, port, debug) diff --git a/ledgerblue/commU2F.py b/ledgerblue/commU2F.py index 3d69ad7..c5561e9 100644 --- a/ledgerblue/commU2F.py +++ b/ledgerblue/commU2F.py @@ -57,7 +57,7 @@ from .commException import CommException -TIMEOUT=30000 +TIMEOUT = 30000 DEVICES = [ (0x1050, 0x0200), # Gnubby @@ -71,14 +71,14 @@ (0x1050, 0x0403), # YubiKey 4 OTP+U2F (0x1050, 0x0406), # YubiKey 4 U2F+CCID (0x1050, 0x0407), # YubiKey 4 OTP+U2F+CCID - (0x2581, 0xf1d0), # Plug-Up U2F Security Key - (0x2581, 0xf1d1), # Ledger Production U2F Dongle - (0x2c97, 0x0000), # Ledger Blue - (0x2c97, 0x0001), # Ledger Nano S - (0x2c97, 0x0002), # Ledger Aramis - (0x2c97, 0x0003), # Ledger HW2 - (0x2c97, 0x0004), # Ledger Blend - (0x2c97, 0xf1d0), # Plug-Up U2F Security Key + (0x2581, 0xF1D0), # Plug-Up U2F Security Key + (0x2581, 0xF1D1), # Ledger Production U2F Dongle + (0x2C97, 0x0000), # Ledger Blue + (0x2C97, 0x0001), # Ledger Nano S + (0x2C97, 0x0002), # Ledger Aramis + (0x2C97, 0x0003), # Ledger HW2 + (0x2C97, 0x0004), # Ledger Blend + (0x2C97, 0xF1D0), # Plug-Up U2F Security Key ] HID_RPT_SIZE = 64 @@ -90,11 +90,12 @@ CMD_APDU = 0x03 U2FHID_YUBIKEY_DEVICE_CONFIG = U2F_VENDOR_FIRST -STAT_ERR = 0xbf +STAT_ERR = 0xBF + def _read_timeout(dev, size, timeout=TIMEOUT): if timeout > 0: - timeout += time.time() + timeout += time.time() while timeout == 0 or time.time() < timeout: resp = dev.read(size) if resp: @@ -102,6 +103,7 @@ def _read_timeout(dev, size, timeout=TIMEOUT): time.sleep(0.01) return [] + class U2FHIDError(Exception): def __init__(self, code): super(Exception, self).__init__("U2FHIDError: 0x%02x" % code) @@ -109,7 +111,6 @@ def __init__(self, code): class HIDDevice(U2FDevice): - """ U2FDevice implementation using the HID transport. """ @@ -125,7 +126,7 @@ def open(self): self.init() def close(self): - if hasattr(self, 'handle'): + if hasattr(self, "handle"): self.handle.close() del self.handle @@ -149,30 +150,31 @@ def wink(self): def _send_req(self, cid, cmd, data): size = len(data) - bc_l = int2byte(size & 0xff) - bc_h = int2byte(size >> 8 & 0xff) - payload = cid + int2byte(TYPE_INIT | cmd) + bc_h + bc_l + \ - data[:HID_RPT_SIZE - 7] - payload += b'\0' * (HID_RPT_SIZE - len(payload)) + bc_l = int2byte(size & 0xFF) + bc_h = int2byte(size >> 8 & 0xFF) + payload = ( + cid + int2byte(TYPE_INIT | cmd) + bc_h + bc_l + data[: HID_RPT_SIZE - 7] + ) + payload += b"\0" * (HID_RPT_SIZE - len(payload)) if self.handle.write([0] + [byte2int(c) for c in payload]) < 0: - raise exc.DeviceError("Cannot write to device!") - data = data[HID_RPT_SIZE - 7:] + raise exc.DeviceError("Cannot write to device!") + data = data[HID_RPT_SIZE - 7 :] seq = 0 while len(data) > 0: - payload = cid + int2byte(0x7f & seq) + data[:HID_RPT_SIZE - 5] - payload += b'\0' * (HID_RPT_SIZE - len(payload)) + payload = cid + int2byte(0x7F & seq) + data[: HID_RPT_SIZE - 5] + payload += b"\0" * (HID_RPT_SIZE - len(payload)) if self.handle.write([0] + [byte2int(c) for c in payload]) < 0: - raise exc.DeviceError("Cannot write to device!") - data = data[HID_RPT_SIZE - 5:] + raise exc.DeviceError("Cannot write to device!") + data = data[HID_RPT_SIZE - 5 :] seq += 1 def _read_resp(self, cid, cmd): - resp = b'.' + resp = b"." header = cid + int2byte(TYPE_INIT | cmd) while resp and resp[:5] != header: # allow for timeout resp_vals = _read_timeout(self.handle, HID_RPT_SIZE) - resp = b''.join(int2byte(v) for v in resp_vals) + resp = b"".join(int2byte(v) for v in resp_vals) if resp[:5] == cid + int2byte(STAT_ERR): raise U2FHIDError(byte2int(resp[7])) @@ -180,147 +182,164 @@ def _read_resp(self, cid, cmd): raise exc.DeviceError("Invalid response from device!") data_len = (byte2int(resp[5]) << 8) + byte2int(resp[6]) - data = resp[7:min(7 + data_len, HID_RPT_SIZE)] + data = resp[7 : min(7 + data_len, HID_RPT_SIZE)] data_len -= len(data) seq = 0 while data_len > 0: resp_vals = _read_timeout(self.handle, HID_RPT_SIZE) - resp = b''.join(int2byte(v) for v in resp_vals) + resp = b"".join(int2byte(v) for v in resp_vals) if resp[:4] != cid: raise exc.DeviceError("Wrong CID from device!") - if resp[4] != (seq & 0x7f): - raise exc.DeviceError("Wrong SEQ from device! {} != {}".format(resp[4], seq)) + if resp[4] != (seq & 0x7F): + raise exc.DeviceError( + "Wrong SEQ from device! {} != {}".format(resp[4], seq) + ) seq += 1 - new_data = resp[5:min(5 + data_len, HID_RPT_SIZE)] + new_data = resp[5 : min(5 + data_len, HID_RPT_SIZE)] data_len -= len(new_data) data += new_data return data - def call(self, cmd, data=b''): + def call(self, cmd, data=b""): if isinstance(data, int): data = int2byte(data) self._send_req(self.cid, cmd, data) return self._read_resp(self.cid, cmd) + class U2FTunnelDongle(Dongle, DongleWait): + def __init__(self, device, scrambleKey="", ledger=False, debug=False): + self.device = device + self.scrambleKey = scrambleKey + self.ledger = ledger + self.debug = debug + self.waitImpl = self + self.opened = True + self.device.open() + + def exchange(self, apdu, timeout=TIMEOUT): + if self.debug: + print("U2F => %s" % apdu.hex()) + + if len(apdu) >= 256: + raise CommException("Too long APDU to transport") + + # wrap apdu + i = 0 + keyHandle = b"" + while i < len(apdu): + val = apdu[i : i + 1] + if len(self.scrambleKey) > 0: + val = b"" + int2byte( + ord(val) ^ ord(self.scrambleKey[i % len(self.scrambleKey)]) + ) + keyHandle += val + i += 1 + + client_param = sha256("u2f_tunnel".encode("utf8")).digest() + app_param = sha256("u2f_tunnel".encode("utf8")).digest() + + request = client_param + app_param + int2byte(len(keyHandle)) + keyHandle + + start = time.time() + while time.time() - start < timeout: + # p1 = 0x07 if check_only else 0x03 + p1 = 0x03 + p2 = 0 + try: + response = self.device.send_apdu(INS_SIGN, p1, p2, request) + except exc.APDUError as e: + if e.code == 0x6985: + time.sleep(0.25) + continue + raise e + + if self.debug: + print("U2F <= %s%.2x" % (response.hex(), 0x9000)) + + # check replied status words of the command (within the APDU tunnel) + if response[-2:] != b"\x90\x00": + raise CommException( + "Invalid status words received: " + response[-2:].hex() + ) + else: + break + + # api expect a byte array, remove the appended status words, remove the user presence and counter + return bytearray(response[5:-2]) + + def apduMaxDataSize(self): + return 256 - 5 + + def close(self): + self.device.close() + + def waitFirstResponse(self, timeout): + raise CommException("Invalid use") - def __init__(self, device, scrambleKey="", ledger=False, debug=False): - self.device = device - self.scrambleKey = scrambleKey - self.ledger = ledger - self.debug = debug - self.waitImpl = self - self.opened = True - self.device.open() - - def exchange(self, apdu, timeout=TIMEOUT): - if self.debug: - print("U2F => %s" % apdu.hex()) - - if len(apdu) >= 256: - raise CommException("Too long APDU to transport") - - # wrap apdu - i=0 - keyHandle = b'' - while i < len(apdu): - val = apdu[i:i+1] - if len(self.scrambleKey) > 0: - val = b'' + int2byte(ord(val) ^ ord(self.scrambleKey[i % len(self.scrambleKey)])) - keyHandle += val - i+=1 - - client_param = sha256("u2f_tunnel".encode('utf8')).digest() - app_param = sha256("u2f_tunnel".encode('utf8')).digest() - - request = client_param + app_param + int2byte(len(keyHandle)) + keyHandle - - start = time.time() - while time.time() - start < timeout: - - #p1 = 0x07 if check_only else 0x03 - p1 = 0x03 - p2 = 0 - try: - response = self.device.send_apdu(INS_SIGN, p1, p2, request) - except exc.APDUError as e: - if e.code == 0x6985: - time.sleep(0.25) - continue - raise e - - if self.debug: - print("U2F <= %s%.2x" % (response.hex(), 0x9000)) - - # check replied status words of the command (within the APDU tunnel) - if response[-2:] != b"\x90\x00": - raise CommException("Invalid status words received: " + response[-2:].hex()) - else: - break - - # api expect a byte array, remove the appended status words, remove the user presence and counter - return bytearray(response[5:-2]) - - def apduMaxDataSize(self): - return 256-5 - - def close(self): - self.device.close() - - def waitFirstResponse(self, timeout): - raise CommException("Invalid use") def getDongles(dev_class=None, scrambleKey="", debug=False): dev_class = dev_class or HIDDevice devices = [] for d in hid.enumerate(0, 0): - usage_page = d['usage_page'] - if usage_page == 0xf1d0 and d['usage'] == 1: - devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)) + usage_page = d["usage_page"] + if usage_page == 0xF1D0 and d["usage"] == 1: + devices.append( + U2FTunnelDongle(dev_class(d["path"]), scrambleKey, debug=debug) + ) # Usage page doesn't work on Linux # well known devices - elif (d['vendor_id'], d['product_id']) in DEVICES: - device = HIDDevice(d['path']) + elif (d["vendor_id"], d["product_id"]) in DEVICES: + device = HIDDevice(d["path"]) try: device.open() device.close() - devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)) + devices.append( + U2FTunnelDongle(dev_class(d["path"]), scrambleKey, debug=debug) + ) except (exc.DeviceError, IOError, OSError): pass # unknown devices else: - device = HIDDevice(d['path']) + device = HIDDevice(d["path"]) try: device.open() # try a ping command to ensure a FIDO device, else timeout (BEST here, modulate the timeout, 2 seconds is way too big) device.ping() device.close() - devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)) + devices.append( + U2FTunnelDongle(dev_class(d["path"]), scrambleKey, debug=debug) + ) except (exc.DeviceError, IOError, OSError): pass return devices + def getDongle(path=None, dev_class=None, scrambleKey="", debug=False): - # if path is none, then use the first device - dev_class = dev_class or HIDDevice - devices = [] - for d in hid.enumerate(0, 0): - if path is None or d['path'] == path: - usage_page = d['usage_page'] - if usage_page == 0xf1d0 and d['usage'] == 1: - return U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug) - # Usage page doesn't work on Linux - # well known devices - elif (d['vendor_id'], d['product_id']) in DEVICES and ('interface_number' not in d or d['interface_number'] == 1): - #print d - device = HIDDevice(d['path']) - try: - device.open() - device.close() - return U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug) - except (exc.DeviceError, IOError, OSError): - traceback.print_exc() - pass - raise CommException("No dongle found") + # if path is none, then use the first device + dev_class = dev_class or HIDDevice + devices = [] + for d in hid.enumerate(0, 0): + if path is None or d["path"] == path: + usage_page = d["usage_page"] + if usage_page == 0xF1D0 and d["usage"] == 1: + return U2FTunnelDongle(dev_class(d["path"]), scrambleKey, debug=debug) + # Usage page doesn't work on Linux + # well known devices + elif (d["vendor_id"], d["product_id"]) in DEVICES and ( + "interface_number" not in d or d["interface_number"] == 1 + ): + # print d + device = HIDDevice(d["path"]) + try: + device.open() + device.close() + return U2FTunnelDongle( + dev_class(d["path"]), scrambleKey, debug=debug + ) + except (exc.DeviceError, IOError, OSError): + traceback.print_exc() + pass + raise CommException("No dongle found") diff --git a/ledgerblue/deleteApp.py b/ledgerblue/deleteApp.py index 090feeb..785ab0b 100644 --- a/ledgerblue/deleteApp.py +++ b/ledgerblue/deleteApp.py @@ -19,79 +19,108 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Delete the app with the specified name.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--appName", help="The name of the application to delete", action='append') - parser.add_argument("--appHash", help="Set the application hash") - parser.add_argument("--rootPrivateKey", help="A private key used to establish a Secure Channel (hex encoded)") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true') - parser.add_argument("--offline", help="Request to only output application load APDUs into given filename") - return parser + parser = argparse.ArgumentParser( + description="Delete the app with the specified name." + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--appName", help="The name of the application to delete", action="append" + ) + parser.add_argument("--appHash", help="Set the application hash") + parser.add_argument( + "--rootPrivateKey", + help="A private key used to establish a Secure Channel (hex encoded)", + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--deployLegacy", help="Use legacy deployment API", action="store_true" + ) + parser.add_argument( + "--offline", + help="Request to only output application load APDUs into given filename", + ) + return parser + def auto_int(x): - return int(x, 0) - -if __name__ == '__main__': - from .ecWrapper import PrivateKey - from .comm import getDongle - from .deployed import getDeployedSecretV1, getDeployedSecretV2 - from .hexLoader import HexLoader - import binascii - - args = get_argparser().parse_args() - - if (args.appName == None or len(args.appName) == 0) and args.appHash == None: - raise Exception("Missing appName or appHash") - - if args.appName != None and len(args.appName) > 0: - for i in range(0, len(args.appName)): - args.appName[i] = bytes(args.appName[i], 'ascii') - - if args.appHash != None: - args.appHash = bytes(args.appHash,'ascii') - args.appHash = bytearray.fromhex(args.appHash) - - if args.rootPrivateKey == None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() - - dongle = None - secret = None - if not args.offline: - dongle = getDongle(args.apdu) - - if args.deployLegacy: - secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - fileTarget = open(args.offline, "wb") - class FileCard: - def __init__(self, target): - self.target = target - def exchange(self, apdu): - if args.apdu: - print(binascii.hexlify(apdu)) - apdu = binascii.hexlify(apdu) - self.target.write(apdu + '\n'.encode()) - return bytearray([]) - def apduMaxDataSize(self): - # ensure to allow for encryption of those apdu afterward - return 240 - def close(self): - self.target.close() - dongle = FileCard(fileTarget) - - loader = HexLoader(dongle, 0xe0, not args.offline, secret) - - if args.appName != None and len(args.appName) > 0: - for name in args.appName: - loader.deleteApp(name) - if args.appHash != None: - loader.deleteAppByHash(args.appHash) - - dongle.close() \ No newline at end of file + return int(x, 0) + + +if __name__ == "__main__": + from .ecWrapper import PrivateKey + from .comm import getDongle + from .deployed import getDeployedSecretV1, getDeployedSecretV2 + from .hexLoader import HexLoader + import binascii + + args = get_argparser().parse_args() + + if (args.appName == None or len(args.appName) == 0) and args.appHash == None: + raise Exception("Missing appName or appHash") + + if args.appName != None and len(args.appName) > 0: + for i in range(0, len(args.appName)): + args.appName[i] = bytes(args.appName[i], "ascii") + + if args.appHash != None: + args.appHash = bytes(args.appHash, "ascii") + args.appHash = bytearray.fromhex(args.appHash) + + if args.rootPrivateKey == None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() + + dongle = None + secret = None + if not args.offline: + dongle = getDongle(args.apdu) + + if args.deployLegacy: + secret = getDeployedSecretV1( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + fileTarget = open(args.offline, "wb") + + class FileCard: + def __init__(self, target): + self.target = target + + def exchange(self, apdu): + if args.apdu: + print(binascii.hexlify(apdu)) + apdu = binascii.hexlify(apdu) + self.target.write(apdu + "\n".encode()) + return bytearray([]) + + def apduMaxDataSize(self): + # ensure to allow for encryption of those apdu afterward + return 240 + + def close(self): + self.target.close() + + dongle = FileCard(fileTarget) + + loader = HexLoader(dongle, 0xE0, not args.offline, secret) + + if args.appName != None and len(args.appName) > 0: + for name in args.appName: + loader.deleteApp(name) + if args.appHash != None: + loader.deleteAppByHash(args.appHash) + + dongle.close() diff --git a/ledgerblue/deployed.py b/ledgerblue/deployed.py index cb609cf..0f64f5b 100644 --- a/ledgerblue/deployed.py +++ b/ledgerblue/deployed.py @@ -22,152 +22,197 @@ import struct import binascii + def getDeployedSecretV1(dongle, masterPrivate, targetId): - testMaster = PrivateKey(bytes(masterPrivate)) - testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) - targetid = bytearray(struct.pack('>I', targetId)) - - if targetId&0xF != 0x1: - raise BaseException("Target ID does not support SCP V1") - - # identify - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid - dongle.exchange(apdu) - - # walk the chain - batch_info = bytearray(dongle.exchange(bytearray.fromhex('E050000000'))) - cardKey = batch_info[5:5 + batch_info[4]] - - # if not found, get another pair - #if cardKey != testMasterPublic: - # raise Exception("Invalid batch public key") - - # provide the ephemeral certificate - ephemeralPrivate = PrivateKey() - ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) - print("Using ephemeral key %s" %binascii.hexlify(ephemeralPublic)) - signature = testMaster.ecdsa_sign(bytes(ephemeralPublic)) - signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # walk the device certificates to retrieve the public key to use for authentication - index = 0 - last_pub_key = PublicKey(bytes(testMasterPublic), raw=True) - while True: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) - if len(certificate) == 0: - break - certificatePublic = certificate[1 : 1 + certificate[0]] - certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificate[2 + certificate[0] :])) - if not last_pub_key.ecdsa_verify(bytes(certificatePublic), certificateSignature): - if index == 0: - # Not an error if loading from user key - print("Broken certificate chain - loading from user key") - else: - raise Exception("Broken certificate chain") - last_pub_key = PublicKey(bytes(certificatePublic), raw=True) - index = index + 1 - - # Commit device ECDH channel - dongle.exchange(bytearray.fromhex('E053000000')) - secret = last_pub_key.ecdh(bytes(ephemeralPrivate.serialize().decode('hex'))) - return secret[0:16] - -def getDeployedSecretV2(dongle, masterPrivate, targetId, signerCertChain=None, ecdh_secret_format=None): - testMaster = PrivateKey(bytes(masterPrivate)) - testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) - targetid = bytearray(struct.pack('>I', targetId)) - - if targetId&0xF < 2: - raise BaseException("Target ID does not support SCP V2") - - # identify - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid - dongle.exchange(apdu) - - # walk the chain - nonce = os.urandom(8) - apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce - auth_info = dongle.exchange(apdu) - batch_signer_serial = auth_info[0:4] - deviceNonce = auth_info[4:12] - - # if not found, get another pair - #if cardKey != testMasterPublic: - # raise Exception("Invalid batch public key") - - if signerCertChain: - for cert in signerCertChain: - apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(cert)]) + cert - dongle.exchange(apdu) - else: - print("Using test master key %s " % binascii.hexlify(testMasterPublic)) - dataToSign = bytes(bytearray([0x01]) + testMasterPublic) - signature = testMaster.ecdsa_sign(bytes(dataToSign)) - signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # provide the ephemeral certificate - ephemeralPrivate = PrivateKey() - ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) - print("Using ephemeral key %s" %binascii.hexlify(ephemeralPublic)) - dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic) - signature = testMaster.ecdsa_sign(bytes(dataToSign)) - signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # walk the device certificates to retrieve the public key to use for authentication - index = 0 - last_dev_pub_key = PublicKey(bytes(testMasterPublic), raw=True) - devicePublicKey = None - while True: - if index == 0: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) - elif index == 1: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) - else: - break - if len(certificate) == 0: - break - offset = 1 - certificateHeader = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificatePublicKey = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificateSignatureArray = certificate[offset : offset + certificate[offset-1]] - certificateSignature = last_dev_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray)) - # first cert contains a header field which holds the certificate's public key role - if index == 0: - devicePublicKey = certificatePublicKey - certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey - # Could check if the device certificate is signed by the issuer public key - # ephemeral key certificate - else: - certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey - if not last_dev_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature): - if index == 0: - # Not an error if loading from user key - print("Broken certificate chain - loading from user key") - else: - raise Exception("Broken certificate chain") - last_dev_pub_key = PublicKey(bytes(certificatePublicKey), raw=True) - index = index + 1 - - # Commit device ECDH channel - dongle.exchange(bytearray.fromhex('E053000000')) - secret = last_dev_pub_key.ecdh(binascii.unhexlify(ephemeralPrivate.serialize())) - - #forced to specific version - if ecdh_secret_format==1 or targetId&0xF == 0x2: - return secret[0:16] - elif targetId&0xF >= 0x3: - ret = {} - ret['ecdh_secret'] = secret - ret['devicePublicKey'] = devicePublicKey - return ret + testMaster = PrivateKey(bytes(masterPrivate)) + testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) + targetid = bytearray(struct.pack(">I", targetId)) + + if targetId & 0xF != 0x1: + raise BaseException("Target ID does not support SCP V1") + + # identify + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + dongle.exchange(apdu) + + # walk the chain + batch_info = bytearray(dongle.exchange(bytearray.fromhex("E050000000"))) + cardKey = batch_info[5 : 5 + batch_info[4]] + + # if not found, get another pair + # if cardKey != testMasterPublic: + # raise Exception("Invalid batch public key") + + # provide the ephemeral certificate + ephemeralPrivate = PrivateKey() + ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) + print("Using ephemeral key %s" % binascii.hexlify(ephemeralPublic)) + signature = testMaster.ecdsa_sign(bytes(ephemeralPublic)) + signature = testMaster.ecdsa_serialize(signature) + certificate = ( + bytearray([len(ephemeralPublic)]) + + ephemeralPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x00, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # walk the device certificates to retrieve the public key to use for authentication + index = 0 + last_pub_key = PublicKey(bytes(testMasterPublic), raw=True) + while True: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) + if len(certificate) == 0: + break + certificatePublic = certificate[1 : 1 + certificate[0]] + certificateSignature = last_pub_key.ecdsa_deserialize( + bytes(certificate[2 + certificate[0] :]) + ) + if not last_pub_key.ecdsa_verify( + bytes(certificatePublic), certificateSignature + ): + if index == 0: + # Not an error if loading from user key + print("Broken certificate chain - loading from user key") + else: + raise Exception("Broken certificate chain") + last_pub_key = PublicKey(bytes(certificatePublic), raw=True) + index = index + 1 + + # Commit device ECDH channel + dongle.exchange(bytearray.fromhex("E053000000")) + secret = last_pub_key.ecdh(bytes(ephemeralPrivate.serialize().decode("hex"))) + return secret[0:16] + + +def getDeployedSecretV2( + dongle, masterPrivate, targetId, signerCertChain=None, ecdh_secret_format=None +): + testMaster = PrivateKey(bytes(masterPrivate)) + testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) + targetid = bytearray(struct.pack(">I", targetId)) + + if targetId & 0xF < 2: + raise BaseException("Target ID does not support SCP V2") + + # identify + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + dongle.exchange(apdu) + + # walk the chain + nonce = os.urandom(8) + apdu = bytearray([0xE0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce + auth_info = dongle.exchange(apdu) + batch_signer_serial = auth_info[0:4] + deviceNonce = auth_info[4:12] + + # if not found, get another pair + # if cardKey != testMasterPublic: + # raise Exception("Invalid batch public key") + + if signerCertChain: + for cert in signerCertChain: + apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(cert)]) + cert + dongle.exchange(apdu) + else: + print("Using test master key %s " % binascii.hexlify(testMasterPublic)) + dataToSign = bytes(bytearray([0x01]) + testMasterPublic) + signature = testMaster.ecdsa_sign(bytes(dataToSign)) + signature = testMaster.ecdsa_serialize(signature) + certificate = ( + bytearray([len(testMasterPublic)]) + + testMasterPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x00, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # provide the ephemeral certificate + ephemeralPrivate = PrivateKey() + ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) + print("Using ephemeral key %s" % binascii.hexlify(ephemeralPublic)) + dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic) + signature = testMaster.ecdsa_sign(bytes(dataToSign)) + signature = testMaster.ecdsa_serialize(signature) + certificate = ( + bytearray([len(ephemeralPublic)]) + + ephemeralPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # walk the device certificates to retrieve the public key to use for authentication + index = 0 + last_dev_pub_key = PublicKey(bytes(testMasterPublic), raw=True) + devicePublicKey = None + while True: + if index == 0: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) + elif index == 1: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) + else: + break + if len(certificate) == 0: + break + offset = 1 + certificateHeader = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificatePublicKey = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificateSignatureArray = certificate[ + offset : offset + certificate[offset - 1] + ] + certificateSignature = last_dev_pub_key.ecdsa_deserialize( + bytes(certificateSignatureArray) + ) + # first cert contains a header field which holds the certificate's public key role + if index == 0: + devicePublicKey = certificatePublicKey + certificateSignedData = ( + bytearray([0x02]) + certificateHeader + certificatePublicKey + ) + # Could check if the device certificate is signed by the issuer public key + # ephemeral key certificate + else: + certificateSignedData = ( + bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey + ) + if not last_dev_pub_key.ecdsa_verify( + bytes(certificateSignedData), certificateSignature + ): + if index == 0: + # Not an error if loading from user key + print("Broken certificate chain - loading from user key") + else: + raise Exception("Broken certificate chain") + last_dev_pub_key = PublicKey(bytes(certificatePublicKey), raw=True) + index = index + 1 + + # Commit device ECDH channel + dongle.exchange(bytearray.fromhex("E053000000")) + secret = last_dev_pub_key.ecdh(binascii.unhexlify(ephemeralPrivate.serialize())) + + # forced to specific version + if ecdh_secret_format == 1 or targetId & 0xF == 0x2: + return secret[0:16] + elif targetId & 0xF >= 0x3: + ret = {} + ret["ecdh_secret"] = secret + ret["devicePublicKey"] = devicePublicKey + return ret diff --git a/ledgerblue/derivePassphrase.py b/ledgerblue/derivePassphrase.py index f1510f7..7ba9fb7 100644 --- a/ledgerblue/derivePassphrase.py +++ b/ledgerblue/derivePassphrase.py @@ -19,35 +19,46 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Set a BIP 39 passphrase on the device.") - parser.add_argument("--persistent", help="""Persist passphrase as secondary PIN (otherwise, it's set as a temporary -passphrase)""", action='store_true') - return parser + parser = argparse.ArgumentParser( + description="Set a BIP 39 passphrase on the device." + ) + parser.add_argument( + "--persistent", + help="""Persist passphrase as secondary PIN (otherwise, it's set as a temporary +passphrase)""", + action="store_true", + ) + return parser + def auto_int(x): - return int(x, 0) - -if __name__ == '__main__': - from .comm import getDongle - import getpass - import unicodedata - import sys - - args = get_argparser().parse_args() - - dongle = getDongle(False) - - passphrase = getpass.getpass("Enter BIP39 passphrase : ") - if isinstance(passphrase, bytes): - passphrase = passphrase.decode(sys.stdin.encoding) - if len(passphrase) != 0: - if args.persistent: - p1 = 0x02 - else: - p1 = 0x01 - passphrase = unicodedata.normalize('NFKD', passphrase) - apdu = bytearray([0xE0, 0xD0, p1, 0x00, len(passphrase)]) + bytearray(passphrase, 'utf8') - dongle.exchange(apdu, timeout=300) - - dongle.close() + return int(x, 0) + + +if __name__ == "__main__": + from .comm import getDongle + import getpass + import unicodedata + import sys + + args = get_argparser().parse_args() + + dongle = getDongle(False) + + passphrase = getpass.getpass("Enter BIP39 passphrase : ") + if isinstance(passphrase, bytes): + passphrase = passphrase.decode(sys.stdin.encoding) + if len(passphrase) != 0: + if args.persistent: + p1 = 0x02 + else: + p1 = 0x01 + passphrase = unicodedata.normalize("NFKD", passphrase) + apdu = bytearray([0xE0, 0xD0, p1, 0x00, len(passphrase)]) + bytearray( + passphrase, "utf8" + ) + dongle.exchange(apdu, timeout=300) + + dongle.close() diff --git a/ledgerblue/ecWrapper.py b/ledgerblue/ecWrapper.py index e795404..cb0f951 100644 --- a/ledgerblue/ecWrapper.py +++ b/ledgerblue/ecWrapper.py @@ -18,130 +18,134 @@ """ import hashlib + try: - import secp256k1 - USE_SECP = secp256k1.HAS_ECDH + import secp256k1 + + USE_SECP = secp256k1.HAS_ECDH except (ImportError, AttributeError): - USE_SECP = False + USE_SECP = False if not USE_SECP: - import ecpy - from builtins import int - from ecpy.curves import Curve, Point - from ecpy.keys import ECPublicKey, ECPrivateKey - from ecpy.ecdsa import ECDSA - CURVE_SECP256K1 = Curve.get_curve('secp256k1') - SIGNER = ECDSA() + import ecpy + from builtins import int + from ecpy.curves import Curve, Point + from ecpy.keys import ECPublicKey, ECPrivateKey + from ecpy.ecdsa import ECDSA + + CURVE_SECP256K1 = Curve.get_curve("secp256k1") + SIGNER = ECDSA() + class PublicKey(object): - def __init__(self, pubkey=None, raw=False): - if USE_SECP: - self.obj = secp256k1.PublicKey(pubkey, raw) - else: - if not raw: - raise Exception("Non raw init unsupported") - pubkey = pubkey[1:] - x = int.from_bytes(pubkey[0:32], 'big') - y = int.from_bytes(pubkey[32:], 'big') - self.obj = ECPublicKey(Point(x, y, CURVE_SECP256K1)) - - def ecdsa_deserialize(self, ser_sig): - if USE_SECP: - return self.obj.ecdsa_deserialize(ser_sig) - else: - return ser_sig - - def serialize(self, compressed=True): - if USE_SECP: - return self.obj.serialize(compressed) - else: - if not compressed: - out = b"\x04" - out += self.obj.W.x.to_bytes(32, 'big') - out += self.obj.W.y.to_bytes(32, 'big') - else: - out = b"\x03" if ((self.obj.W.y & 1) != 0) else "\x02" - out += self.obj.W.x.to_bytes(32, 'big') - return out - - def ecdh(self, scalar, scpv3=False): - if USE_SECP: - return self.obj.ecdh(scalar) - else: - scalar = int.from_bytes(scalar, 'big') - point = self.obj.W * scalar - if not scpv3: - # libsecp256k1 style secret - out = b"\x03" if ((point.y & 1) != 0) else b"\x02" - out += point.x.to_bytes(32, 'big') - else: - out = point.x.to_bytes(32, 'big') - out += b"\x00\x00\x00\x01" - hash = hashlib.sha256() - hash.update(out) - return hash.digest() - - def tweak_add(self, scalar): - if USE_SECP: - self.obj = self.obj.tweak_add(scalar) - else: - scalar = int.from_bytes(scalar, 'big') - privKey = ECPrivateKey(scalar, CURVE_SECP256K1) - self.obj = ECPublicKey(self.obj.W + privKey.get_public_key().W) - - def ecdsa_verify(self, msg, raw_sig, raw=False, digest=hashlib.sha256): - if USE_SECP: - return self.obj.ecdsa_verify(msg, raw_sig, raw, digest) - else: - if not raw: - h = digest() - h.update(msg) - msg = h.digest() - raw_sig = bytearray(raw_sig) - return SIGNER.verify(msg, raw_sig, self.obj) + def __init__(self, pubkey=None, raw=False): + if USE_SECP: + self.obj = secp256k1.PublicKey(pubkey, raw) + else: + if not raw: + raise Exception("Non raw init unsupported") + pubkey = pubkey[1:] + x = int.from_bytes(pubkey[0:32], "big") + y = int.from_bytes(pubkey[32:], "big") + self.obj = ECPublicKey(Point(x, y, CURVE_SECP256K1)) + + def ecdsa_deserialize(self, ser_sig): + if USE_SECP: + return self.obj.ecdsa_deserialize(ser_sig) + else: + return ser_sig + + def serialize(self, compressed=True): + if USE_SECP: + return self.obj.serialize(compressed) + else: + if not compressed: + out = b"\x04" + out += self.obj.W.x.to_bytes(32, "big") + out += self.obj.W.y.to_bytes(32, "big") + else: + out = b"\x03" if ((self.obj.W.y & 1) != 0) else "\x02" + out += self.obj.W.x.to_bytes(32, "big") + return out + + def ecdh(self, scalar, scpv3=False): + if USE_SECP: + return self.obj.ecdh(scalar) + else: + scalar = int.from_bytes(scalar, "big") + point = self.obj.W * scalar + if not scpv3: + # libsecp256k1 style secret + out = b"\x03" if ((point.y & 1) != 0) else b"\x02" + out += point.x.to_bytes(32, "big") + else: + out = point.x.to_bytes(32, "big") + out += b"\x00\x00\x00\x01" + hash = hashlib.sha256() + hash.update(out) + return hash.digest() + + def tweak_add(self, scalar): + if USE_SECP: + self.obj = self.obj.tweak_add(scalar) + else: + scalar = int.from_bytes(scalar, "big") + privKey = ECPrivateKey(scalar, CURVE_SECP256K1) + self.obj = ECPublicKey(self.obj.W + privKey.get_public_key().W) + + def ecdsa_verify(self, msg, raw_sig, raw=False, digest=hashlib.sha256): + if USE_SECP: + return self.obj.ecdsa_verify(msg, raw_sig, raw, digest) + else: + if not raw: + h = digest() + h.update(msg) + msg = h.digest() + raw_sig = bytearray(raw_sig) + return SIGNER.verify(msg, raw_sig, self.obj) + class PrivateKey(object): + def __init__(self, privkey=None, raw=True): + if USE_SECP: + self.obj = secp256k1.PrivateKey(privkey, raw) + self.pubkey = self.obj.pubkey + else: + if not raw: + raise Exception("Non raw init unsupported") + if privkey == None: + privkey = ecpy.ecrand.rnd(CURVE_SECP256K1.order) + else: + privkey = int.from_bytes(privkey, "big") + self.obj = ECPrivateKey(privkey, CURVE_SECP256K1) + pubkey = self.obj.get_public_key().W + out = b"\x04" + out += pubkey.x.to_bytes(32, "big") + out += pubkey.y.to_bytes(32, "big") + self.pubkey = PublicKey(out, raw=True) + + def serialize(self): + if USE_SECP: + return self.obj.serialize() + else: + return "%.64x" % self.obj.d + + def ecdsa_serialize(self, raw_sig): + if USE_SECP: + return self.obj.ecdsa_serialize(raw_sig) + else: + return raw_sig - def __init__(self, privkey=None, raw=True): - if USE_SECP: - self.obj = secp256k1.PrivateKey(privkey, raw) - self.pubkey = self.obj.pubkey - else: - if not raw: - raise Exception("Non raw init unsupported") - if privkey == None: - privkey = ecpy.ecrand.rnd(CURVE_SECP256K1.order) - else: - privkey = int.from_bytes(privkey,'big') - self.obj = ECPrivateKey(privkey, CURVE_SECP256K1) - pubkey = self.obj.get_public_key().W - out = b"\x04" - out += pubkey.x.to_bytes(32, 'big') - out += pubkey.y.to_bytes(32, 'big') - self.pubkey = PublicKey(out, raw=True) - - def serialize(self): - if USE_SECP: - return self.obj.serialize() - else: - return "%.64x"%self.obj.d - - def ecdsa_serialize(self, raw_sig): - if USE_SECP: - return self.obj.ecdsa_serialize(raw_sig) - else: - return raw_sig - - def ecdsa_sign(self, msg, raw=False, digest=hashlib.sha256, rfc6979=False): - if USE_SECP: - return self.obj.ecdsa_sign(msg, raw, digest) - else: - if not raw: - h = digest() - h.update(msg) - msg = h.digest() - if rfc6979: - signature = SIGNER.sign_rfc6979(msg, self.obj, digest, True) - else: - signature = SIGNER.sign(msg, self.obj, True) - return signature + def ecdsa_sign(self, msg, raw=False, digest=hashlib.sha256, rfc6979=False): + if USE_SECP: + return self.obj.ecdsa_sign(msg, raw, digest) + else: + if not raw: + h = digest() + h.update(msg) + msg = h.digest() + if rfc6979: + signature = SIGNER.sign_rfc6979(msg, self.obj, digest, True) + else: + signature = SIGNER.sign(msg, self.obj, True) + return signature diff --git a/ledgerblue/endorsementSetup.py b/ledgerblue/endorsementSetup.py index 8170486..749ea6b 100644 --- a/ledgerblue/endorsementSetup.py +++ b/ledgerblue/endorsementSetup.py @@ -23,143 +23,201 @@ def get_argparser(): - parser = argparse.ArgumentParser(description="""Generate an attestation keypair, using the provided Owner private -key to sign the Owner Certificate.""") - parser.add_argument("--key", help="Which endorsement scheme to use", type=auto_int, choices=(1, 2), required=True) - parser.add_argument("--certificate", help="""Optional certificate to store if finalizing the endorsement (hex -encoded), if no private key is specified""") - parser.add_argument("--privateKey", help="""Optional private key to use to create a test certificate (hex encoded), -if no certificate is specified""") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--issuerKey", help="Issuer key (hex encoded, default is batch 1)", default=DEFAULT_ISSUER_KEY) - parser.add_argument("--rootPrivateKey", help="SCP Host private key") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - return parser + parser = argparse.ArgumentParser( + description="""Generate an attestation keypair, using the provided Owner private +key to sign the Owner Certificate.""" + ) + parser.add_argument( + "--key", + help="Which endorsement scheme to use", + type=auto_int, + choices=(1, 2), + required=True, + ) + parser.add_argument( + "--certificate", + help="""Optional certificate to store if finalizing the endorsement (hex +encoded), if no private key is specified""", + ) + parser.add_argument( + "--privateKey", + help="""Optional private key to use to create a test certificate (hex encoded), +if no certificate is specified""", + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--issuerKey", + help="Issuer key (hex encoded, default is batch 1)", + default=DEFAULT_ISSUER_KEY, + ) + parser.add_argument("--rootPrivateKey", help="SCP Host private key") + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def getDeployedSecretV2(dongle, masterPrivate, targetid, issuerKey): - testMaster = PrivateKey(bytes(masterPrivate)) - testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) - targetid = bytearray(struct.pack('>I', targetid)) - - # identify - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid - dongle.exchange(apdu) - - # walk the chain - nonce = os.urandom(8) - apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce - auth_info = dongle.exchange(apdu) - batch_signer_serial = auth_info[0:4] - deviceNonce = auth_info[4:12] - - # if not found, get another pair - #if cardKey != testMasterPublic: - # raise Exception("Invalid batch public key") - - dataToSign = bytes(bytearray([0x01]) + testMasterPublic) - signature = testMaster.ecdsa_sign(bytes(dataToSign)) - signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # provide the ephemeral certificate - ephemeralPrivate = PrivateKey() - ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) - dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic) - signature = testMaster.ecdsa_sign(bytes(dataToSign)) - signature = testMaster.ecdsa_serialize(signature) - certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # walk the device certificates to retrieve the public key to use for authentication - index = 0 - last_pub_key = PublicKey(binascii.unhexlify(issuerKey), raw=True) - device_pub_key = None - while True: - if index == 0: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) - elif index == 1: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) - else: - break - if len(certificate) == 0: - break - offset = 1 - certificateHeader = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificatePublicKey = certificate[offset : offset + certificate[offset-1]] - offset += certificate[offset-1] + 1 - certificateSignatureArray = certificate[offset : offset + certificate[offset-1]] - certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray)) - # first cert contains a header field which holds the certificate's public key role - if index == 0: - certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey - # Could check if the device certificate is signed by the issuer public key - # ephemeral key certificate - else: - certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey - if not last_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature): - print("pub key not signed by last_pub_key") - return None - last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True) - if index == 0: - device_pub_key = last_pub_key - index = index + 1 - - return device_pub_key - -if __name__ == '__main__': - from .comm import getDongle - from .ecWrapper import PrivateKey, PublicKey - import hashlib - import struct - import os - import binascii - - args = get_argparser().parse_args() - - if (args.privateKey != None) and (args.certificate != None): - raise Exception("Cannot specify both certificate and privateKey") - - privateKey = PrivateKey() - publicKey = str(privateKey.pubkey.serialize(compressed=False)) - if not args.rootPrivateKey: - args.rootPrivateKey = privateKey.serialize() - else: - # parse the issuer key from the rootprivate key - issuerPrivKey = PrivateKey(bytes(bytearray.fromhex(args.rootPrivateKey))) - args.issuerKey = binascii.hexlify(issuerPrivKey.pubkey.serialize(compressed=False)).decode('utf-8') - - dongle = getDongle(args.apdu) - print(args.rootPrivateKey) - publicKey = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey) - - if args.certificate == None: - apdu = bytearray([0xe0, 0xC0, args.key, 0x00, 0x00]) - response = dongle.exchange(apdu) - print("Public key " + response[0:65].hex()) - m = hashlib.sha256() - m.update(bytes(b"\xff")) # Endorsement role - m.update(bytes(response[0:65])) - digest = m.digest() - signature = publicKey.ecdsa_deserialize(bytes(response[65:])) - if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): - raise Exception("Issuer certificate not verified") - if args.privateKey != None: - privateKey = PrivateKey(bytes(bytearray.fromhex(args.privateKey))) - dataToSign = bytes(bytearray([0xfe]) + response[0:65]) - signature = privateKey.ecdsa_sign(bytes(dataToSign)) - args.certificate = privateKey.ecdsa_serialize(signature).hex() - - if args.certificate != None: - certificate = bytearray.fromhex(args.certificate) - apdu = bytearray([0xe0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate - dongle.exchange(apdu) - print("Endorsement setup finalized") - - dongle.close() + testMaster = PrivateKey(bytes(masterPrivate)) + testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False)) + targetid = bytearray(struct.pack(">I", targetid)) + + # identify + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + dongle.exchange(apdu) + + # walk the chain + nonce = os.urandom(8) + apdu = bytearray([0xE0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce + auth_info = dongle.exchange(apdu) + batch_signer_serial = auth_info[0:4] + deviceNonce = auth_info[4:12] + + # if not found, get another pair + # if cardKey != testMasterPublic: + # raise Exception("Invalid batch public key") + + dataToSign = bytes(bytearray([0x01]) + testMasterPublic) + signature = testMaster.ecdsa_sign(bytes(dataToSign)) + signature = testMaster.ecdsa_serialize(signature) + certificate = ( + bytearray([len(testMasterPublic)]) + + testMasterPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x00, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # provide the ephemeral certificate + ephemeralPrivate = PrivateKey() + ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) + dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic) + signature = testMaster.ecdsa_sign(bytes(dataToSign)) + signature = testMaster.ecdsa_serialize(signature) + certificate = ( + bytearray([len(ephemeralPublic)]) + + ephemeralPublic + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # walk the device certificates to retrieve the public key to use for authentication + index = 0 + last_pub_key = PublicKey(binascii.unhexlify(issuerKey), raw=True) + device_pub_key = None + while True: + if index == 0: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) + elif index == 1: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) + else: + break + if len(certificate) == 0: + break + offset = 1 + certificateHeader = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificatePublicKey = certificate[offset : offset + certificate[offset - 1]] + offset += certificate[offset - 1] + 1 + certificateSignatureArray = certificate[ + offset : offset + certificate[offset - 1] + ] + certificateSignature = last_pub_key.ecdsa_deserialize( + bytes(certificateSignatureArray) + ) + # first cert contains a header field which holds the certificate's public key role + if index == 0: + certificateSignedData = ( + bytearray([0x02]) + certificateHeader + certificatePublicKey + ) + # Could check if the device certificate is signed by the issuer public key + # ephemeral key certificate + else: + certificateSignedData = ( + bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey + ) + if not last_pub_key.ecdsa_verify( + bytes(certificateSignedData), certificateSignature + ): + print("pub key not signed by last_pub_key") + return None + last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True) + if index == 0: + device_pub_key = last_pub_key + index = index + 1 + + return device_pub_key + + +if __name__ == "__main__": + from .comm import getDongle + from .ecWrapper import PrivateKey, PublicKey + import hashlib + import struct + import os + import binascii + + args = get_argparser().parse_args() + + if (args.privateKey != None) and (args.certificate != None): + raise Exception("Cannot specify both certificate and privateKey") + + privateKey = PrivateKey() + publicKey = str(privateKey.pubkey.serialize(compressed=False)) + if not args.rootPrivateKey: + args.rootPrivateKey = privateKey.serialize() + else: + # parse the issuer key from the rootprivate key + issuerPrivKey = PrivateKey(bytes(bytearray.fromhex(args.rootPrivateKey))) + args.issuerKey = binascii.hexlify( + issuerPrivKey.pubkey.serialize(compressed=False) + ).decode("utf-8") + + dongle = getDongle(args.apdu) + print(args.rootPrivateKey) + publicKey = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey + ) + + if args.certificate == None: + apdu = bytearray([0xE0, 0xC0, args.key, 0x00, 0x00]) + response = dongle.exchange(apdu) + print("Public key " + response[0:65].hex()) + m = hashlib.sha256() + m.update(bytes(b"\xff")) # Endorsement role + m.update(bytes(response[0:65])) + digest = m.digest() + signature = publicKey.ecdsa_deserialize(bytes(response[65:])) + if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): + raise Exception("Issuer certificate not verified") + if args.privateKey != None: + privateKey = PrivateKey(bytes(bytearray.fromhex(args.privateKey))) + dataToSign = bytes(bytearray([0xFE]) + response[0:65]) + signature = privateKey.ecdsa_sign(bytes(dataToSign)) + args.certificate = privateKey.ecdsa_serialize(signature).hex() + + if args.certificate != None: + certificate = bytearray.fromhex(args.certificate) + apdu = bytearray([0xE0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate + dongle.exchange(apdu) + print("Endorsement setup finalized") + + dongle.close() diff --git a/ledgerblue/endorsementSetupLedger.py b/ledgerblue/endorsementSetupLedger.py index cc2df01..3e427e7 100644 --- a/ledgerblue/endorsementSetupLedger.py +++ b/ledgerblue/endorsementSetupLedger.py @@ -20,150 +20,194 @@ import argparse import ssl -def get_argparser(): - parser = argparse.ArgumentParser(description="""Generate an attestation keypair, using Ledger to sign the Owner -certificate.""") - parser.add_argument("--url", help="Server URL", default="https://hsmprod.hardwarewallet.com/hsm/process") - parser.add_argument("--bypass-ssl-check", help="Keep going even if remote certificate verification fails", action='store_true', default=False) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific -Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11") - parser.add_argument("--endorsement", help="""A reference to the endorsement key to use; this is a reference to the -specific Owner keypair to be used by Ledger to sign the Owner Certificate""", default="attest_1") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int) - parser.add_argument("--key", help="Which endorsement scheme to use", type=auto_int, choices=(1, 2), required=True) - return parser - -def auto_int(x): - return int(x, 0) -def serverQuery(request, url): - data = request.SerializeToString() - urll = urlparse.urlparse(args.url) - req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream" }) - if args.bypass_ssl_check: - res = urllib2.urlopen(req, context=ssl._create_unverified_context()) - else: - res = urllib2.urlopen(req) - data = res.read() - response = Response() - response.ParseFromString(data) - if len(response.exception) != 0: - raise Exception(response.exception) - return response - -if __name__ == '__main__': - import struct - import urllib.request as urllib2 - import urllib.parse as urlparse - from .BlueHSMServer_pb2 import Request, Response - from .comm import getDongle - - args = get_argparser().parse_args() - - if args.targetId == None: - args.targetId = 0x31000002 # Ledger Blue by default - - dongle = getDongle(args.apdu) - - # Identify - - targetid = bytearray(struct.pack('>I', args.targetId)) - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid - dongle.exchange(apdu) - - # Get nonce and ephemeral key - - request = Request() - request.reference = "signEndorsement" - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "persoKey" - parameter.name = args.perso - - response = serverQuery(request, args.url) - - offset = 0 - - remotePublicKey = response.response[offset : offset + 65] - offset += 65 - nonce = response.response[offset : offset + 8] - - # Initialize chain - - apdu = bytearray([0xe0, 0x50, 0x00, 0x00, 0x08]) + nonce - deviceInit = dongle.exchange(apdu) - deviceNonce = deviceInit[4 : 4 + 8] - - # Get remote certificate - - request = Request() - request.reference = "signEndorsement" - request.id = response.id - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "persoKey" - parameter.name = args.perso - request.parameters = bytes(deviceNonce) - - response = serverQuery(request, args.url) - - offset = 0 - - responseLength = response.response[offset + 1] - remotePublicKeySignatureLength = responseLength + 2 - remotePublicKeySignature = response.response[offset : offset + remotePublicKeySignatureLength] - - certificate = bytearray([len(remotePublicKey)]) + remotePublicKey + bytearray([len(remotePublicKeySignature)]) + remotePublicKeySignature - apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # Walk the chain - - index = 0 - while True: - if index == 0: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) - elif index == 1: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) - else: - break - if len(certificate) == 0: - break - request = Request() - request.reference = "signEndorsement" - request.id = response.id - request.parameters = bytes(certificate) - serverQuery(request, args.url) - index += 1 - - # Commit agreement - - request = Request() - request.reference = "signEndorsement" - request.id = response.id - response = serverQuery(request, args.url) - - # Send endorsement request - - apdu = bytearray([0xe0, 0xC0, args.key, 0x00, 0x00]) - endorsementData = dongle.exchange(apdu) +def get_argparser(): + parser = argparse.ArgumentParser( + description="""Generate an attestation keypair, using Ledger to sign the Owner +certificate.""" + ) + parser.add_argument( + "--url", + help="Server URL", + default="https://hsmprod.hardwarewallet.com/hsm/process", + ) + parser.add_argument( + "--bypass-ssl-check", + help="Keep going even if remote certificate verification fails", + action="store_true", + default=False, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--perso", + help="""A reference to the personalization key; this is a reference to the specific +Issuer keypair used by Ledger to sign the device's Issuer Certificate""", + default="perso_11", + ) + parser.add_argument( + "--endorsement", + help="""A reference to the endorsement key to use; this is a reference to the +specific Owner keypair to be used by Ledger to sign the Owner Certificate""", + default="attest_1", + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + ) + parser.add_argument( + "--key", + help="Which endorsement scheme to use", + type=auto_int, + choices=(1, 2), + required=True, + ) + return parser - request = Request() - request.reference = "signEndorsement" - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "endorsementKey" - parameter.name = args.endorsement - request.parameters = bytes(endorsementData) - request.id = response.id - response = serverQuery(request, args.url) - certificate = bytearray(response.response) - # Commit endorsement certificate +def auto_int(x): + return int(x, 0) - apdu = bytearray([0xe0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate - dongle.exchange(apdu) - print("Endorsement setup finalized") - dongle.close() +def serverQuery(request, url): + data = request.SerializeToString() + urll = urlparse.urlparse(args.url) + req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream"}) + if args.bypass_ssl_check: + res = urllib2.urlopen(req, context=ssl._create_unverified_context()) + else: + res = urllib2.urlopen(req) + data = res.read() + response = Response() + response.ParseFromString(data) + if len(response.exception) != 0: + raise Exception(response.exception) + return response + + +if __name__ == "__main__": + import struct + import urllib.request as urllib2 + import urllib.parse as urlparse + from .BlueHSMServer_pb2 import Request, Response + from .comm import getDongle + + args = get_argparser().parse_args() + + if args.targetId == None: + args.targetId = 0x31000002 # Ledger Blue by default + + dongle = getDongle(args.apdu) + + # Identify + + targetid = bytearray(struct.pack(">I", args.targetId)) + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + dongle.exchange(apdu) + + # Get nonce and ephemeral key + + request = Request() + request.reference = "signEndorsement" + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "persoKey" + parameter.name = args.perso + + response = serverQuery(request, args.url) + + offset = 0 + + remotePublicKey = response.response[offset : offset + 65] + offset += 65 + nonce = response.response[offset : offset + 8] + + # Initialize chain + + apdu = bytearray([0xE0, 0x50, 0x00, 0x00, 0x08]) + nonce + deviceInit = dongle.exchange(apdu) + deviceNonce = deviceInit[4 : 4 + 8] + + # Get remote certificate + + request = Request() + request.reference = "signEndorsement" + request.id = response.id + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "persoKey" + parameter.name = args.perso + request.parameters = bytes(deviceNonce) + + response = serverQuery(request, args.url) + + offset = 0 + + responseLength = response.response[offset + 1] + remotePublicKeySignatureLength = responseLength + 2 + remotePublicKeySignature = response.response[ + offset : offset + remotePublicKeySignatureLength + ] + + certificate = ( + bytearray([len(remotePublicKey)]) + + remotePublicKey + + bytearray([len(remotePublicKeySignature)]) + + remotePublicKeySignature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # Walk the chain + + index = 0 + while True: + if index == 0: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) + elif index == 1: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) + else: + break + if len(certificate) == 0: + break + request = Request() + request.reference = "signEndorsement" + request.id = response.id + request.parameters = bytes(certificate) + serverQuery(request, args.url) + index += 1 + + # Commit agreement + + request = Request() + request.reference = "signEndorsement" + request.id = response.id + response = serverQuery(request, args.url) + + # Send endorsement request + + apdu = bytearray([0xE0, 0xC0, args.key, 0x00, 0x00]) + endorsementData = dongle.exchange(apdu) + + request = Request() + request.reference = "signEndorsement" + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "endorsementKey" + parameter.name = args.endorsement + request.parameters = bytes(endorsementData) + request.id = response.id + response = serverQuery(request, args.url) + certificate = bytearray(response.response) + + # Commit endorsement certificate + + apdu = bytearray([0xE0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate + dongle.exchange(apdu) + print("Endorsement setup finalized") + + dongle.close() diff --git a/ledgerblue/endorsementSetupLedger2.py b/ledgerblue/endorsementSetupLedger2.py index 4bb8145..b9e1675 100644 --- a/ledgerblue/endorsementSetupLedger2.py +++ b/ledgerblue/endorsementSetupLedger2.py @@ -19,77 +19,108 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser("Update the firmware by using Ledger to open a Secure Channel.") - parser.add_argument("--url", help="Websocket URL", default="wss://scriptrunner.api.live.ledger.com/update/endorse") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific -Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--endorsement", help="""A reference to the endorsement key to use; this is a reference to the -specific Owner keypair to be used by Ledger to sign the Owner Certificate""", default="attest_1") - parser.add_argument("--key", help="Which endorsement scheme to use", type=auto_int, choices=(1, 2), required=True) - return parser + parser = argparse.ArgumentParser( + "Update the firmware by using Ledger to open a Secure Channel." + ) + parser.add_argument( + "--url", + help="Websocket URL", + default="wss://scriptrunner.api.live.ledger.com/update/endorse", + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--perso", + help="""A reference to the personalization key; this is a reference to the specific +Issuer keypair used by Ledger to sign the device's Issuer Certificate""", + default="perso_11", + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--endorsement", + help="""A reference to the endorsement key to use; this is a reference to the +specific Owner keypair to be used by Ledger to sign the Owner Certificate""", + default="attest_1", + ) + parser.add_argument( + "--key", + help="Which endorsement scheme to use", + type=auto_int, + choices=(1, 2), + required=True, + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def process(dongle, request): - response = {} - apdusList = [] - try: - response['nonce'] = request['nonce'] - if request['query'] == "exchange": - apdusList.append(binascii.unhexlify(request['data'])) - elif request['query'] == "bulk": - for apdu in request['data']: - apdusList.append(binascii.unhexlify(apdu)) - else: - response['response'] = "unsupported" - except: - response['response'] = "parse error" - - if len(apdusList) != 0: - try: - for apdu in apdusList: - response['data'] = dongle.exchange(apdu).hex() - if len(response['data']) == 0: - response['data'] = "9000" - response['response'] = "success" - except: - response['response'] = "I/O" # or error, and SW in data - - return response - -if __name__ == '__main__': - import urllib.parse as urlparse - from .comm import getDongle - from websocket import create_connection - import json - import binascii - - args = get_argparser().parse_args() - - dongle = getDongle(args.apdu) - - url = args.url - queryParameters = {} - queryParameters['targetId'] = args.targetId - queryParameters['perso'] = args.perso - queryParameters['endorse'] = args.endorsement - queryParameters['endorseScheme'] = args.key - queryString = urlparse.urlencode(queryParameters) - ws = create_connection(args.url + '?' + queryString) - while True: - result = json.loads(ws.recv()) - if result['query'] == 'success': - break - if result['query'] == 'error': - raise Exception(result['data'] + " on " + result['uuid'] + "/" + result['session']) - response = process(dongle, result) - ws.send(json.dumps(response)) - ws.close() - - print("Script executed successfully") - - dongle.close() + response = {} + apdusList = [] + try: + response["nonce"] = request["nonce"] + if request["query"] == "exchange": + apdusList.append(binascii.unhexlify(request["data"])) + elif request["query"] == "bulk": + for apdu in request["data"]: + apdusList.append(binascii.unhexlify(apdu)) + else: + response["response"] = "unsupported" + except: + response["response"] = "parse error" + + if len(apdusList) != 0: + try: + for apdu in apdusList: + response["data"] = dongle.exchange(apdu).hex() + if len(response["data"]) == 0: + response["data"] = "9000" + response["response"] = "success" + except: + response["response"] = "I/O" # or error, and SW in data + + return response + + +if __name__ == "__main__": + import urllib.parse as urlparse + from .comm import getDongle + from websocket import create_connection + import json + import binascii + + args = get_argparser().parse_args() + + dongle = getDongle(args.apdu) + + url = args.url + queryParameters = {} + queryParameters["targetId"] = args.targetId + queryParameters["perso"] = args.perso + queryParameters["endorse"] = args.endorsement + queryParameters["endorseScheme"] = args.key + queryString = urlparse.urlencode(queryParameters) + ws = create_connection(args.url + "?" + queryString) + while True: + result = json.loads(ws.recv()) + if result["query"] == "success": + break + if result["query"] == "error": + raise Exception( + result["data"] + " on " + result["uuid"] + "/" + result["session"] + ) + response = process(dongle, result) + ws.send(json.dumps(response)) + ws.close() + + print("Script executed successfully") + + dongle.close() diff --git a/ledgerblue/genCAPair.py b/ledgerblue/genCAPair.py index af9b982..81fcd5b 100644 --- a/ledgerblue/genCAPair.py +++ b/ledgerblue/genCAPair.py @@ -19,15 +19,19 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Generate a Custom CA public-private keypair and print it to console.") - return parser + parser = argparse.ArgumentParser( + description="Generate a Custom CA public-private keypair and print it to console." + ) + return parser + -if __name__ == '__main__': - from .ecWrapper import PrivateKey +if __name__ == "__main__": + from .ecWrapper import PrivateKey - get_argparser().parse_args() - privateKey = PrivateKey() - publicKey = privateKey.pubkey.serialize(compressed=False).hex() - print("Public key : %s" % publicKey) - print("Private key: %s" % privateKey.serialize()) + get_argparser().parse_args() + privateKey = PrivateKey() + publicKey = privateKey.pubkey.serialize(compressed=False).hex() + print("Public key : %s" % publicKey) + print("Private key: %s" % privateKey.serialize()) diff --git a/ledgerblue/getMemInfo.py b/ledgerblue/getMemInfo.py index 5444bd9..3387565 100644 --- a/ledgerblue/getMemInfo.py +++ b/ledgerblue/getMemInfo.py @@ -26,31 +26,40 @@ def auto_int(x): - return int(x, 0) + return int(x, 0) + parser = argparse.ArgumentParser() -parser.add_argument("--targetId", help="Set the chip target ID", type=auto_int, default=0x31100003) +parser.add_argument( + "--targetId", help="Set the chip target ID", type=auto_int, default=0x31100003 +) parser.add_argument("--rootPrivateKey", help="Set the root private key") -parser.add_argument("--apdu", help="Display APDU log", action='store_true') -parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true') +parser.add_argument("--apdu", help="Display APDU log", action="store_true") +parser.add_argument( + "--deployLegacy", help="Use legacy deployment API", action="store_true" +) args = parser.parse_args() print(args.targetId) if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() dongle = getDongle(args.apdu) if args.deployLegacy: - secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) + secret = getDeployedSecretV1( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) else: - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) -loader = HexLoader(dongle, 0xe0, True, secret) + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) +loader = HexLoader(dongle, 0xE0, True, secret) # apps = loader.listApp() memInfo = loader.getMemInfo() print(memInfo) diff --git a/ledgerblue/hashApp.py b/ledgerblue/hashApp.py index c59078c..9e330fe 100644 --- a/ledgerblue/hashApp.py +++ b/ledgerblue/hashApp.py @@ -20,37 +20,48 @@ import argparse import struct + def get_argparser(): - parser = argparse.ArgumentParser(description="Calculate an application hash from the application's hex file.") - parser.add_argument("--hex", help="The application hex file to be hashed", required=True) - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int) - parser.add_argument("--targetVersion", help="Set the chip target version") - return parser + parser = argparse.ArgumentParser( + description="Calculate an application hash from the application's hex file." + ) + parser.add_argument( + "--hex", help="The application hex file to be hashed", required=True + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + ) + parser.add_argument("--targetVersion", help="Set the chip target version") + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + -if __name__ == '__main__': - from .hexParser import IntelHexParser - import hashlib +if __name__ == "__main__": + from .hexParser import IntelHexParser + import hashlib - args = get_argparser().parse_args() + args = get_argparser().parse_args() - # parse - parser = IntelHexParser(args.hex) + # parse + parser = IntelHexParser(args.hex) - # prepare data - m = hashlib.sha256() + # prepare data + m = hashlib.sha256() - if args.targetId: - m.update(struct.pack(">I", args.targetId)) + if args.targetId: + m.update(struct.pack(">I", args.targetId)) - if args.targetVersion: - m.update(args.targetVersion) + if args.targetVersion: + m.update(args.targetVersion) - # consider areas are ordered by ascending address and non-overlaped - for a in parser.getAreas(): - m.update(a.data) - dataToSign = m.digest() + # consider areas are ordered by ascending address and non-overlaped + for a in parser.getAreas(): + m.update(a.data) + dataToSign = m.digest() - print(dataToSign.hex()) + print(dataToSign.hex()) diff --git a/ledgerblue/hexLoader.py b/ledgerblue/hexLoader.py index 206bcd9..9aa14b8 100644 --- a/ledgerblue/hexLoader.py +++ b/ledgerblue/hexLoader.py @@ -37,639 +37,1030 @@ BOLOS_TAG_DATASIZE = 0x05 BOLOS_TAG_DEPENDENCY = 0x06 + def string_to_bytes(x): - return bytes(x, 'ascii') + return bytes(x, "ascii") + def encodelv(v): - l = len(v) - s = b"" - if l < 128: - s += struct.pack(">B", l) - elif l < 256: - s += struct.pack(">B", 0x81) - s += struct.pack(">B", l) - elif l < 65536: - s += struct.pack(">B", 0x82) - s += struct.pack(">H", l) - else: - raise Exception("Unimplemented LV encoding") - s += v - return s + l = len(v) + s = b"" + if l < 128: + s += struct.pack(">B", l) + elif l < 256: + s += struct.pack(">B", 0x81) + s += struct.pack(">B", l) + elif l < 65536: + s += struct.pack(">B", 0x82) + s += struct.pack(">H", l) + else: + raise Exception("Unimplemented LV encoding") + s += v + return s + def encodetlv(t, v): - l = len(v) - s = struct.pack(">B", t) - if l < 128: - s += struct.pack(">B", l) - elif l < 256: - s += struct.pack(">B", 0x81) - s += struct.pack(">B", l) - elif l < 65536: - s += struct.pack(">B", 0x82) - s += struct.pack(">H", l) - else: - raise Exception("Unimplemented TLV encoding") - s += v - return s + l = len(v) + s = struct.pack(">B", t) + if l < 128: + s += struct.pack(">B", l) + elif l < 256: + s += struct.pack(">B", 0x81) + s += struct.pack(">B", l) + elif l < 65536: + s += struct.pack(">B", 0x82) + s += struct.pack(">H", l) + else: + raise Exception("Unimplemented TLV encoding") + s += v + return s + def str2bool(v): - if v is not None: - return v.lower() in ("yes", "true", "t", "1") - return False + if v is not None: + return v.lower() in ("yes", "true", "t", "1") + return False + + SCP_DEBUG = str2bool(os.getenv("SCP_DEBUG")) -class HexLoader: - def scp_derive_key(self, ecdh_secret, keyindex): - if self.scpv3: - mac_block = b'\x01' * 16 - cipher = AES.new(ecdh_secret, AES.MODE_ECB) - mac_key = cipher.encrypt(mac_block) - enc_block = b'\x02' * 16 - cipher = AES.new(ecdh_secret, AES.MODE_ECB) - enc_key = cipher.encrypt(enc_block) - return mac_key + enc_key - retry = 0 - # di = sha256(i || retrycounter || ecdh secret) - while True: - sha256 = hashlib.new('sha256') - sha256.update(struct.pack(">IB", keyindex, retry)) - sha256.update(ecdh_secret) - - # compare di with order - CURVE_SECP256K1 = Curve.get_curve('secp256k1') - if int.from_bytes(sha256.digest(), 'big') < CURVE_SECP256K1.order: - break - #regenerate a new di satisfying order upper bound - retry+=1 - - # Pi = di*G - privkey = PrivateKey(bytes(sha256.digest())) - pubkey = bytearray(privkey.pubkey.serialize(compressed=False)) - # ki = sha256(Pi) - sha256 = hashlib.new('sha256') - sha256.update(pubkey) - #print ("Key " + str (keyindex) + ": " + sha256.hexdigest()) - return sha256.digest() - - def __init__(self, card, cla=0xF0, secure=False, mutauth_result=None, relative=True, cleardata_block_len=None, scpv3=False): - self.card = card - self.cla = cla - self.secure = secure - self.createappParams = None - self.createpackParams = None - self.scpv3 = scpv3 - - #legacy unsecure SCP (pre nanos-1.4, pre blue-2.1) - self.max_mtu = 0xFE - if not self.card is None: - self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize()) - self.scpVersion = 2 - self.key = mutauth_result - self.iv = b'\x00' * 16 - self.relative = relative - - #store the aligned block len to be transported if requested - self.cleardata_block_len=cleardata_block_len - if not (self.cleardata_block_len is None): - if not self.card is None: - self.cleardata_block_len = min(self.cleardata_block_len, self.card.apduMaxDataSize()) - - if scpv3 == True: - self.scp_enc_key = self.scp_derive_key(mutauth_result, 0) - self.scpVersion = 3 - self.max_mtu = 0xFE - if not self.card is None: - self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize() & 0xF0) - return - - # try: - if type(mutauth_result) is dict and 'ecdh_secret' in mutauth_result: - self.scp_enc_key = self.scp_derive_key(mutauth_result['ecdh_secret'], 0)[0:16] - self.scp_enc_iv = b"\x00" * 16 - self.scp_mac_key = self.scp_derive_key(mutauth_result['ecdh_secret'], 1)[0:16] - self.scp_mac_iv = b"\x00" * 16 - self.scpVersion = 3 - self.max_mtu = 0xFE - if not self.card is None: - self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize()&0xF0) - - def crc16(self, data): - TABLE_CRC16_CCITT = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 - ] - crc = 0xFFFF - for i in range(0, len(data)): - b = data[i] & 0xff - b = (b ^ ((crc >> 8) & 0xff)) & 0xff - crc = (TABLE_CRC16_CCITT[b] ^ (crc << 8)) & 0xffff - return crc - - def exchange(self, cla, ins, p1, p2, data): - #wrap - data = self.scpWrap(data) - apdu = bytearray([cla, ins, p1, p2, len(data)]) + bytearray(data) - if self.card == None: - print("%s" % binascii.hexlify(apdu)) - else: - # unwrap after exchanged - return self.scpUnwrap(bytes(self.card.exchange(apdu))) - - def scpWrap(self, data): - if not self.secure or data is None or len(data) == 0: - return data - if self.scpv3 == True: - cipher = AES.new(self.scp_enc_key, mode=AES.MODE_SIV) - ciphertext, tag = cipher.encrypt_and_digest(data) - encryptedData = tag + ciphertext - return encryptedData - - if self.scpVersion == 3: - if SCP_DEBUG: - print(binascii.hexlify(data)) - # ENC - paddedData = data + b'\x80' - while (len(paddedData) % 16) != 0: - paddedData += b'\x00' - if SCP_DEBUG: - print(binascii.hexlify(paddedData)) - cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv) - encryptedData = cipher.encrypt(paddedData) - self.scp_enc_iv = encryptedData[-16:] - if SCP_DEBUG: - print(binascii.hexlify(encryptedData)) - # MAC - cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv) - macData = cipher.encrypt(encryptedData) - self.scp_mac_iv = macData[-16:] - - # only append part of the mac - encryptedData += self.scp_mac_iv[-SCP_MAC_LENGTH:] - if SCP_DEBUG: - print(binascii.hexlify(encryptedData)) - else: - paddedData = data + b'\x80' - while (len(paddedData) % 16) != 0: - paddedData += b'\x00' - cipher = AES.new(self.key, AES.MODE_CBC, self.iv) - if SCP_DEBUG: - print("wrap_old: "+binascii.hexlify(paddedData)) - encryptedData = cipher.encrypt(paddedData) - self.iv = encryptedData[-16:] - - #print (">>") - return encryptedData - - def scpUnwrap(self, data): - if not self.secure or data is None or len(data) == 0 or len(data) == 2: - return data - if self.scpv3 == True: - cipher = AES.new(self.scp_enc_key, mode=AES.MODE_SIV) - tag = data[:16] - decryptedData = cipher.decrypt_and_verify(data[16:], tag) - return decryptedData - - padding_char = 0x80 - - if self.scpVersion == 3: - if SCP_DEBUG: - print(binascii.hexlify(data)) - # MAC - cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv) - macData = cipher.encrypt(bytes(data[0:-SCP_MAC_LENGTH])) - self.scp_mac_iv = macData[-16:] - if self.scp_mac_iv[-SCP_MAC_LENGTH:] != data[-SCP_MAC_LENGTH:] : - raise BaseException("Invalid SCP MAC") - # consume mac - data = data[0:-SCP_MAC_LENGTH] - - if SCP_DEBUG: - print(binascii.hexlify(data)) - # ENC - cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv) - self.scp_enc_iv = bytes(data[-16:]) - data = cipher.decrypt(bytes(data)) - l = len(data) - 1 - while data[l] != padding_char: - l-=1 - if l == -1: - raise BaseException("Invalid SCP ENC padding") - data = data[0:l] - decryptedData = data - - if SCP_DEBUG: - print(binascii.hexlify(data)) - else: - cipher = AES.new(self.key, AES.MODE_CBC, self.iv) - decryptedData = cipher.decrypt(data) - if SCP_DEBUG: - print("unwrap_old: "+binascii.hexlify(decryptedData)) - l = len(decryptedData) - 1 - while decryptedData[l] != padding_char: - l-=1 - if l == -1: - raise BaseException("Invalid SCP ENC padding") - decryptedData = decryptedData[0:l] - self.iv = data[-16:] - - #print ("<<") - return decryptedData - - def selectSegment(self, baseAddress): - data = b'\x05' + struct.pack('>I', baseAddress) - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def loadSegmentChunk(self, offset, chunk): - data = b'\x06' + struct.pack('>H', offset) + chunk - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def flushSegment(self): - data = b'\x07' - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def crcSegment(self, offsetSegment, lengthSegment, crcExpected): - data = b'\x08' + struct.pack('>H', offsetSegment) + struct.pack('>I', lengthSegment) + struct.pack('>H', crcExpected) - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def validateTargetId(self, targetId): - data = struct.pack('>I', targetId) - self.exchange(self.cla, 0x04, 0x00, 0x00, data) - - def boot(self, bootadr, signature=None): - # Force jump into Thumb mode - bootadr |= 1 - data = b'\x09' + struct.pack('>I', bootadr) - if signature != None: - data += struct.pack('>B', len(signature)) + signature - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def commit(self, signature=None): - data = b'\x09' - if signature != None: - data += struct.pack('>B', len(signature)) + signature - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def createAppNoInstallParams(self, appflags, applength, appname, icon=None, path=None, iconOffset=None, iconSize=None, appversion=None): - data = b'\x0B' + struct.pack('>I', applength) + struct.pack('>I', appflags) + struct.pack('>B', len(appname)) + appname - if iconOffset is None: - if not (icon is None): - data += struct.pack('>B', len(icon)) + icon - else: - data += b'\x00' - - if not (path is None): - data += struct.pack('>B', len(path)) + path - else: - data += b'\x00' - - if not iconOffset is None: - data += struct.pack('>I', iconOffset) + struct.pack('>H', iconSize) - - if not appversion is None: - data += struct.pack('>B', len(appversion)) + appversion - - # in previous version, appparams are not part of the application hash yet - self.createappParams = None #data[1:] - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def createApp(self, code_length, api_level=0, data_length=0, install_params_length=0, flags=0, bootOffset=1): - # keep the create app parameters to be included in the load app hash - # maintain compatibility with SDKs not handling API level - if api_level != -1: - self.createappParams = struct.pack('>BIIIII', api_level, code_length, data_length, install_params_length, flags, bootOffset) - else: - self.createappParams = struct.pack('>IIIII', code_length, data_length, install_params_length, flags, bootOffset) - data = b'\x0B' + self.createappParams - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def deleteApp(self, appname): - data = b'\x0C' + struct.pack('>B',len(appname)) + appname - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def deleteAppByHash(self, appfullhash): - if len(appfullhash) != 32: - raise BaseException("Invalid hash format, sha256 expected") - data = b'\x15' + appfullhash - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def createPack(self, language, code_length): - #keep the create pack parameters to be included in the load app hash - self.createpackParams = struct.pack('>I', code_length) - data = self.createpackParams - self.language = language - self.exchange(self.cla, 0x30, language, 0x00, data) - - def loadPackSegmentChunk(self, offset, chunk): - data = struct.pack('>I', offset) + chunk - #print(f"Inside loadPackSegmentChunk, offset={offset}, len(chunk)={len(chunk)}") - self.exchange(self.cla, 0x31, self.language, 0x00, data) - - def commitPack(self, signature=None): - if (signature != None): - data = struct.pack('>B', len(signature)) + signature - else: - data = b'' - self.exchange(self.cla, 0x32, self.language, 0x00, data) - - def deletePack(self, language): - self.language = language - self.exchange(self.cla, 0x33, language, 0x00, b'') - - def listPacks(self, restart=True): - language_id_name = ["English", "Français", "Español"] - if restart: - response = self.exchange(self.cla, 0x34, 0x00, 0x00, b'') - else: - response = self.exchange(self.cla, 0x34, 0x01, 0x00, b'') - result = [] - offset = 0 - if len(response) > 0: - if response[0] != 0x01: - raise Exception(f"Unsupported version format {response[0]}!") - offset += 1 - while offset != len(response): - item = {} - #skip the current entry's size - offset += 1 - #skip len of Language ID - offset += 1 - language_id = response[offset] - if language_id >= len(language_id_name): - language_name = "Unknown" - else: - language_name = language_id_name[language_id] - item['Language ID'] = f"{language_id} ({language_name})" - offset += 1 - offset += 1 - item['size'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['Version'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8') - offset += 1 + response[offset] - result.append(item) - return result - - def getVersion(self): - data = b'\x10' - response = self.exchange(self.cla, 0x00, 0x00, 0x00, data) - result = {} - offset = 0 - result['targetId'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - result['osVersion'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8') - offset += 1 + response[offset] - offset += 1 - result['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - result['mcuVersion'] = response[offset + 1 : offset + 1 + response[offset] - 1].decode('utf-8') - offset += 1 + response[offset] - if offset < len(response): - result['mcuHash'] = response[offset : offset + 32] - return result - - def listApp(self, restart=True): - if self.secure: - if restart: - data = b'\x0E' - else: - data = b'\x0F' - response = self.exchange(self.cla, 0x00, 0x00, 0x00, data) - else: - if restart: - response = self.exchange(self.cla, 0xDE, 0x00, 0x00, b'') - else: - response = self.exchange(self.cla, 0xDF, 0x00, 0x00, b'') - - #print binascii.hexlify(response[0]) - result = [] - offset = 0 - if len(response) > 0: - if response[0] != 0x01: - # support old format - while offset != len(response): - item = {} - offset += 1 - item['name'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8') - offset += 1 + response[offset] - item['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['hash'] = response[offset : offset + 32] - offset += 32 - result.append(item) - else: - offset += 1 - while offset != len(response): - item = {} - #skip the current entry's size - offset += 1 - item['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['hash_code_data'] = response[offset : offset + 32] - offset += 32 - item['hash'] = response[offset : offset + 32] - offset += 32 - item['name'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8') - offset += 1 + response[offset] - result.append(item) - return result - - def getMemInfo(self): - response = self.exchange(self.cla, 0x00, 0x00, 0x00, b'\x11') - item = {} - offset = 0 - item['systemSize'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['applicationsSize'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['freeSize'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['usedAppSlots'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - offset += 4 - item['totalAppSlots'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3] - return item - - def load(self, erase_u8, max_length_per_apdu, hexFile, reverse=False, doCRC=True, targetId=None, targetVersion=None): - if max_length_per_apdu > self.max_mtu: - max_length_per_apdu = self.max_mtu - initialAddress = 0 - if self.relative: - initialAddress = hexFile.minAddr() - sha256 = hashlib.new('sha256') - # stat by hashing the create app params to ensure complete app signature - if targetId != None and (targetId&0xF) > 3: - if targetVersion == None: - print("Target version is not set, application hash will not match!") - targetVersion="" - #encore targetId U4LE, and version string bytes - if not self.createpackParams: - sha256.update(struct.pack('>I', targetId) + string_to_bytes(targetVersion)) - if self.createappParams: - sha256.update(self.createappParams) - areas = hexFile.getAreas() - if reverse: - areas = reversed(hexFile.getAreas()) - for area in areas: - startAddress = area.getStart() - initialAddress - data = area.getData() - if not self.createpackParams: - self.selectSegment(startAddress) - if len(data) == 0: - continue - if len(data) > 0x10000: - raise Exception("Invalid data size for loader") - crc = self.crc16(bytearray(data)) - offset = 0 - length = len(data) - if reverse: - offset = length - while length > 0: - if length > max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH - SCP_MAC_LENGTH: - chunkLen = max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH - SCP_MAC_LENGTH - if (chunkLen%16) != 0: - chunkLen -= (chunkLen%16) - else: - chunkLen = length - - if self.cleardata_block_len and chunkLen%self.cleardata_block_len: - if chunkLen < self.cleardata_block_len: - raise Exception("Cannot transport not block aligned data with fixed block len") - chunkLen -= chunkLen%self.cleardata_block_len - # pad with 00's when not complete block and performing NENC - if reverse: - chunk = data[offset-chunkLen : offset] - if self.createpackParams: - self.loadPackSegmentChunk(offset-chunkLen, bytes(chunk)) - else: - self.loadSegmentChunk(offset-chunkLen, bytes(chunk)) - else: - chunk = data[offset : offset + chunkLen] - sha256.update(chunk) - if self.createpackParams: - self.loadPackSegmentChunk(offset, bytes(chunk)) - else: - self.loadSegmentChunk(offset, bytes(chunk)) - if reverse: - offset -= chunkLen - else: - offset += chunkLen - length -= chunkLen - if not self.createpackParams: - self.flushSegment() - if doCRC: - self.crcSegment(0, len(data), crc) - return sha256.hexdigest() - - def run(self, bootoffset=1, signature=None): - self.boot(bootoffset, signature) - - def resetCustomCA(self): - data = b'\x13' - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def setupCustomCA(self, name, public): - data = b'\x12' + struct.pack('>B', len(name)) + name.encode() + struct.pack('>B', len(public)) + public - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def runApp(self, name): - data = name - self.exchange(self.cla, 0xD8, 0x00, 0x00, data) - - def recoverConfirmID(self, tag, ciphertext): - data = b'\xd4' - data += struct.pack('>B', len(tag + ciphertext)) + tag + ciphertext - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverSetCA(self, name, key): - data = b'\xd2' + struct.pack('>B', len(name)) + name.encode() + struct.pack('>B', len(key)) + key - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverDeleteCA(self, name, key): - data = b'\xd3' + struct.pack('>B', len(name)) + name.encode() + struct.pack('>B', len(key)) + key - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverValidateCertificate(self, version, role, name, key, sign, last=False): - if last == True: - p1 = b'\x80' - else: - p1 = b'\x00' - data = b'\xd5' + p1 - data += version - data += role - data += struct.pack('>B', len(name)) + name.encode() - data += struct.pack('>B', len(key)) + key + struct.pack('>B', len(sign)) + sign - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverMutualAuth(self): - data = b'\xd6' - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverValidateHash(self, tag, ciphertext): - data = b'\xd7' + struct.pack('>B', 48) + tag + ciphertext - return self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverGetShare(self, value='shares'): - if value == 'commitments': - p1 = b'\x01' - elif value == 'point': - p1 = b'\x10' - else: - p1 = b'\x00' - data = b'\xd8' + p1 - return self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverValidateCommit(self, p1, commits, tag=None, ciphertext=None): - data = b'\xd9' - if p1 == 0x2: - data += b'\x02' + struct.pack('>B', len(commits)) + commits - elif p1 == 0x3: - data += b'\x03' + struct.pack('>B', 48) + tag + ciphertext - elif p1 == 0x4: - data += b'\x04' + struct.pack('>B', len(commits)) + commits - else: - data += b'\x00' - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverRestoreSeed(self, tag, ciphertext, words_number): - data = b'\xda' - if words_number == 12: - p1 = b'\x0c' - elif words_number == 18: - p1 = b'\x12' - else : - p1 = b'\x00' - data += p1 + struct.pack('>B', len(tag + ciphertext)) + tag + ciphertext - self.exchange(self.cla, 0x00, 0x00, 0x00, data) - - def recoverDeleteBackup(self, tag, ciphertext): - data = b'\xdb' + struct.pack('>B', len(tag + ciphertext)) + tag + ciphertext - return self.exchange(self.cla, 0x00, 0x00, 0x00, data) +class HexLoader: + def scp_derive_key(self, ecdh_secret, keyindex): + if self.scpv3: + mac_block = b"\x01" * 16 + cipher = AES.new(ecdh_secret, AES.MODE_ECB) + mac_key = cipher.encrypt(mac_block) + enc_block = b"\x02" * 16 + cipher = AES.new(ecdh_secret, AES.MODE_ECB) + enc_key = cipher.encrypt(enc_block) + return mac_key + enc_key + retry = 0 + # di = sha256(i || retrycounter || ecdh secret) + while True: + sha256 = hashlib.new("sha256") + sha256.update(struct.pack(">IB", keyindex, retry)) + sha256.update(ecdh_secret) + + # compare di with order + CURVE_SECP256K1 = Curve.get_curve("secp256k1") + if int.from_bytes(sha256.digest(), "big") < CURVE_SECP256K1.order: + break + # regenerate a new di satisfying order upper bound + retry += 1 + + # Pi = di*G + privkey = PrivateKey(bytes(sha256.digest())) + pubkey = bytearray(privkey.pubkey.serialize(compressed=False)) + # ki = sha256(Pi) + sha256 = hashlib.new("sha256") + sha256.update(pubkey) + # print ("Key " + str (keyindex) + ": " + sha256.hexdigest()) + return sha256.digest() + + def __init__( + self, + card, + cla=0xF0, + secure=False, + mutauth_result=None, + relative=True, + cleardata_block_len=None, + scpv3=False, + ): + self.card = card + self.cla = cla + self.secure = secure + self.createappParams = None + self.createpackParams = None + self.scpv3 = scpv3 + + # legacy unsecure SCP (pre nanos-1.4, pre blue-2.1) + self.max_mtu = 0xFE + if not self.card is None: + self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize()) + self.scpVersion = 2 + self.key = mutauth_result + self.iv = b"\x00" * 16 + self.relative = relative + + # store the aligned block len to be transported if requested + self.cleardata_block_len = cleardata_block_len + if not (self.cleardata_block_len is None): + if not self.card is None: + self.cleardata_block_len = min( + self.cleardata_block_len, self.card.apduMaxDataSize() + ) + + if scpv3 == True: + self.scp_enc_key = self.scp_derive_key(mutauth_result, 0) + self.scpVersion = 3 + self.max_mtu = 0xFE + if not self.card is None: + self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize() & 0xF0) + return + + # try: + if type(mutauth_result) is dict and "ecdh_secret" in mutauth_result: + self.scp_enc_key = self.scp_derive_key(mutauth_result["ecdh_secret"], 0)[ + 0:16 + ] + self.scp_enc_iv = b"\x00" * 16 + self.scp_mac_key = self.scp_derive_key(mutauth_result["ecdh_secret"], 1)[ + 0:16 + ] + self.scp_mac_iv = b"\x00" * 16 + self.scpVersion = 3 + self.max_mtu = 0xFE + if not self.card is None: + self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize() & 0xF0) + + def crc16(self, data): + TABLE_CRC16_CCITT = [ + 0x0000, + 0x1021, + 0x2042, + 0x3063, + 0x4084, + 0x50A5, + 0x60C6, + 0x70E7, + 0x8108, + 0x9129, + 0xA14A, + 0xB16B, + 0xC18C, + 0xD1AD, + 0xE1CE, + 0xF1EF, + 0x1231, + 0x0210, + 0x3273, + 0x2252, + 0x52B5, + 0x4294, + 0x72F7, + 0x62D6, + 0x9339, + 0x8318, + 0xB37B, + 0xA35A, + 0xD3BD, + 0xC39C, + 0xF3FF, + 0xE3DE, + 0x2462, + 0x3443, + 0x0420, + 0x1401, + 0x64E6, + 0x74C7, + 0x44A4, + 0x5485, + 0xA56A, + 0xB54B, + 0x8528, + 0x9509, + 0xE5EE, + 0xF5CF, + 0xC5AC, + 0xD58D, + 0x3653, + 0x2672, + 0x1611, + 0x0630, + 0x76D7, + 0x66F6, + 0x5695, + 0x46B4, + 0xB75B, + 0xA77A, + 0x9719, + 0x8738, + 0xF7DF, + 0xE7FE, + 0xD79D, + 0xC7BC, + 0x48C4, + 0x58E5, + 0x6886, + 0x78A7, + 0x0840, + 0x1861, + 0x2802, + 0x3823, + 0xC9CC, + 0xD9ED, + 0xE98E, + 0xF9AF, + 0x8948, + 0x9969, + 0xA90A, + 0xB92B, + 0x5AF5, + 0x4AD4, + 0x7AB7, + 0x6A96, + 0x1A71, + 0x0A50, + 0x3A33, + 0x2A12, + 0xDBFD, + 0xCBDC, + 0xFBBF, + 0xEB9E, + 0x9B79, + 0x8B58, + 0xBB3B, + 0xAB1A, + 0x6CA6, + 0x7C87, + 0x4CE4, + 0x5CC5, + 0x2C22, + 0x3C03, + 0x0C60, + 0x1C41, + 0xEDAE, + 0xFD8F, + 0xCDEC, + 0xDDCD, + 0xAD2A, + 0xBD0B, + 0x8D68, + 0x9D49, + 0x7E97, + 0x6EB6, + 0x5ED5, + 0x4EF4, + 0x3E13, + 0x2E32, + 0x1E51, + 0x0E70, + 0xFF9F, + 0xEFBE, + 0xDFDD, + 0xCFFC, + 0xBF1B, + 0xAF3A, + 0x9F59, + 0x8F78, + 0x9188, + 0x81A9, + 0xB1CA, + 0xA1EB, + 0xD10C, + 0xC12D, + 0xF14E, + 0xE16F, + 0x1080, + 0x00A1, + 0x30C2, + 0x20E3, + 0x5004, + 0x4025, + 0x7046, + 0x6067, + 0x83B9, + 0x9398, + 0xA3FB, + 0xB3DA, + 0xC33D, + 0xD31C, + 0xE37F, + 0xF35E, + 0x02B1, + 0x1290, + 0x22F3, + 0x32D2, + 0x4235, + 0x5214, + 0x6277, + 0x7256, + 0xB5EA, + 0xA5CB, + 0x95A8, + 0x8589, + 0xF56E, + 0xE54F, + 0xD52C, + 0xC50D, + 0x34E2, + 0x24C3, + 0x14A0, + 0x0481, + 0x7466, + 0x6447, + 0x5424, + 0x4405, + 0xA7DB, + 0xB7FA, + 0x8799, + 0x97B8, + 0xE75F, + 0xF77E, + 0xC71D, + 0xD73C, + 0x26D3, + 0x36F2, + 0x0691, + 0x16B0, + 0x6657, + 0x7676, + 0x4615, + 0x5634, + 0xD94C, + 0xC96D, + 0xF90E, + 0xE92F, + 0x99C8, + 0x89E9, + 0xB98A, + 0xA9AB, + 0x5844, + 0x4865, + 0x7806, + 0x6827, + 0x18C0, + 0x08E1, + 0x3882, + 0x28A3, + 0xCB7D, + 0xDB5C, + 0xEB3F, + 0xFB1E, + 0x8BF9, + 0x9BD8, + 0xABBB, + 0xBB9A, + 0x4A75, + 0x5A54, + 0x6A37, + 0x7A16, + 0x0AF1, + 0x1AD0, + 0x2AB3, + 0x3A92, + 0xFD2E, + 0xED0F, + 0xDD6C, + 0xCD4D, + 0xBDAA, + 0xAD8B, + 0x9DE8, + 0x8DC9, + 0x7C26, + 0x6C07, + 0x5C64, + 0x4C45, + 0x3CA2, + 0x2C83, + 0x1CE0, + 0x0CC1, + 0xEF1F, + 0xFF3E, + 0xCF5D, + 0xDF7C, + 0xAF9B, + 0xBFBA, + 0x8FD9, + 0x9FF8, + 0x6E17, + 0x7E36, + 0x4E55, + 0x5E74, + 0x2E93, + 0x3EB2, + 0x0ED1, + 0x1EF0, + ] + crc = 0xFFFF + for i in range(0, len(data)): + b = data[i] & 0xFF + b = (b ^ ((crc >> 8) & 0xFF)) & 0xFF + crc = (TABLE_CRC16_CCITT[b] ^ (crc << 8)) & 0xFFFF + return crc + + def exchange(self, cla, ins, p1, p2, data): + # wrap + data = self.scpWrap(data) + apdu = bytearray([cla, ins, p1, p2, len(data)]) + bytearray(data) + if self.card == None: + print("%s" % binascii.hexlify(apdu)) + else: + # unwrap after exchanged + return self.scpUnwrap(bytes(self.card.exchange(apdu))) + + def scpWrap(self, data): + if not self.secure or data is None or len(data) == 0: + return data + if self.scpv3 == True: + cipher = AES.new(self.scp_enc_key, mode=AES.MODE_SIV) + ciphertext, tag = cipher.encrypt_and_digest(data) + encryptedData = tag + ciphertext + return encryptedData + + if self.scpVersion == 3: + if SCP_DEBUG: + print(binascii.hexlify(data)) + # ENC + paddedData = data + b"\x80" + while (len(paddedData) % 16) != 0: + paddedData += b"\x00" + if SCP_DEBUG: + print(binascii.hexlify(paddedData)) + cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv) + encryptedData = cipher.encrypt(paddedData) + self.scp_enc_iv = encryptedData[-16:] + if SCP_DEBUG: + print(binascii.hexlify(encryptedData)) + # MAC + cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv) + macData = cipher.encrypt(encryptedData) + self.scp_mac_iv = macData[-16:] + + # only append part of the mac + encryptedData += self.scp_mac_iv[-SCP_MAC_LENGTH:] + if SCP_DEBUG: + print(binascii.hexlify(encryptedData)) + else: + paddedData = data + b"\x80" + while (len(paddedData) % 16) != 0: + paddedData += b"\x00" + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + if SCP_DEBUG: + print("wrap_old: " + binascii.hexlify(paddedData)) + encryptedData = cipher.encrypt(paddedData) + self.iv = encryptedData[-16:] + + # print (">>") + return encryptedData + + def scpUnwrap(self, data): + if not self.secure or data is None or len(data) == 0 or len(data) == 2: + return data + if self.scpv3 == True: + cipher = AES.new(self.scp_enc_key, mode=AES.MODE_SIV) + tag = data[:16] + decryptedData = cipher.decrypt_and_verify(data[16:], tag) + return decryptedData + + padding_char = 0x80 + + if self.scpVersion == 3: + if SCP_DEBUG: + print(binascii.hexlify(data)) + # MAC + cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv) + macData = cipher.encrypt(bytes(data[0:-SCP_MAC_LENGTH])) + self.scp_mac_iv = macData[-16:] + if self.scp_mac_iv[-SCP_MAC_LENGTH:] != data[-SCP_MAC_LENGTH:]: + raise BaseException("Invalid SCP MAC") + # consume mac + data = data[0:-SCP_MAC_LENGTH] + + if SCP_DEBUG: + print(binascii.hexlify(data)) + # ENC + cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv) + self.scp_enc_iv = bytes(data[-16:]) + data = cipher.decrypt(bytes(data)) + l = len(data) - 1 + while data[l] != padding_char: + l -= 1 + if l == -1: + raise BaseException("Invalid SCP ENC padding") + data = data[0:l] + decryptedData = data + + if SCP_DEBUG: + print(binascii.hexlify(data)) + else: + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + decryptedData = cipher.decrypt(data) + if SCP_DEBUG: + print("unwrap_old: " + binascii.hexlify(decryptedData)) + l = len(decryptedData) - 1 + while decryptedData[l] != padding_char: + l -= 1 + if l == -1: + raise BaseException("Invalid SCP ENC padding") + decryptedData = decryptedData[0:l] + self.iv = data[-16:] + + # print ("<<") + return decryptedData + + def selectSegment(self, baseAddress): + data = b"\x05" + struct.pack(">I", baseAddress) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def loadSegmentChunk(self, offset, chunk): + data = b"\x06" + struct.pack(">H", offset) + chunk + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def flushSegment(self): + data = b"\x07" + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def crcSegment(self, offsetSegment, lengthSegment, crcExpected): + data = ( + b"\x08" + + struct.pack(">H", offsetSegment) + + struct.pack(">I", lengthSegment) + + struct.pack(">H", crcExpected) + ) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def validateTargetId(self, targetId): + data = struct.pack(">I", targetId) + self.exchange(self.cla, 0x04, 0x00, 0x00, data) + + def boot(self, bootadr, signature=None): + # Force jump into Thumb mode + bootadr |= 1 + data = b"\x09" + struct.pack(">I", bootadr) + if signature != None: + data += struct.pack(">B", len(signature)) + signature + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def commit(self, signature=None): + data = b"\x09" + if signature != None: + data += struct.pack(">B", len(signature)) + signature + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def createAppNoInstallParams( + self, + appflags, + applength, + appname, + icon=None, + path=None, + iconOffset=None, + iconSize=None, + appversion=None, + ): + data = ( + b"\x0b" + + struct.pack(">I", applength) + + struct.pack(">I", appflags) + + struct.pack(">B", len(appname)) + + appname + ) + if iconOffset is None: + if not (icon is None): + data += struct.pack(">B", len(icon)) + icon + else: + data += b"\x00" + + if not (path is None): + data += struct.pack(">B", len(path)) + path + else: + data += b"\x00" + + if not iconOffset is None: + data += struct.pack(">I", iconOffset) + struct.pack(">H", iconSize) + + if not appversion is None: + data += struct.pack(">B", len(appversion)) + appversion + + # in previous version, appparams are not part of the application hash yet + self.createappParams = None # data[1:] + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def createApp( + self, + code_length, + api_level=0, + data_length=0, + install_params_length=0, + flags=0, + bootOffset=1, + ): + # keep the create app parameters to be included in the load app hash + # maintain compatibility with SDKs not handling API level + if api_level != -1: + self.createappParams = struct.pack( + ">BIIIII", + api_level, + code_length, + data_length, + install_params_length, + flags, + bootOffset, + ) + else: + self.createappParams = struct.pack( + ">IIIII", + code_length, + data_length, + install_params_length, + flags, + bootOffset, + ) + data = b"\x0b" + self.createappParams + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def deleteApp(self, appname): + data = b"\x0c" + struct.pack(">B", len(appname)) + appname + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def deleteAppByHash(self, appfullhash): + if len(appfullhash) != 32: + raise BaseException("Invalid hash format, sha256 expected") + data = b"\x15" + appfullhash + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def createPack(self, language, code_length): + # keep the create pack parameters to be included in the load app hash + self.createpackParams = struct.pack(">I", code_length) + data = self.createpackParams + self.language = language + self.exchange(self.cla, 0x30, language, 0x00, data) + + def loadPackSegmentChunk(self, offset, chunk): + data = struct.pack(">I", offset) + chunk + # print(f"Inside loadPackSegmentChunk, offset={offset}, len(chunk)={len(chunk)}") + self.exchange(self.cla, 0x31, self.language, 0x00, data) + + def commitPack(self, signature=None): + if signature != None: + data = struct.pack(">B", len(signature)) + signature + else: + data = b"" + self.exchange(self.cla, 0x32, self.language, 0x00, data) + + def deletePack(self, language): + self.language = language + self.exchange(self.cla, 0x33, language, 0x00, b"") + + def listPacks(self, restart=True): + language_id_name = ["English", "Français", "Español"] + if restart: + response = self.exchange(self.cla, 0x34, 0x00, 0x00, b"") + else: + response = self.exchange(self.cla, 0x34, 0x01, 0x00, b"") + result = [] + offset = 0 + if len(response) > 0: + if response[0] != 0x01: + raise Exception(f"Unsupported version format {response[0]}!") + offset += 1 + while offset != len(response): + item = {} + # skip the current entry's size + offset += 1 + # skip len of Language ID + offset += 1 + language_id = response[offset] + if language_id >= len(language_id_name): + language_name = "Unknown" + else: + language_name = language_id_name[language_id] + item["Language ID"] = f"{language_id} ({language_name})" + offset += 1 + offset += 1 + item["size"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["Version"] = response[ + offset + 1 : offset + 1 + response[offset] + ].decode("utf-8") + offset += 1 + response[offset] + result.append(item) + return result + + def getVersion(self): + data = b"\x10" + response = self.exchange(self.cla, 0x00, 0x00, 0x00, data) + result = {} + offset = 0 + result["targetId"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + result["osVersion"] = response[ + offset + 1 : offset + 1 + response[offset] + ].decode("utf-8") + offset += 1 + response[offset] + offset += 1 + result["flags"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + result["mcuVersion"] = response[ + offset + 1 : offset + 1 + response[offset] - 1 + ].decode("utf-8") + offset += 1 + response[offset] + if offset < len(response): + result["mcuHash"] = response[offset : offset + 32] + return result + + def listApp(self, restart=True): + if self.secure: + if restart: + data = b"\x0e" + else: + data = b"\x0f" + response = self.exchange(self.cla, 0x00, 0x00, 0x00, data) + else: + if restart: + response = self.exchange(self.cla, 0xDE, 0x00, 0x00, b"") + else: + response = self.exchange(self.cla, 0xDF, 0x00, 0x00, b"") + + # print binascii.hexlify(response[0]) + result = [] + offset = 0 + if len(response) > 0: + if response[0] != 0x01: + # support old format + while offset != len(response): + item = {} + offset += 1 + item["name"] = response[ + offset + 1 : offset + 1 + response[offset] + ].decode("utf-8") + offset += 1 + response[offset] + item["flags"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["hash"] = response[offset : offset + 32] + offset += 32 + result.append(item) + else: + offset += 1 + while offset != len(response): + item = {} + # skip the current entry's size + offset += 1 + item["flags"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["hash_code_data"] = response[offset : offset + 32] + offset += 32 + item["hash"] = response[offset : offset + 32] + offset += 32 + item["name"] = response[ + offset + 1 : offset + 1 + response[offset] + ].decode("utf-8") + offset += 1 + response[offset] + result.append(item) + return result + + def getMemInfo(self): + response = self.exchange(self.cla, 0x00, 0x00, 0x00, b"\x11") + item = {} + offset = 0 + item["systemSize"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["applicationsSize"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["freeSize"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["usedAppSlots"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + offset += 4 + item["totalAppSlots"] = ( + (response[offset] << 24) + | (response[offset + 1] << 16) + | (response[offset + 2] << 8) + | response[offset + 3] + ) + return item + + def load( + self, + erase_u8, + max_length_per_apdu, + hexFile, + reverse=False, + doCRC=True, + targetId=None, + targetVersion=None, + ): + if max_length_per_apdu > self.max_mtu: + max_length_per_apdu = self.max_mtu + initialAddress = 0 + if self.relative: + initialAddress = hexFile.minAddr() + sha256 = hashlib.new("sha256") + # stat by hashing the create app params to ensure complete app signature + if targetId != None and (targetId & 0xF) > 3: + if targetVersion == None: + print("Target version is not set, application hash will not match!") + targetVersion = "" + # encore targetId U4LE, and version string bytes + if not self.createpackParams: + sha256.update( + struct.pack(">I", targetId) + string_to_bytes(targetVersion) + ) + if self.createappParams: + sha256.update(self.createappParams) + areas = hexFile.getAreas() + if reverse: + areas = reversed(hexFile.getAreas()) + for area in areas: + startAddress = area.getStart() - initialAddress + data = area.getData() + if not self.createpackParams: + self.selectSegment(startAddress) + if len(data) == 0: + continue + if len(data) > 0x10000: + raise Exception("Invalid data size for loader") + crc = self.crc16(bytearray(data)) + offset = 0 + length = len(data) + if reverse: + offset = length + while length > 0: + if ( + length + > max_length_per_apdu + - LOAD_SEGMENT_CHUNK_HEADER_LENGTH + - MIN_PADDING_LENGTH + - SCP_MAC_LENGTH + ): + chunkLen = ( + max_length_per_apdu + - LOAD_SEGMENT_CHUNK_HEADER_LENGTH + - MIN_PADDING_LENGTH + - SCP_MAC_LENGTH + ) + if (chunkLen % 16) != 0: + chunkLen -= chunkLen % 16 + else: + chunkLen = length + + if self.cleardata_block_len and chunkLen % self.cleardata_block_len: + if chunkLen < self.cleardata_block_len: + raise Exception( + "Cannot transport not block aligned data with fixed block len" + ) + chunkLen -= chunkLen % self.cleardata_block_len + # pad with 00's when not complete block and performing NENC + if reverse: + chunk = data[offset - chunkLen : offset] + if self.createpackParams: + self.loadPackSegmentChunk(offset - chunkLen, bytes(chunk)) + else: + self.loadSegmentChunk(offset - chunkLen, bytes(chunk)) + else: + chunk = data[offset : offset + chunkLen] + sha256.update(chunk) + if self.createpackParams: + self.loadPackSegmentChunk(offset, bytes(chunk)) + else: + self.loadSegmentChunk(offset, bytes(chunk)) + if reverse: + offset -= chunkLen + else: + offset += chunkLen + length -= chunkLen + if not self.createpackParams: + self.flushSegment() + if doCRC: + self.crcSegment(0, len(data), crc) + return sha256.hexdigest() + + def run(self, bootoffset=1, signature=None): + self.boot(bootoffset, signature) + + def resetCustomCA(self): + data = b"\x13" + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def setupCustomCA(self, name, public): + data = ( + b"\x12" + + struct.pack(">B", len(name)) + + name.encode() + + struct.pack(">B", len(public)) + + public + ) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def runApp(self, name): + data = name + self.exchange(self.cla, 0xD8, 0x00, 0x00, data) + + def recoverConfirmID(self, tag, ciphertext): + data = b"\xd4" + data += struct.pack(">B", len(tag + ciphertext)) + tag + ciphertext + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverSetCA(self, name, key): + data = ( + b"\xd2" + + struct.pack(">B", len(name)) + + name.encode() + + struct.pack(">B", len(key)) + + key + ) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverDeleteCA(self, name, key): + data = ( + b"\xd3" + + struct.pack(">B", len(name)) + + name.encode() + + struct.pack(">B", len(key)) + + key + ) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverValidateCertificate(self, version, role, name, key, sign, last=False): + if last == True: + p1 = b"\x80" + else: + p1 = b"\x00" + data = b"\xd5" + p1 + data += version + data += role + data += struct.pack(">B", len(name)) + name.encode() + data += struct.pack(">B", len(key)) + key + struct.pack(">B", len(sign)) + sign + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverMutualAuth(self): + data = b"\xd6" + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverValidateHash(self, tag, ciphertext): + data = b"\xd7" + struct.pack(">B", 48) + tag + ciphertext + return self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverGetShare(self, value="shares"): + if value == "commitments": + p1 = b"\x01" + elif value == "point": + p1 = b"\x10" + else: + p1 = b"\x00" + data = b"\xd8" + p1 + return self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverValidateCommit(self, p1, commits, tag=None, ciphertext=None): + data = b"\xd9" + if p1 == 0x2: + data += b"\x02" + struct.pack(">B", len(commits)) + commits + elif p1 == 0x3: + data += b"\x03" + struct.pack(">B", 48) + tag + ciphertext + elif p1 == 0x4: + data += b"\x04" + struct.pack(">B", len(commits)) + commits + else: + data += b"\x00" + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverRestoreSeed(self, tag, ciphertext, words_number): + data = b"\xda" + if words_number == 12: + p1 = b"\x0c" + elif words_number == 18: + p1 = b"\x12" + else: + p1 = b"\x00" + data += p1 + struct.pack(">B", len(tag + ciphertext)) + tag + ciphertext + self.exchange(self.cla, 0x00, 0x00, 0x00, data) + + def recoverDeleteBackup(self, tag, ciphertext): + data = b"\xdb" + struct.pack(">B", len(tag + ciphertext)) + tag + ciphertext + return self.exchange(self.cla, 0x00, 0x00, 0x00, data) diff --git a/ledgerblue/hexParser.py b/ledgerblue/hexParser.py index b9af72a..f8c56bb 100644 --- a/ledgerblue/hexParser.py +++ b/ledgerblue/hexParser.py @@ -17,213 +17,263 @@ ******************************************************************************** """ + class IntelHexArea: - def __init__(self, start, data): - self.start = start - self.data = data + def __init__(self, start, data): + self.start = start + self.data = data + + def getStart(self): + return self.start - def getStart(self): - return self.start + def getData(self): + return self.data - def getData(self): - return self.data + def setData(self, data): + self.data = data - def setData(self, data): - self.data = data def insertAreaSorted(areas, area): - i=0 - while i < len(areas): - if area.start < areas[i].start: - break - i+=1 - #areas = areas[0:i] + [area] - areas[i:i] = [area] - return areas + i = 0 + while i < len(areas): + if area.start < areas[i].start: + break + i += 1 + # areas = areas[0:i] + [area] + areas[i:i] = [area] + return areas class IntelHexParser: - # order by start address - def _addArea(self, area): - self.areas = insertAreaSorted(self.areas, area) - - def __init__(self, fileName): - self.bootAddr = 0 - self.areas = [] - lineNumber = 0 - startZone = None - startFirst = None - current = None - zoneData = b'' - file = open(fileName, "r") - for data in file: - lineNumber += 1 - data = data.rstrip('\r\n') - if len(data) == 0: - continue - if data[0] != ':': - raise Exception("Invalid data at line %d" % lineNumber) - data = bytearray.fromhex(data[1:]) - count = data[0] - address = (data[1] << 8) + data[2] - recordType = data[3] - if recordType == 0x00: - if startZone == None: - raise Exception("Data record but no zone defined at line " + str(lineNumber)) - if startFirst == None: - startFirst = address - current = startFirst - if address != current: - self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = b'' - startFirst = address - current = address - zoneData += data[4:4 + count] - current += count - if recordType == 0x01: - if len(zoneData) != 0: - self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = b'' - startZone = None - startFirst = None - current = None - if recordType == 0x02: - raise Exception("Unsupported record 02") - if recordType == 0x03: - raise Exception("Unsupported record 03") - if recordType == 0x04: - if len(zoneData) != 0: - self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = b'' - startZone = None - startFirst = None - current = None - startZone = (data[4] << 8) + data[5] - if recordType == 0x05: - self.bootAddr = ((data[4]&0xFF) << 24) + ((data[5]&0xFF) << 16) + ((data[6]&0xFF) << 8) + (data[7]&0xFF) - #tail add of the last zone + # order by start address + def _addArea(self, area): + self.areas = insertAreaSorted(self.areas, area) + + def __init__(self, fileName): + self.bootAddr = 0 + self.areas = [] + lineNumber = 0 + startZone = None + startFirst = None + current = None + zoneData = b"" + file = open(fileName, "r") + for data in file: + lineNumber += 1 + data = data.rstrip("\r\n") + if len(data) == 0: + continue + if data[0] != ":": + raise Exception("Invalid data at line %d" % lineNumber) + data = bytearray.fromhex(data[1:]) + count = data[0] + address = (data[1] << 8) + data[2] + recordType = data[3] + if recordType == 0x00: + if startZone == None: + raise Exception( + "Data record but no zone defined at line " + str(lineNumber) + ) + if startFirst == None: + startFirst = address + current = startFirst + if address != current: + self._addArea( + IntelHexArea((startZone << 16) + startFirst, zoneData) + ) + zoneData = b"" + startFirst = address + current = address + zoneData += data[4 : 4 + count] + current += count + if recordType == 0x01: + if len(zoneData) != 0: + self._addArea( + IntelHexArea((startZone << 16) + startFirst, zoneData) + ) + zoneData = b"" + startZone = None + startFirst = None + current = None + if recordType == 0x02: + raise Exception("Unsupported record 02") + if recordType == 0x03: + raise Exception("Unsupported record 03") + if recordType == 0x04: if len(zoneData) != 0: - self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = b'' - startZone = None - startFirst = None - current = None - file.close() - - def getAreas(self): - return self.areas - - def getBootAddr(self): - return self.bootAddr - - def maxAddr(self): - addr = 0 - for a in self.areas: - if a.start+len(a.data) > addr: - addr = a.start+len(a.data) - return addr - - def minAddr(self): - addr = 0xFFFFFFFF - for a in self.areas: - if a.start < addr: - addr = a.start - return addr + self._addArea( + IntelHexArea((startZone << 16) + startFirst, zoneData) + ) + zoneData = b"" + startZone = None + startFirst = None + current = None + startZone = (data[4] << 8) + data[5] + if recordType == 0x05: + self.bootAddr = ( + ((data[4] & 0xFF) << 24) + + ((data[5] & 0xFF) << 16) + + ((data[6] & 0xFF) << 8) + + (data[7] & 0xFF) + ) + # tail add of the last zone + if len(zoneData) != 0: + self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) + zoneData = b"" + startZone = None + startFirst = None + current = None + file.close() + + def getAreas(self): + return self.areas + + def getBootAddr(self): + return self.bootAddr + + def maxAddr(self): + addr = 0 + for a in self.areas: + if a.start + len(a.data) > addr: + addr = a.start + len(a.data) + return addr + + def minAddr(self): + addr = 0xFFFFFFFF + for a in self.areas: + if a.start < addr: + addr = a.start + return addr + import binascii + class IntelHexPrinter: - def addArea(self, startaddress, data, insertFirst=False): - #self.areas.append(IntelHexArea(startaddress, data)) - if insertFirst: - self.areas = [IntelHexArea(startaddress, data)] + self.areas - else: - #order by start address - self.areas = insertAreaSorted(self.areas, IntelHexArea(startaddress, data)) - - def __init__(self, parser=None, eol="\r\n"): - self.areas = [] - self.eol = eol - self.bootAddr = 0 - # build bound to the parser - if parser: - for a in parser.areas: - self.addArea(a.start, a.data) - self.bootAddr = parser.bootAddr - - def getAreas(self): - return self.areas - - def getBootAddr(self): - return self.bootAddr - - def maxAddr(self): - addr = 0 - for a in self.areas: - if a.start+len(a.data) > addr: - addr = a.start+len(a.data) - return addr - - def minAddr(self): - addr = 0xFFFFFFFF - for a in self.areas: - if a.start < addr: - addr = a.start - return addr - - def setBootAddr(self, bootAddr): - self.bootAddr = int(bootAddr) - - def checksum(self, bin): - cks = 0 - for b in bin: - cks += b - cks = (-cks) & 0x0FF - return cks - - def _emit_binary(self, file, bin): - cks = self.checksum(bin) - s = (":" + binascii.hexlify(bin).decode('utf-8') + hex(0x100+cks)[3:] + self.eol).upper() - if file != None: - file.write(s) - else: - print(s) - - def writeTo(self, fileName, blocksize=32): - file = None - if fileName != None: - file = open(fileName, "w") - for area in self.areas: - off = 0 - # force the emission of selection record at start - oldoff = area.start + 0x10000 - while off < len(area.data): - # emit a offset selection record - if (off & 0xFFFF0000) != (oldoff & 0xFFFF0000): - self._emit_binary(file, bytearray.fromhex("02000004" + hex(0x10000+(area.start>>16))[3:7])) - - # emit data record - if off+blocksize > len(area.data): - self._emit_binary(file, bytearray.fromhex(hex(0x100+(len(area.data)-off))[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00") + area.data[off:len(area.data)]) - else: - self._emit_binary(file, bytearray.fromhex(hex(0x100+blocksize)[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00") + area.data[off:off+blocksize]) - - oldoff = off - off += blocksize - - bootAddrHex = hex(0x100000000+self.bootAddr)[3:] - s = ":04000005"+bootAddrHex+hex(0x100+self.checksum( bytearray.fromhex("04000005"+bootAddrHex)))[3:]+self.eol - if file != None: - file.write(s) - else: - print(s) + def addArea(self, startaddress, data, insertFirst=False): + # self.areas.append(IntelHexArea(startaddress, data)) + if insertFirst: + self.areas = [IntelHexArea(startaddress, data)] + self.areas + else: + # order by start address + self.areas = insertAreaSorted(self.areas, IntelHexArea(startaddress, data)) - s = ":00000001FF"+self.eol + def __init__(self, parser=None, eol="\r\n"): + self.areas = [] + self.eol = eol + self.bootAddr = 0 + # build bound to the parser + if parser: + for a in parser.areas: + self.addArea(a.start, a.data) + self.bootAddr = parser.bootAddr - if file != None: - file.write(s) + def getAreas(self): + return self.areas + + def getBootAddr(self): + return self.bootAddr + + def maxAddr(self): + addr = 0 + for a in self.areas: + if a.start + len(a.data) > addr: + addr = a.start + len(a.data) + return addr + + def minAddr(self): + addr = 0xFFFFFFFF + for a in self.areas: + if a.start < addr: + addr = a.start + return addr + + def setBootAddr(self, bootAddr): + self.bootAddr = int(bootAddr) + + def checksum(self, bin): + cks = 0 + for b in bin: + cks += b + cks = (-cks) & 0x0FF + return cks + + def _emit_binary(self, file, bin): + cks = self.checksum(bin) + s = ( + ":" + + binascii.hexlify(bin).decode("utf-8") + + hex(0x100 + cks)[3:] + + self.eol + ).upper() + if file != None: + file.write(s) + else: + print(s) + + def writeTo(self, fileName, blocksize=32): + file = None + if fileName != None: + file = open(fileName, "w") + for area in self.areas: + off = 0 + # force the emission of selection record at start + oldoff = area.start + 0x10000 + while off < len(area.data): + # emit a offset selection record + if (off & 0xFFFF0000) != (oldoff & 0xFFFF0000): + self._emit_binary( + file, + bytearray.fromhex( + "02000004" + hex(0x10000 + (area.start >> 16))[3:7] + ), + ) + + # emit data record + if off + blocksize > len(area.data): + self._emit_binary( + file, + bytearray.fromhex( + hex(0x100 + (len(area.data) - off))[3:] + + hex(0x10000 + off + (area.start & 0xFFFF))[3:] + + "00" + ) + + area.data[off : len(area.data)], + ) else: - print(s) + self._emit_binary( + file, + bytearray.fromhex( + hex(0x100 + blocksize)[3:] + + hex(0x10000 + off + (area.start & 0xFFFF))[3:] + + "00" + ) + + area.data[off : off + blocksize], + ) + + oldoff = off + off += blocksize + + bootAddrHex = hex(0x100000000 + self.bootAddr)[3:] + s = ( + ":04000005" + + bootAddrHex + + hex(0x100 + self.checksum(bytearray.fromhex("04000005" + bootAddrHex)))[ + 3: + ] + + self.eol + ) + if file != None: + file.write(s) + else: + print(s) + + s = ":00000001FF" + self.eol + + if file != None: + file.write(s) + else: + print(s) - if file != None: - file.close() + if file != None: + file.close() diff --git a/ledgerblue/hostOnboard.py b/ledgerblue/hostOnboard.py index 999a304..3bda0e4 100644 --- a/ledgerblue/hostOnboard.py +++ b/ledgerblue/hostOnboard.py @@ -19,72 +19,89 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description=""" + parser = argparse.ArgumentParser( + description=""" .. warning:: Using this script undermines the security of the device. Caveat emptor. -""") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--id", help="Identity to initialize", type=auto_int, choices=(0, 1, 2), required=True) - parser.add_argument("--pin", help="Set a PINs to backup the seed for future use") - parser.add_argument("--prefix", help="Derivation prefix") - parser.add_argument("--passphrase", help="Derivation passphrase") - parser.add_argument("--words", help="Derivation phrase") - return parser - -def auto_int(x): - return int(x, 0) - -if __name__ == '__main__': - import getpass - import unicodedata - - from .comm import getDongle - - args = get_argparser().parse_args() - - dongle = getDongle(args.apdu) - - def enter_if_none_and_normalize(hint, strg): - if strg is None: # or len(string) == 0: len 0 is accepted, to specify without being bothered by a message - strg = getpass.getpass(hint) - if len(strg) != 0 : - strg = unicodedata.normalize('NFKD', u''+strg) - return strg - - if args.id < 2: - args.pin = enter_if_none_and_normalize("PIN: ", args.pin) - if args.pin is None or len(args.pin) == 0: - raise Exception("Missing PIN for persistent identity") - elif not args.pin is None: - raise Exception("Can't set a PIN for the temporary identity") - - args.prefix = enter_if_none_and_normalize("Derivation prefix: ", args.prefix) - args.passphrase = enter_if_none_and_normalize("Derivation passphrase: ", args.passphrase) - args.words = enter_if_none_and_normalize("Derivation phrase: ", args.words) - - if args.pin: - apdudata = bytearray([len(args.pin)]) + bytearray(args.pin, 'utf8') - else: - apdudata = bytearray([0]) - - if args.prefix: - apdudata += bytearray([len(args.prefix)]) + bytearray(args.prefix, 'utf8') - else: - apdudata += bytearray([0]) - - if args.passphrase: - apdudata += bytearray([len(args.passphrase)]) + bytearray(args.passphrase, 'utf8') - else: - apdudata += bytearray([0]) - - if args.words: - apdudata += bytearray([len(args.words)]) + bytearray(args.words, 'utf8') - else: - apdudata += bytearray([0]) +""" + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--id", + help="Identity to initialize", + type=auto_int, + choices=(0, 1, 2), + required=True, + ) + parser.add_argument("--pin", help="Set a PINs to backup the seed for future use") + parser.add_argument("--prefix", help="Derivation prefix") + parser.add_argument("--passphrase", help="Derivation passphrase") + parser.add_argument("--words", help="Derivation phrase") + return parser - apdu = bytearray([0xE0, 0xD0, args.id, 0x00, len(apdudata)]) + apdudata - dongle.exchange(apdu, timeout=3000) - dongle.close() \ No newline at end of file +def auto_int(x): + return int(x, 0) + + +if __name__ == "__main__": + import getpass + import unicodedata + + from .comm import getDongle + + args = get_argparser().parse_args() + + dongle = getDongle(args.apdu) + + def enter_if_none_and_normalize(hint, strg): + if ( + strg is None + ): # or len(string) == 0: len 0 is accepted, to specify without being bothered by a message + strg = getpass.getpass(hint) + if len(strg) != 0: + strg = unicodedata.normalize("NFKD", "" + strg) + return strg + + if args.id < 2: + args.pin = enter_if_none_and_normalize("PIN: ", args.pin) + if args.pin is None or len(args.pin) == 0: + raise Exception("Missing PIN for persistent identity") + elif not args.pin is None: + raise Exception("Can't set a PIN for the temporary identity") + + args.prefix = enter_if_none_and_normalize("Derivation prefix: ", args.prefix) + args.passphrase = enter_if_none_and_normalize( + "Derivation passphrase: ", args.passphrase + ) + args.words = enter_if_none_and_normalize("Derivation phrase: ", args.words) + + if args.pin: + apdudata = bytearray([len(args.pin)]) + bytearray(args.pin, "utf8") + else: + apdudata = bytearray([0]) + + if args.prefix: + apdudata += bytearray([len(args.prefix)]) + bytearray(args.prefix, "utf8") + else: + apdudata += bytearray([0]) + + if args.passphrase: + apdudata += bytearray([len(args.passphrase)]) + bytearray( + args.passphrase, "utf8" + ) + else: + apdudata += bytearray([0]) + + if args.words: + apdudata += bytearray([len(args.words)]) + bytearray(args.words, "utf8") + else: + apdudata += bytearray([0]) + + apdu = bytearray([0xE0, 0xD0, args.id, 0x00, len(apdudata)]) + apdudata + dongle.exchange(apdu, timeout=3000) + + dongle.close() diff --git a/ledgerblue/ledgerWrapper.py b/ledgerblue/ledgerWrapper.py index e1f4b17..5eeda78 100644 --- a/ledgerblue/ledgerWrapper.py +++ b/ledgerblue/ledgerWrapper.py @@ -20,88 +20,92 @@ import struct from .commException import CommException + def wrapCommandAPDU(channel, command, packetSize, ble=False): - if packetSize < 3: - raise CommException("Can't handle Ledger framing with less than 3 bytes for the report") - sequenceIdx = 0 - offset = 0 - if not ble: - result = struct.pack(">H", channel) - extraHeaderSize = 2 - else: - result = "" - extraHeaderSize = 0 - result += struct.pack(">BHH", 0x05, sequenceIdx, len(command)) - sequenceIdx = sequenceIdx + 1 - if len(command) > packetSize - 5 - extraHeaderSize: - blockSize = packetSize - 5 - extraHeaderSize - else: - blockSize = len(command) - result += command[offset : offset + blockSize] - offset = offset + blockSize - while offset != len(command): - if not ble: - result += struct.pack(">H", channel) - result += struct.pack(">BH", 0x05, sequenceIdx) - sequenceIdx = sequenceIdx + 1 - if (len(command) - offset) > packetSize - 3 - extraHeaderSize: - blockSize = packetSize - 3 - extraHeaderSize - else: - blockSize = len(command) - offset - result += command[offset : offset + blockSize] - offset = offset + blockSize - if not ble: - while (len(result) % packetSize) != 0: - result += b"\x00" - return bytearray(result) + if packetSize < 3: + raise CommException( + "Can't handle Ledger framing with less than 3 bytes for the report" + ) + sequenceIdx = 0 + offset = 0 + if not ble: + result = struct.pack(">H", channel) + extraHeaderSize = 2 + else: + result = "" + extraHeaderSize = 0 + result += struct.pack(">BHH", 0x05, sequenceIdx, len(command)) + sequenceIdx = sequenceIdx + 1 + if len(command) > packetSize - 5 - extraHeaderSize: + blockSize = packetSize - 5 - extraHeaderSize + else: + blockSize = len(command) + result += command[offset : offset + blockSize] + offset = offset + blockSize + while offset != len(command): + if not ble: + result += struct.pack(">H", channel) + result += struct.pack(">BH", 0x05, sequenceIdx) + sequenceIdx = sequenceIdx + 1 + if (len(command) - offset) > packetSize - 3 - extraHeaderSize: + blockSize = packetSize - 3 - extraHeaderSize + else: + blockSize = len(command) - offset + result += command[offset : offset + blockSize] + offset = offset + blockSize + if not ble: + while (len(result) % packetSize) != 0: + result += b"\x00" + return bytearray(result) + def unwrapResponseAPDU(channel, data, packetSize, ble=False): - sequenceIdx = 0 - offset = 0 - if not ble: - extraHeaderSize = 2 - else: - extraHeaderSize = 0 - if (data is None) or (len(data) < 5 + extraHeaderSize + 5): - return None - if not ble: - if struct.unpack(">H", data[offset : offset + 2])[0] != channel: - raise CommException("Invalid channel") - offset += 2 - if data[offset] != 0x05: - raise CommException("Invalid tag") - offset += 1 - if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: - raise CommException("Invalid sequence") - offset += 2 - responseLength = struct.unpack(">H", data[offset : offset + 2])[0] - offset += 2 - if len(data) < 5 + extraHeaderSize + responseLength: - return None - if responseLength > packetSize - 5 - extraHeaderSize: - blockSize = packetSize - 5 - extraHeaderSize - else: - blockSize = responseLength - result = data[offset : offset + blockSize] - offset += blockSize - while len(result) != responseLength: - sequenceIdx = sequenceIdx + 1 - if offset == len(data): - return None - if not ble: - if struct.unpack(">H", data[offset : offset + 2])[0] != channel: - raise CommException("Invalid channel") - offset += 2 - if data[offset] != 0x05: - raise CommException("Invalid tag") - offset += 1 - if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: - raise CommException("Invalid sequence") - offset += 2 - if (responseLength - len(result)) > packetSize - 3 - extraHeaderSize: - blockSize = packetSize - 3 - extraHeaderSize - else: - blockSize = responseLength - len(result) - result += data[offset : offset + blockSize] - offset += blockSize - return bytearray(result) + sequenceIdx = 0 + offset = 0 + if not ble: + extraHeaderSize = 2 + else: + extraHeaderSize = 0 + if (data is None) or (len(data) < 5 + extraHeaderSize + 5): + return None + if not ble: + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise CommException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise CommException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise CommException("Invalid sequence") + offset += 2 + responseLength = struct.unpack(">H", data[offset : offset + 2])[0] + offset += 2 + if len(data) < 5 + extraHeaderSize + responseLength: + return None + if responseLength > packetSize - 5 - extraHeaderSize: + blockSize = packetSize - 5 - extraHeaderSize + else: + blockSize = responseLength + result = data[offset : offset + blockSize] + offset += blockSize + while len(result) != responseLength: + sequenceIdx = sequenceIdx + 1 + if offset == len(data): + return None + if not ble: + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise CommException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise CommException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise CommException("Invalid sequence") + offset += 2 + if (responseLength - len(result)) > packetSize - 3 - extraHeaderSize: + blockSize = packetSize - 3 - extraHeaderSize + else: + blockSize = responseLength - len(result) + result += data[offset : offset + blockSize] + offset += blockSize + return bytearray(result) diff --git a/ledgerblue/listApps.py b/ledgerblue/listApps.py index bb25a06..6aa251c 100644 --- a/ledgerblue/listApps.py +++ b/ledgerblue/listApps.py @@ -19,48 +19,66 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="List all apps on the device.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel -(otherwise, a random one will be generated)""") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true') - parser.add_argument("--scp", help="Use a secure channel to list applications", action='store_true') - return parser + parser = argparse.ArgumentParser(description="List all apps on the device.") + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel +(otherwise, a random one will be generated)""", + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--deployLegacy", help="Use legacy deployment API", action="store_true" + ) + parser.add_argument( + "--scp", help="Use a secure channel to list applications", action="store_true" + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) -if __name__ == '__main__': - from .ecWrapper import PrivateKey - from .comm import getDongle - from .deployed import getDeployedSecretV1, getDeployedSecretV2 - from .hexLoader import HexLoader - import binascii - args = get_argparser().parse_args() +if __name__ == "__main__": + from .ecWrapper import PrivateKey + from .comm import getDongle + from .deployed import getDeployedSecretV1, getDeployedSecretV2 + from .hexLoader import HexLoader + import binascii - dongle = getDongle(args.apdu) + args = get_argparser().parse_args() - if args.scp: - if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() + dongle = getDongle(args.apdu) + if args.scp: + if args.rootPrivateKey is None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() - if args.deployLegacy: - secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - secret = None - loader = HexLoader(dongle, 0xe0, args.scp, secret) - apps = loader.listApp() - while len(apps) != 0: - print(apps) - apps = loader.listApp(False) + if args.deployLegacy: + secret = getDeployedSecretV1( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + secret = None + loader = HexLoader(dongle, 0xE0, args.scp, secret) + apps = loader.listApp() + while len(apps) != 0: + print(apps) + apps = loader.listApp(False) - dongle.close() + dongle.close() diff --git a/ledgerblue/loadApp.py b/ledgerblue/loadApp.py index 98cd69f..f529c68 100644 --- a/ledgerblue/loadApp.py +++ b/ledgerblue/loadApp.py @@ -23,277 +23,431 @@ import argparse import os -NOCRC=False +NOCRC = False if "NOCRC" in os.environ and len(os.environ["NOCRC"]) != 0: - NOCRC=os.environ["NOCRC"] + NOCRC = os.environ["NOCRC"] def get_argparser(): - parser = argparse.ArgumentParser(description="Load an app onto the device from a hex file.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--targetVersion", help="Set the chip target version") - parser.add_argument("--apiLevel", help="Set the API level of the SDK used to build the app", type=auto_int, default=-1) - parser.add_argument("--fileName", help="The application hex file to be loaded onto the device", required=True) - parser.add_argument("--icon", help="The icon content to use (hex encoded)") - parser.add_argument("--curve", help="""A curve on which BIP 32 derivation is locked ("secp256k1", "secp256r1", -"ed25519" or "bls12381g1"), can be repeated""", action='append') - parser.add_argument("--path", help="""A BIP 32 path to which derivation is locked (format decimal a'/b'/c), can be -repeated""", action='append') - parser.add_argument("--path_slip21", help="""A SLIP 21 path to which derivation is locked""", action='append') - parser.add_argument("--appName", help="The name to give the application after loading it", required=True) - parser.add_argument("--signature", help="A signature of the application (hex encoded)") - parser.add_argument("--signApp", help="Sign application with provided signPrivateKey", action='store_true') - parser.add_argument("--appFlags", help="The application flags", type=auto_int, default=0) - parser.add_argument("--bootAddr", help="The application's boot address", type=auto_int) - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - parser.add_argument("--signPrivateKey", help="Set the private key used to sign the loaded app") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true') - parser.add_argument("--delete", help="Delete the app with the same name before loading the provided one", action='store_true') - parser.add_argument("--params", help="Store icon and install parameters in a parameter section before the code", action='store_true') - parser.add_argument("--tlv", help="Use install parameters for all variable length parameters", action='store_true') - parser.add_argument("--dataSize", help="The code section's size in the provided hex file (to separate data from code, if not provided the whole allocated NVRAM section for the application will remain readonly.", type=auto_int) - parser.add_argument("--appVersion", help="The application version (as a string)") - parser.add_argument("--offline", help="Request to only output application load APDUs into given filename") - parser.add_argument("--offlineText", help="Request to only output application load APDUs into given filename in text mode", action='store_true') - parser.add_argument("--installparamsSize", help="The loaded install parameters section size (when parameters are already included within the .hex file.", type=auto_int) - parser.add_argument("--tlvraw", help="Add a custom install param with the hextag:hexvalue encoding", action='append') - parser.add_argument("--dep", help="Add a dependency over an appname[:appversion]", action='append') - parser.add_argument("--nocrc", help="Skip CRC generation when loading", action='store_true') - - return parser + parser = argparse.ArgumentParser( + description="Load an app onto the device from a hex file." + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument("--targetVersion", help="Set the chip target version") + parser.add_argument( + "--apiLevel", + help="Set the API level of the SDK used to build the app", + type=auto_int, + default=-1, + ) + parser.add_argument( + "--fileName", + help="The application hex file to be loaded onto the device", + required=True, + ) + parser.add_argument("--icon", help="The icon content to use (hex encoded)") + parser.add_argument( + "--curve", + help="""A curve on which BIP 32 derivation is locked ("secp256k1", "secp256r1", +"ed25519" or "bls12381g1"), can be repeated""", + action="append", + ) + parser.add_argument( + "--path", + help="""A BIP 32 path to which derivation is locked (format decimal a'/b'/c), can be +repeated""", + action="append", + ) + parser.add_argument( + "--path_slip21", + help="""A SLIP 21 path to which derivation is locked""", + action="append", + ) + parser.add_argument( + "--appName", + help="The name to give the application after loading it", + required=True, + ) + parser.add_argument( + "--signature", help="A signature of the application (hex encoded)" + ) + parser.add_argument( + "--signApp", + help="Sign application with provided signPrivateKey", + action="store_true", + ) + parser.add_argument( + "--appFlags", help="The application flags", type=auto_int, default=0 + ) + parser.add_argument( + "--bootAddr", help="The application's boot address", type=auto_int + ) + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + parser.add_argument( + "--signPrivateKey", help="Set the private key used to sign the loaded app" + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--deployLegacy", help="Use legacy deployment API", action="store_true" + ) + parser.add_argument( + "--delete", + help="Delete the app with the same name before loading the provided one", + action="store_true", + ) + parser.add_argument( + "--params", + help="Store icon and install parameters in a parameter section before the code", + action="store_true", + ) + parser.add_argument( + "--tlv", + help="Use install parameters for all variable length parameters", + action="store_true", + ) + parser.add_argument( + "--dataSize", + help="The code section's size in the provided hex file (to separate data from code, if not provided the whole allocated NVRAM section for the application will remain readonly.", + type=auto_int, + ) + parser.add_argument("--appVersion", help="The application version (as a string)") + parser.add_argument( + "--offline", + help="Request to only output application load APDUs into given filename", + ) + parser.add_argument( + "--offlineText", + help="Request to only output application load APDUs into given filename in text mode", + action="store_true", + ) + parser.add_argument( + "--installparamsSize", + help="The loaded install parameters section size (when parameters are already included within the .hex file.", + type=auto_int, + ) + parser.add_argument( + "--tlvraw", + help="Add a custom install param with the hextag:hexvalue encoding", + action="append", + ) + parser.add_argument( + "--dep", help="Add a dependency over an appname[:appversion]", action="append" + ) + parser.add_argument( + "--nocrc", help="Skip CRC generation when loading", action="store_true" + ) + + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def parse_bip32_path(path): - import struct - if len(path) == 0: - return b"" - elements = path.split('/') - result = struct.pack('>B', len(elements)) - for pathElement in elements: - element = pathElement.split('\'') - if len(element) == 1: - result = result + struct.pack(">I", int(element[0])) - else: - result = result + struct.pack(">I", 0x80000000 | int(element[0])) - return result + import struct + + if len(path) == 0: + return b"" + elements = path.split("/") + result = struct.pack(">B", len(elements)) + for pathElement in elements: + element = pathElement.split("'") + if len(element) == 1: + result = result + struct.pack(">I", int(element[0])) + else: + result = result + struct.pack(">I", 0x80000000 | int(element[0])) + return result + def parse_slip21_path(path): - import struct - result = struct.pack('>B', 0x80 | (len(path) + 1)) - result = result + b'\x00' + string_to_bytes(path) - return result + import struct + + result = struct.pack(">B", 0x80 | (len(path) + 1)) + result = result + b"\x00" + string_to_bytes(path) + return result + def string_to_bytes(x): - return bytes(x, 'ascii') + return bytes(x, "ascii") def main(args, debug: bool = True): - from .ecWrapper import PrivateKey - from .comm import getDongle - from .hexParser import IntelHexParser, IntelHexPrinter - from .hexLoader import ( - HexLoader, encodelv, encodetlv, - BOLOS_TAG_APPNAME, BOLOS_TAG_DERIVEPATH, - BOLOS_TAG_APPVERSION, BOLOS_TAG_ICON, BOLOS_TAG_DEPENDENCY + from .ecWrapper import PrivateKey + from .comm import getDongle + from .hexParser import IntelHexParser, IntelHexPrinter + from .hexLoader import ( + HexLoader, + encodelv, + encodetlv, + BOLOS_TAG_APPNAME, + BOLOS_TAG_DERIVEPATH, + BOLOS_TAG_APPVERSION, + BOLOS_TAG_ICON, + BOLOS_TAG_DEPENDENCY, + ) + from .deployed import getDeployedSecretV1, getDeployedSecretV2 + import struct + import binascii + + args = get_argparser().parse_args(args) + + if args.rootPrivateKey == None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + if debug: + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() + + args.appName = string_to_bytes(args.appName) + + parser = IntelHexParser(args.fileName) + if args.bootAddr == None: + args.bootAddr = parser.getBootAddr() + + path = b"" + curveMask = 0xFF + if args.curve != None: + curveMask = 0x00 + for curve in args.curve: + if curve == "secp256k1": + curveMask |= 0x01 + elif curve == "secp256r1": + curveMask |= 0x02 + elif curve == "ed25519": + curveMask |= 0x04 + elif curve == "bls12381g1": + curveMask |= 0x10 + else: + raise Exception("Unknown curve " + curve) + + if args.path_slip21 != None: + curveMask |= 0x08 + path += struct.pack(">B", curveMask) + if args.path != None: + for item in args.path: + if len(item) != 0: + path += parse_bip32_path(item) + if args.path_slip21 != None: + for item in args.path_slip21: + if len(item) != 0: + path += parse_slip21_path(item) + if (args.path == None) or ((len(args.path) == 1) and (len(args.path[0]) == 0)): + path += struct.pack( + ">B", 0 + ) # Unrestricted, authorize all paths for regular derivation + + if not args.icon is None: + args.icon = bytearray.fromhex(args.icon) + + signature = None + if not args.signature is None: + signature = bytearray.fromhex(args.signature) + + # prepend app's data with the icon content (could also add other various install parameters) + printer = IntelHexPrinter(parser) + + # Use of Nested Encryption Key within the SCP protocol is mandartory for upgrades + cleardata_block_len = None + if args.appFlags & 2: + # Not true for scp < 3 + # if signature is None: + # raise BaseException('Upgrades must be signed') + + # ensure data can be decoded with code decryption key without troubles. + cleardata_block_len = 16 + + dongle = None + secret = None + if not args.offline: + dongle = getDongle(args.apdu) + if args.deployLegacy: + secret = getDeployedSecretV1( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + else: + fileTarget = open(args.offline, "wb") + + class FileCard: + def __init__(self, target): + self.target = target + + def exchange(self, apdu): + if args.apdu: + print(binascii.hexlify(apdu)) + apdu = binascii.hexlify(apdu) + self.target.write(apdu + "\n".encode()) + return bytearray([]) + + def apduMaxDataSize(self): + # ensure to allow for encryption of those apdu afterward + return 240 + + def close(self): + self.target.close() + + dongle = FileCard(fileTarget) + + loader = HexLoader( + dongle, 0xE0, not args.offline, secret, cleardata_block_len=cleardata_block_len + ) + + # tlv mode does not support explicit by name removal, would require a list app before to identify the hash to be removed + if (not (args.appFlags & 2)) and args.delete: + loader.deleteApp(args.appName) + + if args.tlv: + # if code length is not provided, then consider the whole provided hex file is the code and no data section is split + code_length = printer.maxAddr() - printer.minAddr() + if not args.dataSize is None: + code_length -= args.dataSize + else: + args.dataSize = 0 + + installparams = b"" + + # express dependency + if args.dep: + for dep in args.dep: + appname = dep + appversion = None + # split if version is specified + if dep.find(":") != -1: + (appname, appversion) = dep.split(":") + depvalue = encodelv(string_to_bytes(appname)) + if appversion: + depvalue += encodelv(string_to_bytes(appversion)) + installparams += encodetlv(BOLOS_TAG_DEPENDENCY, depvalue) + + # add raw install parameters as requested + if args.tlvraw: + for tlvraw in args.tlvraw: + (hextag, hexvalue) = tlvraw.split(":") + installparams += encodetlv( + int(hextag, 16), binascii.unhexlify(hexvalue) + ) + + if (not (args.appFlags & 2)) and ( + args.installparamsSize is None or args.installparamsSize == 0 + ): + # build install parameters + # mandatory app name + installparams += encodetlv(BOLOS_TAG_APPNAME, args.appName) + if not args.appVersion is None: + installparams += encodetlv( + BOLOS_TAG_APPVERSION, string_to_bytes(args.appVersion) + ) + if not args.icon is None: + installparams += encodetlv(BOLOS_TAG_ICON, bytes(args.icon)) + if len(path) > 0: + installparams += encodetlv(BOLOS_TAG_DERIVEPATH, path) + + # append install parameters to the loaded file + param_start = ( + printer.maxAddr() + + (PAGE_ALIGNMENT - (args.dataSize % PAGE_ALIGNMENT)) % PAGE_ALIGNMENT + ) + # only append install param section when not an upgrade as it has already been computed in the encrypted and signed chunk + printer.addArea(param_start, installparams) + paramsSize = len(installparams) + else: + paramsSize = args.installparamsSize + # split code and install params in the code + code_length -= args.installparamsSize + # create app + # ensure the boot address is an offset + if args.bootAddr > printer.minAddr(): + args.bootAddr -= printer.minAddr() + loader.createApp( + code_length, + args.apiLevel, + args.dataSize, + paramsSize, + args.appFlags, + args.bootAddr | 1, + ) + elif args.params: + paramsSectionContent = [] + if not args.icon is None: + paramsSectionContent = args.icon + # take care of aligning the parameters sections to avoid possible invalid dereference of aligned words in the program nvram. + # also use the default MPU alignment + param_start = ( + printer.minAddr() + - len(paramsSectionContent) + - (DEFAULT_ALIGNMENT - (len(paramsSectionContent) % DEFAULT_ALIGNMENT)) + ) + printer.addArea(param_start, paramsSectionContent) + # account for added regions (install parameters, icon ...) + appLength = printer.maxAddr() - printer.minAddr() + loader.createAppNoInstallParams( + args.appFlags, + appLength, + args.appName, + None, + path, + 0, + len(paramsSectionContent), + string_to_bytes(args.appVersion), ) - from .deployed import getDeployedSecretV1, getDeployedSecretV2 - import struct - import binascii - - args = get_argparser().parse_args(args) - - if args.rootPrivateKey == None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - if debug: - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() - - args.appName = string_to_bytes(args.appName) - - parser = IntelHexParser(args.fileName) - if args.bootAddr == None: - args.bootAddr = parser.getBootAddr() - - path = b"" - curveMask = 0xff - if args.curve != None: - curveMask = 0x00 - for curve in args.curve: - if curve == 'secp256k1': - curveMask |= 0x01 - elif curve == 'secp256r1': - curveMask |= 0x02 - elif curve == 'ed25519': - curveMask |= 0x04 - elif curve == 'bls12381g1': - curveMask |= 0x10 - else: - raise Exception("Unknown curve " + curve) - - if args.path_slip21 != None: - curveMask |= 0x08 - path += struct.pack('>B',curveMask) - if args.path != None: - for item in args.path: - if len(item) != 0: - path += parse_bip32_path(item) - if args.path_slip21 != None: - for item in args.path_slip21: - if len(item) != 0: - path += parse_slip21_path(item) - if (args.path == None) or ((len(args.path) == 1) and (len(args.path[0]) == 0)): - path += struct.pack('>B', 0) # Unrestricted, authorize all paths for regular derivation - - if not args.icon is None: - args.icon = bytearray.fromhex(args.icon) - - signature = None - if not args.signature is None: - signature = bytearray.fromhex(args.signature) - - #prepend app's data with the icon content (could also add other various install parameters) - printer = IntelHexPrinter(parser) - - # Use of Nested Encryption Key within the SCP protocol is mandartory for upgrades - cleardata_block_len=None - if args.appFlags & 2: - # Not true for scp < 3 - # if signature is None: - # raise BaseException('Upgrades must be signed') - - # ensure data can be decoded with code decryption key without troubles. - cleardata_block_len = 16 - - dongle = None - secret = None - if not args.offline: - dongle = getDongle(args.apdu) - if args.deployLegacy: - secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - else: - fileTarget = open(args.offline, "wb") - class FileCard: - def __init__(self, target): - self.target = target - def exchange(self, apdu): - if args.apdu: - print(binascii.hexlify(apdu)) - apdu = binascii.hexlify(apdu) - self.target.write(apdu + '\n'.encode()) - return bytearray([]) - def apduMaxDataSize(self): - # ensure to allow for encryption of those apdu afterward - return 240 - def close(self): - self.target.close() - dongle = FileCard(fileTarget) - - loader = HexLoader(dongle, 0xe0, not args.offline, secret, cleardata_block_len=cleardata_block_len) - - #tlv mode does not support explicit by name removal, would require a list app before to identify the hash to be removed - if (not (args.appFlags & 2)) and args.delete: - loader.deleteApp(args.appName) - - if args.tlv: - #if code length is not provided, then consider the whole provided hex file is the code and no data section is split - code_length = printer.maxAddr() - printer.minAddr() - if not args.dataSize is None: - code_length -= args.dataSize - else: - args.dataSize = 0 - - installparams = b"" - - # express dependency - if args.dep: - for dep in args.dep: - appname = dep - appversion = None - # split if version is specified - if dep.find(":") != -1: - (appname,appversion) = dep.split(":") - depvalue = encodelv(string_to_bytes(appname)) - if appversion: - depvalue += encodelv(string_to_bytes(appversion)) - installparams += encodetlv(BOLOS_TAG_DEPENDENCY, depvalue) - - #add raw install parameters as requested - if args.tlvraw: - for tlvraw in args.tlvraw: - (hextag,hexvalue) = tlvraw.split(":") - installparams += encodetlv(int(hextag, 16), binascii.unhexlify(hexvalue)) - - if (not (args.appFlags & 2)) and ( args.installparamsSize is None or args.installparamsSize == 0 ): - #build install parameters - #mandatory app name - installparams += encodetlv(BOLOS_TAG_APPNAME, args.appName) - if not args.appVersion is None: - installparams += encodetlv(BOLOS_TAG_APPVERSION, string_to_bytes(args.appVersion)) - if not args.icon is None: - installparams += encodetlv(BOLOS_TAG_ICON, bytes(args.icon)) - if len(path) > 0: - installparams += encodetlv(BOLOS_TAG_DERIVEPATH, path) - - # append install parameters to the loaded file - param_start = printer.maxAddr()+(PAGE_ALIGNMENT-(args.dataSize%PAGE_ALIGNMENT))%PAGE_ALIGNMENT - # only append install param section when not an upgrade as it has already been computed in the encrypted and signed chunk - printer.addArea(param_start, installparams) - paramsSize = len(installparams) - else: - paramsSize = args.installparamsSize - # split code and install params in the code - code_length -= args.installparamsSize - # create app - #ensure the boot address is an offset - if args.bootAddr > printer.minAddr(): - args.bootAddr -= printer.minAddr() - loader.createApp(code_length, args.apiLevel, args.dataSize, paramsSize, args.appFlags, args.bootAddr|1) - elif args.params: - paramsSectionContent = [] - if not args.icon is None: - paramsSectionContent = args.icon - #take care of aligning the parameters sections to avoid possible invalid dereference of aligned words in the program nvram. - #also use the default MPU alignment - param_start = printer.minAddr()-len(paramsSectionContent)-(DEFAULT_ALIGNMENT-(len(paramsSectionContent)%DEFAULT_ALIGNMENT)) - printer.addArea(param_start, paramsSectionContent) - # account for added regions (install parameters, icon ...) - appLength = printer.maxAddr() - printer.minAddr() - loader.createAppNoInstallParams(args.appFlags, appLength, args.appName, None, path, 0, len(paramsSectionContent), string_to_bytes(args.appVersion)) - else: - # account for added regions (install parameters, icon ...) - appLength = printer.maxAddr() - printer.minAddr() - loader.createAppNoInstallParams(args.appFlags, appLength, args.appName, args.icon, path, None, None, string_to_bytes(args.appVersion)) - - - hash = loader.load(0x0, 0xF0, printer, targetId=args.targetId, targetVersion=args.targetVersion, doCRC=not (args.nocrc or NOCRC)) - - if debug: - print("Application full hash : " + hash) - - if signature == None and args.signApp: - masterPrivate = PrivateKey(bytes(bytearray.fromhex(args.signPrivateKey))) - signature = masterPrivate.ecdsa_serialize(masterPrivate.ecdsa_sign(bytes(binascii.unhexlify(hash)), raw=True)) - if debug: - print("Application signature: " + str(binascii.hexlify(signature))) - - if args.tlv: - loader.commit(signature) - else: - loader.run(args.bootAddr-printer.minAddr(), signature) - - dongle.close() - return hash - - -if __name__ == '__main__': - import sys - - main(sys.argv[1:]) - sys.exit(0) + else: + # account for added regions (install parameters, icon ...) + appLength = printer.maxAddr() - printer.minAddr() + loader.createAppNoInstallParams( + args.appFlags, + appLength, + args.appName, + args.icon, + path, + None, + None, + string_to_bytes(args.appVersion), + ) + + hash = loader.load( + 0x0, + 0xF0, + printer, + targetId=args.targetId, + targetVersion=args.targetVersion, + doCRC=not (args.nocrc or NOCRC), + ) + + if debug: + print("Application full hash : " + hash) + + if signature == None and args.signApp: + masterPrivate = PrivateKey(bytes(bytearray.fromhex(args.signPrivateKey))) + signature = masterPrivate.ecdsa_serialize( + masterPrivate.ecdsa_sign(bytes(binascii.unhexlify(hash)), raw=True) + ) + if debug: + print("Application signature: " + str(binascii.hexlify(signature))) + + if args.tlv: + loader.commit(signature) + else: + loader.run(args.bootAddr - printer.minAddr(), signature) + + dongle.close() + return hash + + +if __name__ == "__main__": + import sys + + main(sys.argv[1:]) + sys.exit(0) diff --git a/ledgerblue/loadMCU.py b/ledgerblue/loadMCU.py index da2f1e2..f0541f1 100644 --- a/ledgerblue/loadMCU.py +++ b/ledgerblue/loadMCU.py @@ -19,21 +19,38 @@ import argparse + def auto_int(x): return int(x, 0) + def get_argparser(): - parser = argparse.ArgumentParser(description="""Load the firmware onto the MCU. The MCU must already be in -bootloader mode.""") - parser.add_argument("--targetId", help="The device's target ID", type=auto_int, required=True) - parser.add_argument("--fileName", help="The name of the firmware file to load", required=True) + parser = argparse.ArgumentParser( + description="""Load the firmware onto the MCU. The MCU must already be in +bootloader mode.""" + ) + parser.add_argument( + "--targetId", help="The device's target ID", type=auto_int, required=True + ) + parser.add_argument( + "--fileName", help="The name of the firmware file to load", required=True + ) parser.add_argument("--bootAddr", help="The firmware's boot address", type=auto_int) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--reverse", help="Load HEX file in reverse from the highest address to the lowest", action='store_true') - parser.add_argument("--nocrc", help="Load HEX file without checking CRC of loaded sections", action='store_true') + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--reverse", + help="Load HEX file in reverse from the highest address to the lowest", + action="store_true", + ) + parser.add_argument( + "--nocrc", + help="Load HEX file without checking CRC of loaded sections", + action="store_true", + ) return parser -if __name__ == '__main__': + +if __name__ == "__main__": from .hexParser import IntelHexParser from .hexLoader import HexLoader from .comm import getDongle @@ -46,8 +63,8 @@ def get_argparser(): dongle = getDongle(args.apdu) - #relative load - loader = HexLoader(dongle, 0xe0, False, None, False) + # relative load + loader = HexLoader(dongle, 0xE0, False, None, False) loader.validateTargetId(args.targetId) hash = loader.load(0xFF, 0xF0, parser, reverse=args.reverse, doCRC=(not args.nocrc)) diff --git a/ledgerblue/mcuBootloader.py b/ledgerblue/mcuBootloader.py index 727fcd2..fd4df01 100644 --- a/ledgerblue/mcuBootloader.py +++ b/ledgerblue/mcuBootloader.py @@ -19,37 +19,52 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Request the MCU to execute its bootloader.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - return parser + parser = argparse.ArgumentParser( + description="Request the MCU to execute its bootloader." + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + -if __name__ == '__main__': - import binascii +if __name__ == "__main__": + import binascii - from .comm import getDongle - from .deployed import getDeployedSecretV2 - from .ecWrapper import PrivateKey - from .hexLoader import HexLoader + from .comm import getDongle + from .deployed import getDeployedSecretV2 + from .ecWrapper import PrivateKey + from .hexLoader import HexLoader - args = get_argparser().parse_args() + args = get_argparser().parse_args() - if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() + if args.rootPrivateKey is None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() - dongle = getDongle(args.apdu) + dongle = getDongle(args.apdu) - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - loader = HexLoader(dongle, 0xe0, True, secret) - loader.exchange(0xE0, 0, 0, 0, loader.encryptAES(b'\xB0')) + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + loader = HexLoader(dongle, 0xE0, True, secret) + loader.exchange(0xE0, 0, 0, 0, loader.encryptAES(b"\xb0")) - dongle.close() + dongle.close() diff --git a/ledgerblue/readElfMetadata.py b/ledgerblue/readElfMetadata.py index aec2bce..944fce7 100644 --- a/ledgerblue/readElfMetadata.py +++ b/ledgerblue/readElfMetadata.py @@ -34,10 +34,11 @@ "sdk_hash", ] + @contextmanager def _get_elf_file(filename): if os.path.exists(filename): - with open(filename, 'rb') as fp: + with open(filename, "rb") as fp: yield ELFFile(fp) else: raise FileNotFoundError(f"File {filename} does not exist.") @@ -62,22 +63,27 @@ def get_target_id_from_elf(filename): def get_argparser(): parser = argparse.ArgumentParser( - description="""Read the metadata of a Ledger device's ELF binary file.""") + description="""Read the metadata of a Ledger device's ELF binary file.""" + ) parser.add_argument( - "--fileName", help="The name of the ELF binary file to read", required=True) + "--fileName", help="The name of the ELF binary file to read", required=True + ) parser.add_argument( - "--section", help=f"The name of the metadata section to be read. If no value is provided, all sections are read.", choices=__ELF_METADATA_SECTIONS + ["all"], default="all") + "--section", + help=f"The name of the metadata section to be read. If no value is provided, all sections are read.", + choices=__ELF_METADATA_SECTIONS + ["all"], + default="all", + ) return parser -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() with _get_elf_file(args.fileName) as elf: - if(args.section == "all"): + if args.section == "all": for section_name in __ELF_METADATA_SECTIONS: section_value = _get_elf_section_value(elf, section_name) print(f"{section_name} : {section_value}") else: - print(_get_elf_section_value(elf, args.section)) + print(_get_elf_section_value(elf, args.section)) diff --git a/ledgerblue/recoverBackup.py b/ledgerblue/recoverBackup.py index dfbdb00..31eb274 100755 --- a/ledgerblue/recoverBackup.py +++ b/ledgerblue/recoverBackup.py @@ -13,14 +13,36 @@ def get_argparser(): parser = argparse.ArgumentParser(description="Backup 3 shares of a seed.") - parser.add_argument("--targetId", help="The device's target ID (default is Nano X)", type=auto_int) - parser.add_argument("--rootPrivateKey", help="The private key of the Certificate Authority") - parser.add_argument("--issuerPublicKey", help="The public key of the Issuer (used to verify the certificate of " - "the device)") - parser.add_argument("--numberOfWords", help="The number of words in the mnemonic (default is 24 words)", type=auto_int) - parser.add_argument("-c", "--configuration", help="Configuration file", default=None, required=True, action="store") - parser.add_argument("--gpg", help="Encrypt the backup data. Enter the email address associated to your gpg key", - default=None, action="store") + parser.add_argument( + "--targetId", help="The device's target ID (default is Nano X)", type=auto_int + ) + parser.add_argument( + "--rootPrivateKey", help="The private key of the Certificate Authority" + ) + parser.add_argument( + "--issuerPublicKey", + help="The public key of the Issuer (used to verify the certificate of " + "the device)", + ) + parser.add_argument( + "--numberOfWords", + help="The number of words in the mnemonic (default is 24 words)", + type=auto_int, + ) + parser.add_argument( + "-c", + "--configuration", + help="Configuration file", + default=None, + required=True, + action="store", + ) + parser.add_argument( + "--gpg", + help="Encrypt the backup data. Enter the email address associated to your gpg key", + default=None, + action="store", + ) return parser @@ -29,16 +51,15 @@ def auto_int(x): def decode_bytes(my_bytes): - return my_bytes.decode('utf-8') + return my_bytes.decode("utf-8") -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() # Read the configuration file try: - with open(args.configuration,'r') as file: + with open(args.configuration, "r") as file: conf = json.load(file) except Exception as err: print(err) @@ -49,39 +70,50 @@ def decode_bytes(my_bytes): if args.rootPrivateKey is None: raise Exception("Missing Certificate Authority private key") if args.issuerPublicKey is None: - args.issuerPublicKey = '0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805' \ - '7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609' + args.issuerPublicKey = ( + "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805" + "7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609" + ) if args.numberOfWords is None: # Default is 24 words args.numberOfWords = 24 if not args.gpg: print("Your backup is going to be saved unencrypted !") else: - home = os.path.join(os.environ['HOME'], ".gnupg") + home = os.path.join(os.environ["HOME"], ".gnupg") gpg = gnupg.GPG(gnupghome=home) dongle = getDongle(True) - orchestratorPrivateKey = conf['orchestrator']['key'] + orchestratorPrivateKey = conf["orchestrator"]["key"] # Execute device-orchestrator secure channel protocol - secret, devicePublicKey = SCPv3(dongle, bytearray.fromhex(args.issuerPublicKey), args.targetId, - bytearray.fromhex(args.rootPrivateKey), bytearray.fromhex(orchestratorPrivateKey)) - loader = HexLoader(dongle, 0xe0, True, secret, scpv3=True) + secret, devicePublicKey = SCPv3( + dongle, + bytearray.fromhex(args.issuerPublicKey), + args.targetId, + bytearray.fromhex(args.rootPrivateKey), + bytearray.fromhex(orchestratorPrivateKey), + ) + loader = HexLoader(dongle, 0xE0, True, secret, scpv3=True) # Initialize the session with the info from the configuration file recoverSession = Recover(conf) confirmed = False - for provider in conf['providers']: + for provider in conf["providers"]: backup_data = dict() delete_data = dict() - name = provider['name'] - privateKey = provider['key'] + name = provider["name"] + privateKey = provider["key"] # Execute device-provider mutual authentication - providerSk, providerPk = recoverValidate(loader, bytearray.fromhex(args.rootPrivateKey), name, - bytearray.fromhex(privateKey)) + providerSk, providerPk = recoverValidate( + loader, + bytearray.fromhex(args.rootPrivateKey), + name, + bytearray.fromhex(privateKey), + ) loader.recoverMutualAuth() sharedKey = recoverMutualAuth(providerSk, devicePublicKey) recoverSession.sharedKey = sharedKey @@ -103,16 +135,18 @@ def decode_bytes(my_bytes): # Get the share value and the number of words of the mnemonic (can be 12, 18 or 24) # The share is encrypted with the device-provider shared key - resp = loader.recoverGetShare(value='shares') - encryptedData = resp[:len(resp)- 1] + resp = loader.recoverGetShare(value="shares") + encryptedData = resp[: len(resp) - 1] numberOfWords = resp[len(resp) - 1] - share, idx, deletePublicKey, commitHash = recoverSession.recoverBackupProviderDecrypt(encryptedData) + share, idx, deletePublicKey, commitHash = ( + recoverSession.recoverBackupProviderDecrypt(encryptedData) + ) # Get the commitments to the coefficients used to calculate the share - commitments = loader.recoverGetShare(value='commitments') + commitments = loader.recoverGetShare(value="commitments") # Get the VSS point - commitmentPoint = loader.recoverGetShare(value='point') + commitmentPoint = loader.recoverGetShare(value="point") h = sha256() h.update(commitments) @@ -123,43 +157,60 @@ def decode_bytes(my_bytes): raise Exception("Hashes of the commitments don't match") # Verify whether the share is consistent - result, shareCommit = recoverSession.recoverVerifyCommitments(share, idx, commitments, commitmentPoint) + result, shareCommit = recoverSession.recoverVerifyCommitments( + share, idx, commitments, commitmentPoint + ) if result: # Do the backup backup_data[name] = dict() - backup_data[name]['share'] = decode_bytes(binascii.hexlify(share)) - backup_data[name]['index'] = decode_bytes(binascii.hexlify(idx.to_bytes(4, 'little'))) - backup_data[name]['commitments'] = decode_bytes(binascii.hexlify(commitments)) - backup_data[name]['share_commit'] = decode_bytes(binascii.hexlify(shareCommit)) - backup_data[name]['hash'] = decode_bytes(binascii.hexlify(commitHash)) - backup_data[name]['point'] = decode_bytes(binascii.hexlify(commitmentPoint)) - backup_data[name]['words_number'] = numberOfWords + backup_data[name]["share"] = decode_bytes(binascii.hexlify(share)) + backup_data[name]["index"] = decode_bytes( + binascii.hexlify(idx.to_bytes(4, "little")) + ) + backup_data[name]["commitments"] = decode_bytes( + binascii.hexlify(commitments) + ) + backup_data[name]["share_commit"] = decode_bytes( + binascii.hexlify(shareCommit) + ) + backup_data[name]["hash"] = decode_bytes(binascii.hexlify(commitHash)) + backup_data[name]["point"] = decode_bytes(binascii.hexlify(commitmentPoint)) + backup_data[name]["words_number"] = numberOfWords delete_data[name] = dict() - delete_data[name]['public_key'] = decode_bytes(binascii.hexlify(deletePublicKey)) + delete_data[name]["public_key"] = decode_bytes( + binascii.hexlify(deletePublicKey) + ) else: raise Exception("Share's commitments not verified") - # Validate the share's commitment + # Validate the share's commitment recoverSession.recoverValidateShareCommit(loader, shareCommit) # Store the backup (it is up to the user to store it in a safe location) for name in backup_data: try: if args.gpg: - with open(name + ".json.gpg",'w') as file: - encode_backup_data = json.dumps(backup_data, indent = 4, sort_keys = True).encode("utf-8") - encrypted_backup = gpg.encrypt(encode_backup_data,recipients=[args.gpg]) + with open(name + ".json.gpg", "w") as file: + encode_backup_data = json.dumps( + backup_data, indent=4, sort_keys=True + ).encode("utf-8") + encrypted_backup = gpg.encrypt( + encode_backup_data, recipients=[args.gpg] + ) file.write(str(encrypted_backup)) - with open("Delete" + name + ".json.gpg",'w') as file: - encode_delete_data = json.dumps(delete_data, indent = 4).encode("utf-8") - encrypted_data = gpg.encrypt(encode_delete_data,recipients=[args.gpg]) + with open("Delete" + name + ".json.gpg", "w") as file: + encode_delete_data = json.dumps(delete_data, indent=4).encode( + "utf-8" + ) + encrypted_data = gpg.encrypt( + encode_delete_data, recipients=[args.gpg] + ) file.write(str(encrypted_data)) else: - with open(name + ".json",'w') as file: - json.dump(backup_data, file, indent = 4, sort_keys = True) - with open("Delete" + name + ".json",'w') as file: - json.dump(delete_data, file, indent = 4) + with open(name + ".json", "w") as file: + json.dump(backup_data, file, indent=4, sort_keys=True) + with open("Delete" + name + ".json", "w") as file: + json.dump(delete_data, file, indent=4) except Exception as err: print(err) exit() - diff --git a/ledgerblue/recoverDeleteBackup.py b/ledgerblue/recoverDeleteBackup.py index 4fdf2fc..c71f0d3 100755 --- a/ledgerblue/recoverDeleteBackup.py +++ b/ledgerblue/recoverDeleteBackup.py @@ -11,15 +11,42 @@ def get_argparser(): parser = argparse.ArgumentParser(description="Delete shares backups") - parser.add_argument("--targetId", help="The device's target ID (default is Nano X)", type=auto_int) - parser.add_argument("--rootPrivateKey", help="The private key of the Certificate Authority") - parser.add_argument("--issuerPublicKey", help="The public key of the Issuer (used to verify the certificate of " - "the device)") - parser.add_argument("-s", "--select", help="Select the backups to use", required = True, action="store", - dest="backups", type=str, nargs='*') - parser.add_argument("-c", "--configuration", help="Configuration file", default=None, required=True, action="store") - parser.add_argument("--gpg", help="Decrypt the deletion public key. Enter the email address " - "associated to your gpg key", default=None, action="store") + parser.add_argument( + "--targetId", help="The device's target ID (default is Nano X)", type=auto_int + ) + parser.add_argument( + "--rootPrivateKey", help="The private key of the Certificate Authority" + ) + parser.add_argument( + "--issuerPublicKey", + help="The public key of the Issuer (used to verify the certificate of " + "the device)", + ) + parser.add_argument( + "-s", + "--select", + help="Select the backups to use", + required=True, + action="store", + dest="backups", + type=str, + nargs="*", + ) + parser.add_argument( + "-c", + "--configuration", + help="Configuration file", + default=None, + required=True, + action="store", + ) + parser.add_argument( + "--gpg", + help="Decrypt the deletion public key. Enter the email address " + "associated to your gpg key", + default=None, + action="store", + ) return parser @@ -27,13 +54,12 @@ def auto_int(x): return int(x, 0) -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() # Read the configuration file try: - with open(args.configuration,'r') as file: + with open(args.configuration, "r") as file: conf = json.load(file) except Exception as err: print(err) @@ -44,23 +70,29 @@ def auto_int(x): if args.rootPrivateKey is None: raise Exception("Missing Certificate Authority private key") if args.issuerPublicKey is None: - args.issuerPublicKey = '0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805' \ - '7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609' + args.issuerPublicKey = ( + "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805" + "7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609" + ) if not args.gpg: print("Reading your deletion public key from unencrypted files") else: - home = os.path.join(os.environ['HOME'], ".gnupg") + home = os.path.join(os.environ["HOME"], ".gnupg") gpg = gnupg.GPG(gnupghome=home) dongle = getDongle(True) - orchestratorPrivateKey = conf['orchestrator']['key'] + orchestratorPrivateKey = conf["orchestrator"]["key"] # Execute device-orchestrator secure channel protocol - secret, devicePublicKey = SCPv3(dongle, bytearray.fromhex(args.issuerPublicKey), args.targetId, - bytearray.fromhex(args.rootPrivateKey), - bytearray.fromhex(orchestratorPrivateKey)) - loader = HexLoader(dongle, 0xe0, True, secret, scpv3=True) + secret, devicePublicKey = SCPv3( + dongle, + bytearray.fromhex(args.issuerPublicKey), + args.targetId, + bytearray.fromhex(args.rootPrivateKey), + bytearray.fromhex(orchestratorPrivateKey), + ) + loader = HexLoader(dongle, 0xE0, True, secret, scpv3=True) # Initialize the session with the info from the configuration file recoverSession = Recover(conf) @@ -69,7 +101,7 @@ def auto_int(x): for backup in args.backups: # Read the deletion public key try: - with open(backup, 'r') as file: + with open(backup, "r") as file: if args.gpg: enc_delete_data = file.read() dec_data = gpg.decrypt(enc_delete_data) @@ -80,16 +112,20 @@ def auto_int(x): print(err) exit() - for p in conf['providers']: - if p['name'] == list(delete_data.keys())[0]: + for p in conf["providers"]: + if p["name"] == list(delete_data.keys())[0]: break - name = p['name'] - privateKey = p['key'] - backupPublicKey = delete_data[name]['public_key'] + name = p["name"] + privateKey = p["key"] + backupPublicKey = delete_data[name]["public_key"] # Execute device-provider mutual authentication - providerSk, providerPk = recoverValidate(loader, bytearray.fromhex(args.rootPrivateKey), name, - bytearray.fromhex(privateKey)) + providerSk, providerPk = recoverValidate( + loader, + bytearray.fromhex(args.rootPrivateKey), + name, + bytearray.fromhex(privateKey), + ) loader.recoverMutualAuth() sharedKey = recoverMutualAuth(providerSk, devicePublicKey) recoverSession.sharedKey = sharedKey diff --git a/ledgerblue/recoverDeleteCA.py b/ledgerblue/recoverDeleteCA.py index 326d820..98c0533 100755 --- a/ledgerblue/recoverDeleteCA.py +++ b/ledgerblue/recoverDeleteCA.py @@ -7,15 +7,29 @@ def get_argparser(): - parser = argparse.ArgumentParser(description="Delete the custom Certificate Authority used to " - "backup/restore a seed.") - parser.add_argument("--targetId", help="The device's target ID (default is Nano X)", type=auto_int) + parser = argparse.ArgumentParser( + description="Delete the custom Certificate Authority used to " + "backup/restore a seed." + ) + parser.add_argument( + "--targetId", help="The device's target ID (default is Nano X)", type=auto_int + ) parser.add_argument("--name", help="The certificate's name", required=True) - parser.add_argument("--issuerPublicKey", help="The public key of the Issuer (used to verify the certificate of " - "the device)") - parser.add_argument("--rootPrivateKey", help="The private key of the Signer used to establish a secure channel " - "(otherwise a random one will be generated)") - parser.add_argument("--caPublicKey", help="The Custom CA's public key to be enrolled (hex encoded)", required=True) + parser.add_argument( + "--issuerPublicKey", + help="The public key of the Issuer (used to verify the certificate of " + "the device)", + ) + parser.add_argument( + "--rootPrivateKey", + help="The private key of the Signer used to establish a secure channel " + "(otherwise a random one will be generated)", + ) + parser.add_argument( + "--caPublicKey", + help="The Custom CA's public key to be enrolled (hex encoded)", + required=True, + ) return parser @@ -23,8 +37,7 @@ def auto_int(x): return int(x, 0) -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() if args.targetId is None: @@ -32,8 +45,10 @@ def auto_int(x): if args.name is None: raise Exception("Missing certificate's name") if args.issuerPublicKey is None: - args.issuerPublicKey = '0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805' \ - '7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609' + args.issuerPublicKey = ( + "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805" + "7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609" + ) if args.rootPrivateKey is None: privateKey = PrivateKey() publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) @@ -46,10 +61,15 @@ def auto_int(x): dongle = getDongle(True) # Execute secure channel protocol (new version) - secret, devicePublicKey = SCPv3(dongle, bytearray.fromhex(args.issuerPublicKey), args.targetId, - None, bytearray.fromhex(args.rootPrivateKey), - user_mode=True) - loader = HexLoader(dongle, 0xe0, True, secret, scpv3=True) + secret, devicePublicKey = SCPv3( + dongle, + bytearray.fromhex(args.issuerPublicKey), + args.targetId, + None, + bytearray.fromhex(args.rootPrivateKey), + user_mode=True, + ) + loader = HexLoader(dongle, 0xE0, True, secret, scpv3=True) # Delete the Certificate Authority's public key loader.recoverDeleteCA(args.name, publicKey) diff --git a/ledgerblue/recoverMutualAuth.py b/ledgerblue/recoverMutualAuth.py index 7d4f10f..98e2c13 100755 --- a/ledgerblue/recoverMutualAuth.py +++ b/ledgerblue/recoverMutualAuth.py @@ -1,6 +1,10 @@ from ledgerblue.ecWrapper import PublicKey, PrivateKey from ledgerblue.hexLoader import HexLoader -from ledgerblue.recoverSCP import scp_derive_key, extract_from_certificate, decrypt_certificate +from ledgerblue.recoverSCP import ( + scp_derive_key, + extract_from_certificate, + decrypt_certificate, +) import binascii import struct import os @@ -17,19 +21,25 @@ CERT_FORMAT_VERSION = 0x01 -def SCPv3(dongle, issuer_public_key, target_id, ca_private_key=None, signer_private_key=None, - user_mode=False): +def SCPv3( + dongle, + issuer_public_key, + target_id, + ca_private_key=None, + signer_private_key=None, + user_mode=False, +): if not user_mode: ca_sk = PrivateKey(bytes(ca_private_key)) - target = bytearray(struct.pack('>I', target_id)) + target = bytearray(struct.pack(">I", target_id)) scpv3 = 0x02 - apdu = bytearray([0xe0, 0x04, scpv3, 0x00]) + bytearray([len(target)]) + target + apdu = bytearray([0xE0, 0x04, scpv3, 0x00]) + bytearray([len(target)]) + target dongle.exchange(apdu) # Initialize authentication nonce = os.urandom(8) - apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce + apdu = bytearray([0xE0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce auth_info = dongle.exchange(apdu) device_nonce = auth_info[4:12] @@ -54,32 +64,53 @@ def SCPv3(dongle, issuer_public_key, target_id, ca_private_key=None, signer_priv signature = ca_sk.ecdsa_sign(bytes(data_to_sign)) signature = ca_sk.ecdsa_serialize(signature) certificate_id = 0x01 - certificate = bytearray([len(signer_pk)]) + signer_pk + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x00, certificate_id]) + bytearray([len(certificate)]) + certificate + certificate = ( + bytearray([len(signer_pk)]) + + signer_pk + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x00, certificate_id]) + + bytearray([len(certificate)]) + + certificate + ) dongle.exchange(apdu) # Validate signer ephemeral certificate ephemeral_private = PrivateKey() ephemeral_public = bytearray(ephemeral_private.pubkey.serialize(compressed=False)) - data_to_sign = bytes(bytearray([ephemeral_role]) + nonce + device_nonce + ephemeral_public) + data_to_sign = bytes( + bytearray([ephemeral_role]) + nonce + device_nonce + ephemeral_public + ) signature = signer_sk.ecdsa_sign(bytes(data_to_sign)) signature = signer_sk.ecdsa_serialize(signature) - certificate = bytearray([len(ephemeral_public)]) + ephemeral_public + bytearray([len(signature)]) + signature - apdu = bytearray([0xE0, 0x51, 0x80, certificate_id]) + bytearray([len(certificate)]) + certificate + certificate = ( + bytearray([len(ephemeral_public)]) + + ephemeral_public + + bytearray([len(signature)]) + + signature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, certificate_id]) + + bytearray([len(certificate)]) + + certificate + ) dongle.exchange(apdu) # Get device certificates issuer_pk = PublicKey(bytes(issuer_public_key), raw=True) - encrypted_certificate_static = dongle.exchange(bytearray.fromhex('E052000000')) + encrypted_certificate_static = dongle.exchange(bytearray.fromhex("E052000000")) # First extract the device ephemeral public key from the device ephemeral certificate - certificate_ephemeral = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) + certificate_ephemeral = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) - certificate_header, certificate_public_key, certificate_signature_array = \ + certificate_header, certificate_public_key, certificate_signature_array = ( extract_from_certificate(certificate_ephemeral) + ) # Check the certificate's header if not certificate_header == bytearray(): @@ -97,23 +128,43 @@ def SCPv3(dongle, issuer_public_key, target_id, ca_private_key=None, signer_priv certificate_signature_array = decrypt_certificate(certificate_signature_array, key) # Verify the device static certificate - certificate_static_header, certificate_static_public_key, certificate_static_signature_array = \ - extract_from_certificate(certificate_static) - certificate_signature = issuer_pk.ecdsa_deserialize(bytes(certificate_static_signature_array)) - certificate_signed_data = bytearray([CERT_ROLE_DEVICE]) + certificate_static_header + certificate_static_public_key - - if not issuer_pk.ecdsa_verify(bytes(certificate_signed_data), certificate_signature): + ( + certificate_static_header, + certificate_static_public_key, + certificate_static_signature_array, + ) = extract_from_certificate(certificate_static) + certificate_signature = issuer_pk.ecdsa_deserialize( + bytes(certificate_static_signature_array) + ) + certificate_signed_data = ( + bytearray([CERT_ROLE_DEVICE]) + + certificate_static_header + + certificate_static_public_key + ) + + if not issuer_pk.ecdsa_verify( + bytes(certificate_signed_data), certificate_signature + ): raise Exception("Device certificate not verified") # Verify the device ephemeral certificate device_pub_key = PublicKey(bytes(certificate_static_public_key), raw=True) - certificate_signature = device_pub_key.ecdsa_deserialize(bytes(certificate_signature_array)) - certificate_signed_data = bytearray([CERT_ROLE_DEVICE_EPHEMERAL]) + device_nonce + nonce + certificate_public_key - - if not device_pub_key.ecdsa_verify(bytes(certificate_signed_data), certificate_signature): + certificate_signature = device_pub_key.ecdsa_deserialize( + bytes(certificate_signature_array) + ) + certificate_signed_data = ( + bytearray([CERT_ROLE_DEVICE_EPHEMERAL]) + + device_nonce + + nonce + + certificate_public_key + ) + + if not device_pub_key.ecdsa_verify( + bytes(certificate_signed_data), certificate_signature + ): raise Exception("Device ephemeral certificate not verified") - dongle.exchange(bytearray.fromhex('E053000000')) + dongle.exchange(bytearray.fromhex("E053000000")) return secret, certificate_public_key @@ -127,33 +178,47 @@ def recoverValidate(loader, caKey, name, staticPrivateKey): # Validate provider's static certificate role = bytearray([CERT_ROLE_RECOVER_PROVIDER]) version = bytearray([CERT_FORMAT_VERSION]) - dataToSign = bytes(version + role + struct.pack('>B', len(name)) + name.encode() + - struct.pack('>B', len(providerPublicKey)) + providerPublicKey) + dataToSign = bytes( + version + + role + + struct.pack(">B", len(name)) + + name.encode() + + struct.pack(">B", len(providerPublicKey)) + + providerPublicKey + ) signature = caPrivateKey.ecdsa_sign(bytes(dataToSign)) signature = caPrivateKey.ecdsa_serialize(signature) - loader.recoverValidateCertificate(bytes(version), bytes(role), name, providerPublicKey, signature) + loader.recoverValidateCertificate( + bytes(version), bytes(role), name, providerPublicKey, signature + ) # Validate provider's ephemeral certificate ephemeralPrivate = PrivateKey() ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False)) role = bytearray([CERT_ROLE_RECOVER_PROVIDER_EPHEMERAL]) - dataToSign = bytes(version + role + struct.pack('>B', len(name)) + name.encode() + - struct.pack('>B', len(ephemeralPublic)) + ephemeralPublic) + dataToSign = bytes( + version + + role + + struct.pack(">B", len(name)) + + name.encode() + + struct.pack(">B", len(ephemeralPublic)) + + ephemeralPublic + ) signature = providerPrivateKey.ecdsa_sign(bytes(dataToSign)) signature = providerPrivateKey.ecdsa_serialize(signature) - loader.recoverValidateCertificate(bytes(version), bytes(role), name, ephemeralPublic, signature, True) + loader.recoverValidateCertificate( + bytes(version), bytes(role), name, ephemeralPublic, signature, True + ) return ephemeralPrivate, ephemeralPublic def recoverMutualAuth(privateKey, devicePublicKey): - # Compute the shared key pk = PublicKey(devicePublicKey, raw=True) secret = pk.ecdh(binascii.unhexlify(privateKey.serialize()), True) sharedKey = scp_derive_key(secret, 0, True) return sharedKey - diff --git a/ledgerblue/recoverRestore.py b/ledgerblue/recoverRestore.py index 540459b..41ffbd0 100755 --- a/ledgerblue/recoverRestore.py +++ b/ledgerblue/recoverRestore.py @@ -13,15 +13,41 @@ def get_argparser(): parser = argparse.ArgumentParser(description="Restore a seed given 2 shares.") - parser.add_argument("--targetId", help="The device's target ID (default is Nano X)", type=auto_int) - parser.add_argument("--rootPrivateKey", help="The private key of the Certificate Authority") - parser.add_argument("--issuerPublicKey", help="The public key of the Issuer (used to verify the certificate of " - "the device)") - parser.add_argument("-s", "--select", help="Select the backups to use", required = True, action="store", - dest="backups", type=str, nargs='*') - parser.add_argument("-c", "--configuration", help="Configuration file", default=None, required=True, action="store") - parser.add_argument("--gpg", help="Decrypt the backup data. Enter the email address associated to your gpg key", - default=None, action="store") + parser.add_argument( + "--targetId", help="The device's target ID (default is Nano X)", type=auto_int + ) + parser.add_argument( + "--rootPrivateKey", help="The private key of the Certificate Authority" + ) + parser.add_argument( + "--issuerPublicKey", + help="The public key of the Issuer (used to verify the certificate of " + "the device)", + ) + parser.add_argument( + "-s", + "--select", + help="Select the backups to use", + required=True, + action="store", + dest="backups", + type=str, + nargs="*", + ) + parser.add_argument( + "-c", + "--configuration", + help="Configuration file", + default=None, + required=True, + action="store", + ) + parser.add_argument( + "--gpg", + help="Decrypt the backup data. Enter the email address associated to your gpg key", + default=None, + action="store", + ) return parser @@ -29,13 +55,12 @@ def auto_int(x): return int(x, 0) -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() # Read the configuration file try: - with open(args.configuration,'r') as file: + with open(args.configuration, "r") as file: conf = json.load(file) except Exception as err: print(err) @@ -46,22 +71,28 @@ def auto_int(x): if args.rootPrivateKey is None: raise Exception("Missing Certificate Authority private key") if args.issuerPublicKey is None: - args.issuerPublicKey = '0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805' \ - '7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609' + args.issuerPublicKey = ( + "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805" + "7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609" + ) if not args.gpg: print("Reading your backups from unencrypted files") else: - home = os.path.join(os.environ['HOME'], ".gnupg") + home = os.path.join(os.environ["HOME"], ".gnupg") gpg = gnupg.GPG(gnupghome=home) dongle = getDongle(True) - orchestratorPrivateKey = conf['orchestrator']['key'] + orchestratorPrivateKey = conf["orchestrator"]["key"] # Execute device-orchestrator secure channel protocol - secret, devicePublicKey = SCPv3(dongle, bytearray.fromhex(args.issuerPublicKey), args.targetId, - bytearray.fromhex(args.rootPrivateKey), - bytearray.fromhex(orchestratorPrivateKey)) - loader = HexLoader(dongle, 0xe0, True, secret, scpv3=True) + secret, devicePublicKey = SCPv3( + dongle, + bytearray.fromhex(args.issuerPublicKey), + args.targetId, + bytearray.fromhex(args.rootPrivateKey), + bytearray.fromhex(orchestratorPrivateKey), + ) + loader = HexLoader(dongle, 0xE0, True, secret, scpv3=True) # Initialize the session with the info from the configuration file recoverSession = Recover(conf) @@ -70,7 +101,7 @@ def auto_int(x): for backup in args.backups: # Read the backup file try: - with open(backup, 'r') as file: + with open(backup, "r") as file: if args.gpg: enc_restore_data = file.read() dec_data = gpg.decrypt(enc_restore_data) @@ -81,14 +112,18 @@ def auto_int(x): print(err) exit() - for p in conf['providers']: - if p['name'] == list(restore_data.keys())[0]: + for p in conf["providers"]: + if p["name"] == list(restore_data.keys())[0]: break - name = p['name'] - privateKey = p['key'] + name = p["name"] + privateKey = p["key"] # Execute device-provider mutual authentication - providerSk, providerPk = recoverValidate(loader, bytearray.fromhex(args.rootPrivateKey), name, - bytearray.fromhex(privateKey)) + providerSk, providerPk = recoverValidate( + loader, + bytearray.fromhex(args.rootPrivateKey), + name, + bytearray.fromhex(privateKey), + ) loader.recoverMutualAuth() sharedKey = recoverMutualAuth(providerSk, devicePublicKey) @@ -110,18 +145,24 @@ def auto_int(x): response = loader.recoverValidateHash(tag, ciphertext) cipher = AES.new(sharedKey, AES.MODE_SIV) clear_response = cipher.decrypt_and_verify(response[16:], response[:16]) - assert clear_response == b'Confirm restore' + assert clear_response == b"Confirm restore" - share = restore_data[name]['share'] - point = restore_data[name]['point'] - commitments = restore_data[name]['commitments'] - shareAndIndex = share + restore_data[name]['index'] - numberOfWords = restore_data[name]['words_number'] + share = restore_data[name]["share"] + point = restore_data[name]["point"] + commitments = restore_data[name]["commitments"] + shareAndIndex = share + restore_data[name]["index"] + numberOfWords = restore_data[name]["words_number"] - shareCommit = recoverSession.recoverShareCommit(bytearray.fromhex(point), bytearray.fromhex(share)) + shareCommit = recoverSession.recoverShareCommit( + bytearray.fromhex(point), bytearray.fromhex(share) + ) # Validate share's commitment - recoverSession.recoverValidateCommit(loader, bytearray.fromhex(commitments), shareCommit) + recoverSession.recoverValidateCommit( + loader, bytearray.fromhex(commitments), shareCommit + ) # Send the share to the device (encrypted with the device-provider shared key) - recoverSession.recoverRestoreSeed(loader, bytearray.fromhex(shareAndIndex), numberOfWords) + recoverSession.recoverRestoreSeed( + loader, bytearray.fromhex(shareAndIndex), numberOfWords + ) diff --git a/ledgerblue/recoverSCP.py b/ledgerblue/recoverSCP.py index 6e2b299..bd84bfc 100755 --- a/ledgerblue/recoverSCP.py +++ b/ledgerblue/recoverSCP.py @@ -15,33 +15,33 @@ def decrypt_certificate(encrypted_certificate, key): def extract_from_certificate(certificate): offset = 1 - certificate_header = certificate[offset: offset + certificate[offset - 1]] + certificate_header = certificate[offset : offset + certificate[offset - 1]] offset += certificate[offset - 1] + 1 - certificate_public_key = certificate[offset: offset + certificate[offset - 1]] + certificate_public_key = certificate[offset : offset + certificate[offset - 1]] offset += certificate[offset - 1] + 1 - certificate_signature_array = certificate[offset: offset + certificate[offset - 1]] + certificate_signature_array = certificate[offset : offset + certificate[offset - 1]] return certificate_header, certificate_public_key, certificate_signature_array def scp_derive_key(ecdh_secret, keyindex, scpv3=False): if scpv3: - mac_block = b'\x01' * 16 + mac_block = b"\x01" * 16 cipher = AES.new(ecdh_secret, AES.MODE_ECB) mac_key = cipher.encrypt(mac_block) - enc_block = b'\x02' * 16 + enc_block = b"\x02" * 16 cipher = AES.new(ecdh_secret, AES.MODE_ECB) enc_key = cipher.encrypt(enc_block) return mac_key + enc_key retry = 0 # di = sha256(i || retrycounter || ecdh secret) while True: - sha256 = hashlib.new('sha256') + sha256 = hashlib.new("sha256") sha256.update(struct.pack(">IB", keyindex, retry)) sha256.update(ecdh_secret) # compare di with order - CURVE_SECP256K1 = Curve.get_curve('secp256k1') - if int.from_bytes(sha256.digest(), 'big') < CURVE_SECP256K1.order: + CURVE_SECP256K1 = Curve.get_curve("secp256k1") + if int.from_bytes(sha256.digest(), "big") < CURVE_SECP256K1.order: break # regenerate a new di satisfying order upper bound retry += 1 @@ -50,7 +50,6 @@ def scp_derive_key(ecdh_secret, keyindex, scpv3=False): privkey = PrivateKey(bytes(sha256.digest())) pubkey = bytearray(privkey.pubkey.serialize(compressed=False)) # ki = sha256(Pi) - sha256 = hashlib.new('sha256') + sha256 = hashlib.new("sha256") sha256.update(pubkey) return sha256.digest() - diff --git a/ledgerblue/recoverSetCA.py b/ledgerblue/recoverSetCA.py index b0bbf8b..4859bd7 100755 --- a/ledgerblue/recoverSetCA.py +++ b/ledgerblue/recoverSetCA.py @@ -7,14 +7,28 @@ def get_argparser(): - parser = argparse.ArgumentParser(description="Set a custom Certificate Authority to backup/restore a seed.") - parser.add_argument("--targetId", help="The device's target ID (default is Nano X)", type=auto_int) + parser = argparse.ArgumentParser( + description="Set a custom Certificate Authority to backup/restore a seed." + ) + parser.add_argument( + "--targetId", help="The device's target ID (default is Nano X)", type=auto_int + ) parser.add_argument("--name", help="The certificate name", required=True) - parser.add_argument("--issuerPublicKey", help="The public key of the Issuer (used to verify the certificate of " - "the device)") - parser.add_argument("--rootPrivateKey", help="The private key of the Signer used to establish a secure channel " - "(otherwise a random one will be generated)") - parser.add_argument("--caPublicKey", help="The Custom CA's public key to be enrolled (hex encoded)", required=True) + parser.add_argument( + "--issuerPublicKey", + help="The public key of the Issuer (used to verify the certificate of " + "the device)", + ) + parser.add_argument( + "--rootPrivateKey", + help="The private key of the Signer used to establish a secure channel " + "(otherwise a random one will be generated)", + ) + parser.add_argument( + "--caPublicKey", + help="The Custom CA's public key to be enrolled (hex encoded)", + required=True, + ) return parser @@ -22,8 +36,7 @@ def auto_int(x): return int(x, 0) -if __name__ == '__main__': - +if __name__ == "__main__": args = get_argparser().parse_args() if args.targetId is None: @@ -31,8 +44,10 @@ def auto_int(x): if args.name is None: raise Exception("Missing certificate's name") if args.issuerPublicKey is None: - args.issuerPublicKey = '0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805' \ - '7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609' + args.issuerPublicKey = ( + "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f81805" + "7224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609" + ) if args.rootPrivateKey is None: privateKey = PrivateKey() publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) @@ -45,10 +60,15 @@ def auto_int(x): dongle = getDongle(True) # Execute secure channel protocol (new version) - secret, devicePublicKey = SCPv3(dongle, bytearray.fromhex(args.issuerPublicKey), args.targetId, - None, bytearray.fromhex(args.rootPrivateKey), - user_mode=True) - loader = HexLoader(dongle, 0xe0, True, secret, scpv3=True) + secret, devicePublicKey = SCPv3( + dongle, + bytearray.fromhex(args.issuerPublicKey), + args.targetId, + None, + bytearray.fromhex(args.rootPrivateKey), + user_mode=True, + ) + loader = HexLoader(dongle, 0xE0, True, secret, scpv3=True) # Load the Certificate Authority's public key loader.recoverSetCA(args.name, publicKey) diff --git a/ledgerblue/recoverUtil.py b/ledgerblue/recoverUtil.py index cd8e922..be85a1a 100644 --- a/ledgerblue/recoverUtil.py +++ b/ledgerblue/recoverUtil.py @@ -18,14 +18,16 @@ class Recover: def __init__(self, conf): self.sharedKey = bytes() - self.backupId = bytearray.fromhex(conf['backup_info']['backup_id']) - self.backupName = conf['backup_info']['backup_name'] - user = conf['user_info'] - self.firstName = user['first_name'] - self.lastName = user['last_name'] - self.birthDate = user['birth'] - self.birthPlace = user['city'] - self.userInfo = self.firstName + self.lastName + self.birthDate + self.birthPlace + self.backupId = bytearray.fromhex(conf["backup_info"]["backup_id"]) + self.backupName = conf["backup_info"]["backup_name"] + user = conf["user_info"] + self.firstName = user["first_name"] + self.lastName = user["last_name"] + self.birthDate = user["birth"] + self.birthPlace = user["city"] + self.userInfo = ( + self.firstName + self.lastName + self.birthDate + self.birthPlace + ) self.f_tag = FIRST_NAME_TAG self.n_tag = NAME_TAG self.d_tag = DATE_OF_BIRTH_TAG @@ -49,10 +51,16 @@ def recoverValidateCommit(self, loader, commits, shareCommit): loader.recoverValidateCommit(0x3, None, tag, ciphertext) def recoverShareCommit(self, point, share): - Q = Point(int.from_bytes(point[:self.VSS.domain_len], 'big'), - int.from_bytes(point[self.VSS.domain_len:2 * self.VSS.domain_len], 'big'), self.VSS.curve) + Q = Point( + int.from_bytes(point[: self.VSS.domain_len], "big"), + int.from_bytes(point[self.VSS.domain_len : 2 * self.VSS.domain_len], "big"), + self.VSS.curve, + ) P = self.VSS.pedersen_share_commit(Q, share) - return bytearray(P.x.to_bytes(self.VSS.domain_len, 'big') + P.y.to_bytes(self.VSS.domain_len, 'big')) + return bytearray( + P.x.to_bytes(self.VSS.domain_len, "big") + + P.y.to_bytes(self.VSS.domain_len, "big") + ) def recoverValidateShareCommit(self, loader, shareCommit): dataToHash = bytes(shareCommit) @@ -66,13 +74,35 @@ def recoverValidateShareCommit(self, loader, shareCommit): def recoverVerifyCommitments(self, share, idx, commitments, point): point_len = 2 * self.VSS.domain_len - commitsPoints = [Point(int.from_bytes(commitments[i * point_len:i * point_len + self.VSS.domain_len], 'big'), - int.from_bytes(commitments[i * point_len + self.VSS.domain_len: i * point_len + 2 * self.VSS.domain_len], - 'big'), self.VSS.curve) for i in range(2)] - Q = Point(int.from_bytes(point[:self.VSS.domain_len], 'big'), - int.from_bytes(point[self.VSS.domain_len:2 * self.VSS.domain_len], 'big'), self.VSS.curve) - result, shareCommitPoint = self.VSS.pedersen_verify_commit(Q, share, idx, commitsPoints) - shareCommit = bytearray(shareCommitPoint.x.to_bytes(self.VSS.domain_len, 'big') + shareCommitPoint.y.to_bytes(self.VSS.domain_len, 'big')) + commitsPoints = [ + Point( + int.from_bytes( + commitments[i * point_len : i * point_len + self.VSS.domain_len], + "big", + ), + int.from_bytes( + commitments[ + i * point_len + self.VSS.domain_len : i * point_len + + 2 * self.VSS.domain_len + ], + "big", + ), + self.VSS.curve, + ) + for i in range(2) + ] + Q = Point( + int.from_bytes(point[: self.VSS.domain_len], "big"), + int.from_bytes(point[self.VSS.domain_len : 2 * self.VSS.domain_len], "big"), + self.VSS.curve, + ) + result, shareCommitPoint = self.VSS.pedersen_verify_commit( + Q, share, idx, commitsPoints + ) + shareCommit = bytearray( + shareCommitPoint.x.to_bytes(self.VSS.domain_len, "big") + + shareCommitPoint.y.to_bytes(self.VSS.domain_len, "big") + ) return result, shareCommit def recoverPrepareDataHash(self, publicKey): @@ -92,7 +122,7 @@ def recoverBackupProviderDecrypt(self, encryptedData): plaintext = cipher.decrypt_and_verify(encryptedData[16:], encryptedData[:16]) share = plaintext[:96] - idx = int.from_bytes(plaintext[96:100], 'little') + idx = int.from_bytes(plaintext[96:100], "little") deletePublicKey = plaintext[100:165] commitHash = plaintext[165:] @@ -104,7 +134,9 @@ def recoverDeleteBackup(self, loader, backupPublicKey): ciphertext, tag = cipher.encrypt_and_digest(nonce) encryptedSignature = loader.recoverDeleteBackup(tag, ciphertext) cipher = AES.new(self.sharedKey, AES.MODE_SIV) - signature = cipher.decrypt_and_verify(encryptedSignature[16:], encryptedSignature[:16]) + signature = cipher.decrypt_and_verify( + encryptedSignature[16:], encryptedSignature[:16] + ) verifyKey = PublicKey(bytes(backupPublicKey), raw=True) signature = verifyKey.ecdsa_deserialize(signature) if not verifyKey.ecdsa_verify(nonce, signature): @@ -118,12 +150,13 @@ def recoverPrepareDataIdv(self, delete=False): (self.birthDate, self.d_tag), (self.birthPlace, self.c_tag), ] - flow = b'\x01' if delete else b'\x00' + flow = b"\x01" if delete else b"\x00" def pack(tup): identifier, tag = tup data = struct.pack(">B", tag) if tag is not None else b"" - return data + struct.pack(">B", len(identifier.encode())) + identifier.encode() + return ( + data + struct.pack(">B", len(identifier.encode())) + identifier.encode() + ) return self.backupId + b"".join(map(pack, identifiers)) + flow - diff --git a/ledgerblue/resetCustomCA.py b/ledgerblue/resetCustomCA.py index 5727025..21467a2 100644 --- a/ledgerblue/resetCustomCA.py +++ b/ledgerblue/resetCustomCA.py @@ -19,40 +19,54 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="""Remove all Custom CA public keys previously enrolled onto the -device.""") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - return parser + parser = argparse.ArgumentParser( + description="""Remove all Custom CA public keys previously enrolled onto the +device.""" + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) -if __name__ == '__main__': - import binascii - from .comm import getDongle - from .deployed import getDeployedSecretV2 - from .ecWrapper import PrivateKey - from .hexLoader import HexLoader +if __name__ == "__main__": + import binascii - args = get_argparser().parse_args() + from .comm import getDongle + from .deployed import getDeployedSecretV2 + from .ecWrapper import PrivateKey + from .hexLoader import HexLoader - if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() + args = get_argparser().parse_args() + if args.rootPrivateKey is None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() - dongle = getDongle(args.apdu) + dongle = getDongle(args.apdu) - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - loader = HexLoader(dongle, 0xe0, True, secret) + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + loader = HexLoader(dongle, 0xE0, True, secret) - loader.resetCustomCA() + loader.resetCustomCA() - dongle.close() + dongle.close() diff --git a/ledgerblue/runApp.py b/ledgerblue/runApp.py index be96f13..08f20fc 100644 --- a/ledgerblue/runApp.py +++ b/ledgerblue/runApp.py @@ -22,11 +22,21 @@ def get_argparser(): parser = argparse.ArgumentParser("Run an application on the device.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - parser.add_argument("--appName", help="The name of the application to run", required=True) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + parser.add_argument( + "--appName", help="The name of the application to run", required=True + ) return parser @@ -34,7 +44,7 @@ def auto_int(x): return int(x, 0) -if __name__ == '__main__': +if __name__ == "__main__": from .ecWrapper import PrivateKey from .comm import getDongle from .hexLoader import HexLoader @@ -50,7 +60,7 @@ def auto_int(x): dongle = getDongle(args.apdu) - loader = HexLoader(dongle, 0xe0) + loader = HexLoader(dongle, 0xE0) loader.runApp(args.appName.encode()) diff --git a/ledgerblue/runScript.py b/ledgerblue/runScript.py index e66cec9..f41d188 100644 --- a/ledgerblue/runScript.py +++ b/ledgerblue/runScript.py @@ -19,80 +19,98 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="""Read a sequence of command APDUs from a file and send them to the -device. The file must be formatted as hex, with one CAPDU per line.""") - parser.add_argument("--fileName", help="The name of the APDU script to load") - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--scp", help="Open a Secure Channel to exchange APDU", action='store_true') - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Nano S). If --elfFile is used, the targetId from the ELF file will be used instead.", type=auto_int, default=0x31100002) - parser.add_argument("--elfFile", help="ELF file from which the target ID is fetched. Overrides '--targetId'") - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - return parser + parser = argparse.ArgumentParser( + description="""Read a sequence of command APDUs from a file and send them to the +device. The file must be formatted as hex, with one CAPDU per line.""" + ) + parser.add_argument("--fileName", help="The name of the APDU script to load") + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--scp", help="Open a Secure Channel to exchange APDU", action="store_true" + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Nano S). If --elfFile is used, the targetId from the ELF file will be used instead.", + type=auto_int, + default=0x31100002, + ) + parser.add_argument( + "--elfFile", + help="ELF file from which the target ID is fetched. Overrides '--targetId'", + ) + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + return parser + def auto_int(x): - return int(x, 0) - -if __name__ == '__main__': - import binascii - import sys - - from .comm import getDongle - from .deployed import getDeployedSecretV2 - from .ecWrapper import PrivateKey - from .hexLoader import HexLoader - from .readElfMetadata import get_target_id_from_elf - - args = get_argparser().parse_args() - - if not args.fileName: - #raise Exception("Missing fileName") - file = sys.stdin - else: - file = open(args.fileName, "r") - - - class SCP: - - def __init__(self, dongle, targetId, rootPrivateKey): - secret = getDeployedSecretV2(dongle, rootPrivateKey, targetId) - self.loader = HexLoader(dongle, 0xe0, True, secret) - - def encryptAES(self, data): - return self.loader.scpWrap(data) - - def decryptAES(self, data): - return self.loader.scpUnwrap(data) - - dongle = getDongle(args.apdu) - - targetId = args.targetId - - if args.elfFile: - targetId = auto_int(get_target_id_from_elf(args.elfFile)) - - if args.scp: - if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() - scp = SCP(dongle, targetId, bytearray.fromhex(args.rootPrivateKey)) - - for data in file: - data = binascii.unhexlify(data.replace("\n", "")) - if len(data) < 5: - continue - if args.scp: - apduData = data[5:] - apduData = scp.encryptAES(bytes(apduData)) - apdu = bytearray([data[0], data[1], data[2], data[3], len(apduData)]) + bytearray(apduData) - result = dongle.exchange(apdu) - result = scp.decryptAES(result) - else: - result = dongle.exchange(bytearray(data)) - if args.apdu: - print("<= Clear " + str(result)) - - dongle.close() + return int(x, 0) + + +if __name__ == "__main__": + import binascii + import sys + + from .comm import getDongle + from .deployed import getDeployedSecretV2 + from .ecWrapper import PrivateKey + from .hexLoader import HexLoader + from .readElfMetadata import get_target_id_from_elf + + args = get_argparser().parse_args() + + if not args.fileName: + # raise Exception("Missing fileName") + file = sys.stdin + else: + file = open(args.fileName, "r") + + class SCP: + def __init__(self, dongle, targetId, rootPrivateKey): + secret = getDeployedSecretV2(dongle, rootPrivateKey, targetId) + self.loader = HexLoader(dongle, 0xE0, True, secret) + + def encryptAES(self, data): + return self.loader.scpWrap(data) + + def decryptAES(self, data): + return self.loader.scpUnwrap(data) + + dongle = getDongle(args.apdu) + + targetId = args.targetId + + if args.elfFile: + targetId = auto_int(get_target_id_from_elf(args.elfFile)) + + if args.scp: + if args.rootPrivateKey is None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() + scp = SCP(dongle, targetId, bytearray.fromhex(args.rootPrivateKey)) + + for data in file: + data = binascii.unhexlify(data.replace("\n", "")) + if len(data) < 5: + continue + if args.scp: + apduData = data[5:] + apduData = scp.encryptAES(bytes(apduData)) + apdu = bytearray( + [data[0], data[1], data[2], data[3], len(apduData)] + ) + bytearray(apduData) + result = dongle.exchange(apdu) + result = scp.decryptAES(result) + else: + result = dongle.exchange(bytearray(data)) + if args.apdu: + print("<= Clear " + str(result)) + + dongle.close() diff --git a/ledgerblue/setupCustomCA.py b/ledgerblue/setupCustomCA.py index 8124b83..28b885e 100644 --- a/ledgerblue/setupCustomCA.py +++ b/ledgerblue/setupCustomCA.py @@ -19,44 +19,66 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Enroll a Custom CA public key onto the device.") - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise -a random one will be generated)""") - parser.add_argument("--public", help="The Custom CA public key to be enrolled (hex encoded)", required=True) - parser.add_argument("--name", help="""The name to assign to the Custom CA (this will be displayed on screen upon -auth requests)""", required=True) - return parser + parser = argparse.ArgumentParser( + description="Enroll a Custom CA public key onto the device." + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--rootPrivateKey", + help="""The Signer private key used to establish a Secure Channel (otherwise +a random one will be generated)""", + ) + parser.add_argument( + "--public", + help="The Custom CA public key to be enrolled (hex encoded)", + required=True, + ) + parser.add_argument( + "--name", + help="""The name to assign to the Custom CA (this will be displayed on screen upon +auth requests)""", + required=True, + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) -if __name__ == '__main__': - import binascii - from .comm import getDongle - from .deployed import getDeployedSecretV2 - from .ecWrapper import PrivateKey - from .hexLoader import HexLoader +if __name__ == "__main__": + import binascii - args = get_argparser().parse_args() + from .comm import getDongle + from .deployed import getDeployedSecretV2 + from .ecWrapper import PrivateKey + from .hexLoader import HexLoader - if args.rootPrivateKey is None: - privateKey = PrivateKey() - publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) - print("Generated random root public key : %s" % publicKey) - args.rootPrivateKey = privateKey.serialize() + args = get_argparser().parse_args() - public = bytearray.fromhex(args.public) + if args.rootPrivateKey is None: + privateKey = PrivateKey() + publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False)) + print("Generated random root public key : %s" % publicKey) + args.rootPrivateKey = privateKey.serialize() + public = bytearray.fromhex(args.public) - dongle = getDongle(args.apdu) + dongle = getDongle(args.apdu) - secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) - loader = HexLoader(dongle, 0xe0, True, secret) + secret = getDeployedSecretV2( + dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId + ) + loader = HexLoader(dongle, 0xE0, True, secret) - loader.setupCustomCA(args.name, public) + loader.setupCustomCA(args.name, public) - dongle.close() + dongle.close() diff --git a/ledgerblue/updateFirmware.py b/ledgerblue/updateFirmware.py index 59103f3..4b8ad8a 100644 --- a/ledgerblue/updateFirmware.py +++ b/ledgerblue/updateFirmware.py @@ -20,187 +20,227 @@ import argparse import ssl + def get_argparser(): - parser = argparse.ArgumentParser("Update the firmware by using Ledger to open a Secure Channel.") - parser.add_argument("--url", help="Server URL", default="https://hsmprod.hardwarewallet.com/hsm/process") - parser.add_argument("--bypass-ssl-check", help="Keep going even if remote certificate verification fails", action='store_true', default=False) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific -Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11") - parser.add_argument("--firmware", help="A reference to the firmware to load", required=True) - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--firmwareKey", help="A reference to the firmware key to use", required=True) - return parser + parser = argparse.ArgumentParser( + "Update the firmware by using Ledger to open a Secure Channel." + ) + parser.add_argument( + "--url", + help="Server URL", + default="https://hsmprod.hardwarewallet.com/hsm/process", + ) + parser.add_argument( + "--bypass-ssl-check", + help="Keep going even if remote certificate verification fails", + action="store_true", + default=False, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--perso", + help="""A reference to the personalization key; this is a reference to the specific +Issuer keypair used by Ledger to sign the device's Issuer Certificate""", + default="perso_11", + ) + parser.add_argument( + "--firmware", help="A reference to the firmware to load", required=True + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--firmwareKey", help="A reference to the firmware key to use", required=True + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def serverQuery(request, url): - data = request.SerializeToString() - urll = urlparse.urlparse(args.url) - req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream" }) - if args.bypass_ssl_check: - res = urllib2.urlopen(req, context=ssl._create_unverified_context()) - else: - res = urllib2.urlopen(req) - data = res.read() - response = Response() - response.ParseFromString(data) - if len(response.exception) != 0: - raise Exception(response.exception) - return response - -if __name__ == '__main__': - import struct - import urllib.request as urllib2 - import urllib.parse as urlparse - from .BlueHSMServer_pb2 import Request, Response - from .comm import getDongle - - args = get_argparser().parse_args() - - dongle = getDongle(args.apdu) - - # Identify - - targetid = bytearray(struct.pack('>I', args.targetId)) - apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid - dongle.exchange(apdu) - - # Get nonce and ephemeral key - - request = Request() - request.reference = "distributeFirmware11_scan" - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "persoKey" - parameter.name = args.perso - if args.targetId&0xF >= 0x3: - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "scpv2" - parameter.name = "dummy" - request.largeStack = True - - response = serverQuery(request, args.url) - - offset = 0 - - remotePublicKey = response.response[offset : offset + 65] - offset += 65 - nonce = response.response[offset : offset + 8] - if args.targetId&0xF >= 0x3: - offset += 8 - masterPublicKey = response.response[offset : offset + 65] - offset += 65 - masterPublicKeySignatureLength = response.response[offset + 1] + 2 - masterPublicKeySignature = response.response[offset : offset + masterPublicKeySignatureLength] - - # Initialize chain - - apdu = bytearray([0xe0, 0x50, 0x00, 0x00, 0x08]) + nonce - deviceInit = dongle.exchange(apdu) - deviceNonce = deviceInit[4 : 4 + 8] - - # Get remote certificate - - request = Request() - request.reference = "distributeFirmware11_scan" - request.id = response.id - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "persoKey" - parameter.name = args.perso - request.parameters = bytes(deviceNonce) - request.largeStack = True - - response = serverQuery(request, args.url) - - offset = 0 - - responseLength = response.response[offset + 1] - remotePublicKeySignatureLength = responseLength + 2 - remotePublicKeySignature = response.response[offset : offset + remotePublicKeySignatureLength] - - certificate = bytearray([len(remotePublicKey)]) + remotePublicKey + bytearray([len(remotePublicKeySignature)]) + remotePublicKeySignature - apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate - dongle.exchange(apdu) - - # Walk the chain - - index = 0 - while True: - if index == 0: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000'))) - elif index == 1: - certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000'))) - else: - break - if len(certificate) == 0: - break - request = Request() - request.reference = "distributeFirmware11_scan" - request.id = response.id - request.parameters = bytes(certificate) - request.largeStack = True - serverQuery(request, args.url) - index += 1 - - # Commit agreement and send firmware - - request = Request() - request.reference = "distributeFirmware11_scan" - if args.targetId&0xF >= 0x3: - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "scpv2" - parameter.name = "dummy" - request.id = response.id - request.largeStack = True - - response = serverQuery(request, args.url) - responseData = bytearray(response.response) - - dongle.exchange(bytearray.fromhex('E053000000')) - - for i in range(100): - if len(responseData) == 0: - break - if bytes(responseData[0:4]) == b"SECU": - raise Exception("Security exception " + chr(responseData[4])) - - responseData = dongle.exchange(responseData) - - request = Request() - request.reference = "distributeFirmware11_scan" - request.parameters = b"\xFF" + b"\xFF" + bytes(responseData) - request.id = response.id - request.largeStack = True - - response = serverQuery(request, args.url) - responseData = bytearray(response.response) - - - request = Request() - request.reference = "distributeFirmware11_scan" - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "firmware" - parameter.name = args.firmware - parameter = request.remote_parameters.add() - parameter.local = False - parameter.alias = "firmwareKey" - parameter.name = args.firmwareKey - request.id = response.id - request.largeStack = True - - response = serverQuery(request, args.url) - responseData = bytearray(response.response) - - offset = 0 - while offset < len(responseData): - apdu = responseData[offset : offset + 5 + responseData[offset + 4]] - dongle.exchange(apdu) - offset += 5 + responseData[offset + 4] - - dongle.close() + data = request.SerializeToString() + urll = urlparse.urlparse(args.url) + req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream"}) + if args.bypass_ssl_check: + res = urllib2.urlopen(req, context=ssl._create_unverified_context()) + else: + res = urllib2.urlopen(req) + data = res.read() + response = Response() + response.ParseFromString(data) + if len(response.exception) != 0: + raise Exception(response.exception) + return response + + +if __name__ == "__main__": + import struct + import urllib.request as urllib2 + import urllib.parse as urlparse + from .BlueHSMServer_pb2 import Request, Response + from .comm import getDongle + + args = get_argparser().parse_args() + + dongle = getDongle(args.apdu) + + # Identify + + targetid = bytearray(struct.pack(">I", args.targetId)) + apdu = bytearray([0xE0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid + dongle.exchange(apdu) + + # Get nonce and ephemeral key + + request = Request() + request.reference = "distributeFirmware11_scan" + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "persoKey" + parameter.name = args.perso + if args.targetId & 0xF >= 0x3: + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "scpv2" + parameter.name = "dummy" + request.largeStack = True + + response = serverQuery(request, args.url) + + offset = 0 + + remotePublicKey = response.response[offset : offset + 65] + offset += 65 + nonce = response.response[offset : offset + 8] + if args.targetId & 0xF >= 0x3: + offset += 8 + masterPublicKey = response.response[offset : offset + 65] + offset += 65 + masterPublicKeySignatureLength = response.response[offset + 1] + 2 + masterPublicKeySignature = response.response[ + offset : offset + masterPublicKeySignatureLength + ] + + # Initialize chain + + apdu = bytearray([0xE0, 0x50, 0x00, 0x00, 0x08]) + nonce + deviceInit = dongle.exchange(apdu) + deviceNonce = deviceInit[4 : 4 + 8] + + # Get remote certificate + + request = Request() + request.reference = "distributeFirmware11_scan" + request.id = response.id + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "persoKey" + parameter.name = args.perso + request.parameters = bytes(deviceNonce) + request.largeStack = True + + response = serverQuery(request, args.url) + + offset = 0 + + responseLength = response.response[offset + 1] + remotePublicKeySignatureLength = responseLength + 2 + remotePublicKeySignature = response.response[ + offset : offset + remotePublicKeySignatureLength + ] + + certificate = ( + bytearray([len(remotePublicKey)]) + + remotePublicKey + + bytearray([len(remotePublicKeySignature)]) + + remotePublicKeySignature + ) + apdu = ( + bytearray([0xE0, 0x51, 0x80, 0x00]) + + bytearray([len(certificate)]) + + certificate + ) + dongle.exchange(apdu) + + # Walk the chain + + index = 0 + while True: + if index == 0: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052000000"))) + elif index == 1: + certificate = bytearray(dongle.exchange(bytearray.fromhex("E052800000"))) + else: + break + if len(certificate) == 0: + break + request = Request() + request.reference = "distributeFirmware11_scan" + request.id = response.id + request.parameters = bytes(certificate) + request.largeStack = True + serverQuery(request, args.url) + index += 1 + + # Commit agreement and send firmware + + request = Request() + request.reference = "distributeFirmware11_scan" + if args.targetId & 0xF >= 0x3: + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "scpv2" + parameter.name = "dummy" + request.id = response.id + request.largeStack = True + + response = serverQuery(request, args.url) + responseData = bytearray(response.response) + + dongle.exchange(bytearray.fromhex("E053000000")) + + for i in range(100): + if len(responseData) == 0: + break + if bytes(responseData[0:4]) == b"SECU": + raise Exception("Security exception " + chr(responseData[4])) + + responseData = dongle.exchange(responseData) + + request = Request() + request.reference = "distributeFirmware11_scan" + request.parameters = b"\xff" + b"\xff" + bytes(responseData) + request.id = response.id + request.largeStack = True + + response = serverQuery(request, args.url) + responseData = bytearray(response.response) + + request = Request() + request.reference = "distributeFirmware11_scan" + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "firmware" + parameter.name = args.firmware + parameter = request.remote_parameters.add() + parameter.local = False + parameter.alias = "firmwareKey" + parameter.name = args.firmwareKey + request.id = response.id + request.largeStack = True + + response = serverQuery(request, args.url) + responseData = bytearray(response.response) + + offset = 0 + while offset < len(responseData): + apdu = responseData[offset : offset + 5 + responseData[offset + 4]] + dongle.exchange(apdu) + offset += 5 + responseData[offset + 4] + + dongle.close() diff --git a/ledgerblue/updateFirmware2.py b/ledgerblue/updateFirmware2.py index b2ee527..4e851c9 100644 --- a/ledgerblue/updateFirmware2.py +++ b/ledgerblue/updateFirmware2.py @@ -19,84 +19,114 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser("Update the firmware by using Ledger to open a Secure Channel.") - parser.add_argument("--url", help="Websocket URL", default="wss://scriptrunner.api.live.ledger.com/update/install") - parser.add_argument("--bypass-ssl-check", help="Keep going even if remote certificate verification fails", action='store_true', default=False) - parser.add_argument("--apdu", help="Display APDU log", action='store_true') - parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific -Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11") - parser.add_argument("--firmware", help="A reference to the firmware to load", required=True) - parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int, default=0x31000002) - parser.add_argument("--firmwareKey", help="A reference to the firmware key to use", required=True) - return parser + parser = argparse.ArgumentParser( + "Update the firmware by using Ledger to open a Secure Channel." + ) + parser.add_argument( + "--url", + help="Websocket URL", + default="wss://scriptrunner.api.live.ledger.com/update/install", + ) + parser.add_argument( + "--bypass-ssl-check", + help="Keep going even if remote certificate verification fails", + action="store_true", + default=False, + ) + parser.add_argument("--apdu", help="Display APDU log", action="store_true") + parser.add_argument( + "--perso", + help="""A reference to the personalization key; this is a reference to the specific +Issuer keypair used by Ledger to sign the device's Issuer Certificate""", + default="perso_11", + ) + parser.add_argument( + "--firmware", help="A reference to the firmware to load", required=True + ) + parser.add_argument( + "--targetId", + help="The device's target ID (default is Ledger Blue)", + type=auto_int, + default=0x31000002, + ) + parser.add_argument( + "--firmwareKey", help="A reference to the firmware key to use", required=True + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + def process(dongle, request): - response = {} - apdusList = [] - try: - response['nonce'] = request['nonce'] - if request['query'] == "exchange": - apdusList.append(binascii.unhexlify(request['data'])) - elif request['query'] == "bulk": - for apdu in request['data']: - apdusList.append(binascii.unhexlify(apdu)) - else: - response['response'] = "unsupported" - except: - response['response'] = "parse error" - - if len(apdusList) != 0: - try: - for apdu in apdusList: - response['data'] = dongle.exchange(apdu).hex() - response['response'] = "success" - except: - response['response'] = "I/O" # or error, and SW in data - - return response - -if __name__ == '__main__': - import urllib.parse as urlparse - from .comm import getDongle - from websocket import create_connection - import json - import binascii - import ssl - - args = get_argparser().parse_args() - - dongle = getDongle(args.apdu) - - url = args.url - queryParameters = {} - queryParameters['targetId'] = args.targetId - queryParameters['firmware'] = args.firmware - queryParameters['firmwareKey'] = args.firmwareKey - queryParameters['perso'] = args.perso - queryString = urlparse.urlencode(queryParameters) - if args.bypass_ssl_check: - # SEE: https://docs.python.org/3/library/ssl.html#ssl.CERT_NONE - # According to the documentation: - # > With client-side sockets, just about any cert is accepted. Validation errors, such - # > as untrusted or expired cert, are ignored and do not abort the TLS/SSL handshake. - sslopt = { "cert_reqs": ssl.CERT_NONE } - else: - sslopt = {} - ws = create_connection(args.url + '?' + queryString, sslopt=sslopt) - while True: - result = json.loads(ws.recv()) - if result['query'] == 'success': - break - if result['query'] == 'error': - raise Exception(result['data'] + " on " + result['uuid'] + "/" + result['session']) - response = process(dongle, result) - ws.send(json.dumps(response)) - ws.close() - - print("Script executed successfully") - - dongle.close() + response = {} + apdusList = [] + try: + response["nonce"] = request["nonce"] + if request["query"] == "exchange": + apdusList.append(binascii.unhexlify(request["data"])) + elif request["query"] == "bulk": + for apdu in request["data"]: + apdusList.append(binascii.unhexlify(apdu)) + else: + response["response"] = "unsupported" + except: + response["response"] = "parse error" + + if len(apdusList) != 0: + try: + for apdu in apdusList: + response["data"] = dongle.exchange(apdu).hex() + response["response"] = "success" + except: + response["response"] = "I/O" # or error, and SW in data + + return response + + +if __name__ == "__main__": + import urllib.parse as urlparse + from .comm import getDongle + from websocket import create_connection + import json + import binascii + import ssl + + args = get_argparser().parse_args() + + dongle = getDongle(args.apdu) + + url = args.url + queryParameters = {} + queryParameters["targetId"] = args.targetId + queryParameters["firmware"] = args.firmware + queryParameters["firmwareKey"] = args.firmwareKey + queryParameters["perso"] = args.perso + queryString = urlparse.urlencode(queryParameters) + if args.bypass_ssl_check: + # SEE: https://docs.python.org/3/library/ssl.html#ssl.CERT_NONE + # According to the documentation: + # > With client-side sockets, just about any cert is accepted. Validation errors, such + # > as untrusted or expired cert, are ignored and do not abort the TLS/SSL handshake. + sslopt = {"cert_reqs": ssl.CERT_NONE} + else: + sslopt = {} + ws = create_connection(args.url + "?" + queryString, sslopt=sslopt) + while True: + result = json.loads(ws.recv()) + if result["query"] == "success": + break + if result["query"] == "error": + raise Exception( + result["data"] + " on " + result["uuid"] + "/" + result["session"] + ) + response = process(dongle, result) + ws.send(json.dumps(response)) + ws.close() + + print("Script executed successfully") + + dongle.close() diff --git a/ledgerblue/verifyApp.py b/ledgerblue/verifyApp.py index 9f21b17..937a0f8 100644 --- a/ledgerblue/verifyApp.py +++ b/ledgerblue/verifyApp.py @@ -19,37 +19,48 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser("""Verify that the provided signature is a valid signature of the provided + parser = argparse.ArgumentParser("""Verify that the provided signature is a valid signature of the provided application.""") - parser.add_argument("--hex", help="The hex file of the signed application", required=True) - parser.add_argument("--key", help="The Custom CA public key with which to verify the signature (hex encoded)", required=True) - parser.add_argument("--signature", help="The signature to be verified (hex encoded)", required=True) - return parser + parser.add_argument( + "--hex", help="The hex file of the signed application", required=True + ) + parser.add_argument( + "--key", + help="The Custom CA public key with which to verify the signature (hex encoded)", + required=True, + ) + parser.add_argument( + "--signature", help="The signature to be verified (hex encoded)", required=True + ) + return parser + def auto_int(x): - return int(x, 0) + return int(x, 0) + -if __name__ == '__main__': - from .hexParser import IntelHexParser - from .ecWrapper import PublicKey - import hashlib +if __name__ == "__main__": + from .hexParser import IntelHexParser + from .ecWrapper import PublicKey + import hashlib - args = get_argparser().parse_args() + args = get_argparser().parse_args() - # parse - parser = IntelHexParser(args.hex) + # parse + parser = IntelHexParser(args.hex) - # prepare data - m = hashlib.sha256() - # consider areas are ordered by ascending address and non-overlaped - for a in parser.getAreas(): - m.update(a.data) - dataToSign = m.digest() + # prepare data + m = hashlib.sha256() + # consider areas are ordered by ascending address and non-overlaped + for a in parser.getAreas(): + m.update(a.data) + dataToSign = m.digest() - publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) - signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) - if not publicKey.ecdsa_verify(bytes(dataToSign), signature, raw=True): - raise Exception("Signature not verified") + publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) + signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) + if not publicKey.ecdsa_verify(bytes(dataToSign), signature, raw=True): + raise Exception("Signature not verified") - print("Signature verified") + print("Signature verified") diff --git a/ledgerblue/verifyEndorsement1.py b/ledgerblue/verifyEndorsement1.py index 2d0e30f..3e529a9 100644 --- a/ledgerblue/verifyEndorsement1.py +++ b/ledgerblue/verifyEndorsement1.py @@ -19,30 +19,48 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Verify a message signature created with Endorsement Scheme #1.") - parser.add_argument("--key", help="The endorsement public key with which to verify the signature (hex encoded)", required=True) - parser.add_argument("--codehash", help="The hash of the app associated with the endorsement request (hex encoded)", required=True) - parser.add_argument("--message", help="The message associated to the endorsement request (hex encoded)", required=True) - parser.add_argument("--signature", help="The signature to be verified (hex encoded)", required=True) - return parser + parser = argparse.ArgumentParser( + description="Verify a message signature created with Endorsement Scheme #1." + ) + parser.add_argument( + "--key", + help="The endorsement public key with which to verify the signature (hex encoded)", + required=True, + ) + parser.add_argument( + "--codehash", + help="The hash of the app associated with the endorsement request (hex encoded)", + required=True, + ) + parser.add_argument( + "--message", + help="The message associated to the endorsement request (hex encoded)", + required=True, + ) + parser.add_argument( + "--signature", help="The signature to be verified (hex encoded)", required=True + ) + return parser + -if __name__ == '__main__': - import hashlib +if __name__ == "__main__": + import hashlib - from .ecWrapper import PublicKey + from .ecWrapper import PublicKey - args = get_argparser().parse_args() + args = get_argparser().parse_args() - # prepare data - m = hashlib.sha256() - m.update(bytes(bytearray.fromhex(args.message))) - m.update(bytes(bytearray.fromhex(args.codehash))) - digest = m.digest() + # prepare data + m = hashlib.sha256() + m.update(bytes(bytearray.fromhex(args.message))) + m.update(bytes(bytearray.fromhex(args.codehash))) + digest = m.digest() - publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) - signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) - if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): - raise Exception("Endorsement not verified") + publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) + signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) + if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): + raise Exception("Endorsement not verified") - print("Endorsement verified") + print("Endorsement verified") diff --git a/ledgerblue/verifyEndorsement2.py b/ledgerblue/verifyEndorsement2.py index d9cfbd3..a03889d 100644 --- a/ledgerblue/verifyEndorsement2.py +++ b/ledgerblue/verifyEndorsement2.py @@ -19,32 +19,54 @@ import argparse + def get_argparser(): - parser = argparse.ArgumentParser(description="Verify a message signature created with Endorsement Scheme #2.") - parser.add_argument("--key", help="The endorsement public key with which to verify the signature (hex encoded)", required=True) - parser.add_argument("--codehash", help="The hash of the app associated with the endorsement request (hex encoded)", required=True) - parser.add_argument("--message", help="The message associated to the endorsement request (hex encoded)", required=True) - parser.add_argument("--signature", help="The signature to be verified (hex encoded)", required=True) - return parser - -if __name__ == '__main__': - import hashlib - import hmac - - from .ecWrapper import PublicKey - - args = get_argparser().parse_args() - - # prepare data - tweak = hmac.new(bytes(bytearray.fromhex(args.codehash)), bytes(bytearray.fromhex(args.key)), hashlib.sha256).digest() - m = hashlib.sha256() - m.update(bytes(bytearray.fromhex(args.message))) - digest = m.digest() - - publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) - publicKey.tweak_add(bytes(tweak)) - signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) - if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): - raise Exception("Endorsement not verified") - - print("Endorsement verified") + parser = argparse.ArgumentParser( + description="Verify a message signature created with Endorsement Scheme #2." + ) + parser.add_argument( + "--key", + help="The endorsement public key with which to verify the signature (hex encoded)", + required=True, + ) + parser.add_argument( + "--codehash", + help="The hash of the app associated with the endorsement request (hex encoded)", + required=True, + ) + parser.add_argument( + "--message", + help="The message associated to the endorsement request (hex encoded)", + required=True, + ) + parser.add_argument( + "--signature", help="The signature to be verified (hex encoded)", required=True + ) + return parser + + +if __name__ == "__main__": + import hashlib + import hmac + + from .ecWrapper import PublicKey + + args = get_argparser().parse_args() + + # prepare data + tweak = hmac.new( + bytes(bytearray.fromhex(args.codehash)), + bytes(bytearray.fromhex(args.key)), + hashlib.sha256, + ).digest() + m = hashlib.sha256() + m.update(bytes(bytearray.fromhex(args.message))) + digest = m.digest() + + publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True) + publicKey.tweak_add(bytes(tweak)) + signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature))) + if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True): + raise Exception("Endorsement not verified") + + print("Endorsement verified") diff --git a/ledgerblue/vss.py b/ledgerblue/vss.py index 801cb3a..10c09d9 100755 --- a/ledgerblue/vss.py +++ b/ledgerblue/vss.py @@ -1,33 +1,30 @@ from ecpy.curves import Curve, Point -class PedersenVSS: - - def __init__(self, curve: Curve): - self.curve = curve - self.P = Point(curve.generator.x, curve.generator.y, curve) - self.domain_len = curve.generator.x.bit_length() // 8 - - def pedersen_commit(self, Q: Point, a: int, b: int) -> Point: - point1 = self.curve.mul_point(a, self.P) - point2 = self.curve.mul_point(b, Q) - return self.curve.add_point(point1, point2) - - def pedersen_share_commit(self, Q: Point, share: bytes) -> Point: - a = int.from_bytes(share[:self.domain_len], 'big') - b = int.from_bytes(share[self.domain_len:], 'big') - return self.pedersen_commit(Q, a, b) +class PedersenVSS: + def __init__(self, curve: Curve): + self.curve = curve + self.P = Point(curve.generator.x, curve.generator.y, curve) + self.domain_len = curve.generator.x.bit_length() // 8 + def pedersen_commit(self, Q: Point, a: int, b: int) -> Point: + point1 = self.curve.mul_point(a, self.P) + point2 = self.curve.mul_point(b, Q) - def pedersen_verify_commit(self, Q: Point, share: bytes, index: int, commits: list): - s_point = self.pedersen_share_commit(Q, share) + return self.curve.add_point(point1, point2) - r_point = commits[0] + def pedersen_share_commit(self, Q: Point, share: bytes) -> Point: + a = int.from_bytes(share[: self.domain_len], "big") + b = int.from_bytes(share[self.domain_len :], "big") + return self.pedersen_commit(Q, a, b) - for i in range(1, len(commits)): - r = self.curve.mul_point(index**i, commits[i]) - r_point = self.curve.add_point(r_point, r) + def pedersen_verify_commit(self, Q: Point, share: bytes, index: int, commits: list): + s_point = self.pedersen_share_commit(Q, share) - return s_point == r_point, s_point + r_point = commits[0] + for i in range(1, len(commits)): + r = self.curve.mul_point(index**i, commits[i]) + r_point = self.curve.add_point(r_point, r) + return s_point == r_point, s_point diff --git a/pyproject.toml b/pyproject.toml index 66d8b7b..a9438c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,8 +62,12 @@ doc = [ "sphinx_rtd_theme", "sphinx_argparse" ] +dev = [ + "pre-commit==3.2.0", + "ruff==0.3.7" +] [tool.setuptools_scm] write_to = "ledgerblue/__version__.py" local_scheme = "no-local-version" -fallback_version = "0.0.0" \ No newline at end of file +fallback_version = "0.0.0" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..2386cb1 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,8 @@ +[lint] +# Using rules from: pycodestyle, Pyflake and isort +# INP001 __init__ files check on package +select = [ "E", "F", "I", "INP001"] + + +[lint.per-file-ignores] +"!ledgerblue/*" = ["INP001"]