diff --git a/.flake8 b/.flake8 index 96f00b36..9412ef00 100644 --- a/.flake8 +++ b/.flake8 @@ -4,4 +4,3 @@ # E501 (line length) disabled as this is handled by black which takes better care of edge cases extend-ignore = E203,E501,E701 max-complexity = 18 -extend-exclude = pynitrokey/trussed/bootloader/nrf52_upload diff --git a/Makefile b/Makefile index 7bca1f76..c46b37a0 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON3=python3 PYTHON3_VENV=venv/bin/python3 # whitelist of directories for flake8 -FLAKE8_DIRS=pynitrokey/cli/nk3 pynitrokey/cli/nkpk.py pynitrokey/cli/trussed pynitrokey/nk3 pynitrokey/nkpk.py pynitrokey/trussed +FLAKE8_DIRS=pynitrokey/cli/nk3 pynitrokey/cli/nkpk.py pynitrokey/cli/trussed all: init diff --git a/pynitrokey/cli/nk3/__init__.py b/pynitrokey/cli/nk3/__init__.py index 1c205d68..6c1e5a5a 100644 --- a/pynitrokey/cli/nk3/__init__.py +++ b/pynitrokey/cli/nk3/__init__.py @@ -11,16 +11,16 @@ from typing import List, Optional import click +from nitrokey.nk3 import NK3_DATA +from nitrokey.nk3.bootloader import Nitrokey3Bootloader +from nitrokey.nk3.device import Nitrokey3Device +from nitrokey.trussed.base import NitrokeyTrussedBase +from nitrokey.trussed.bootloader import Device from pynitrokey.cli import trussed from pynitrokey.cli.exceptions import CliException from pynitrokey.cli.trussed.test import TestCase -from pynitrokey.helpers import local_print -from pynitrokey.nk3 import NK3_DATA -from pynitrokey.nk3.bootloader import Nitrokey3Bootloader -from pynitrokey.nk3.device import Nitrokey3Device -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader import Device +from pynitrokey.helpers import local_critical, local_print class Context(trussed.Context[Nitrokey3Bootloader, Nitrokey3Device]): @@ -42,12 +42,12 @@ def test_cases(self) -> list[TestCase]: ] def open(self, path: str) -> Optional[NitrokeyTrussedBase]: - from pynitrokey.nk3 import open + from nitrokey.nk3 import open return open(path) def list_all(self) -> List[NitrokeyTrussedBase]: - from pynitrokey.nk3 import list + from nitrokey.nk3 import list return list() @@ -252,7 +252,12 @@ def factory_reset(ctx: Context, experimental: bool) -> None: ) with ctx.connect_device() as device: - device.admin.factory_reset() + local_print("Please touch the device to confirm the operation", file=sys.stderr) + if not device.admin.factory_reset(): + local_critical( + "Factory reset is not supported by the firmware version on the device", + support_hint=False, + ) # We consciously do not allow resetting the admin app @@ -278,7 +283,12 @@ def factory_reset_app(ctx: Context, application: str, experimental: bool) -> Non ) with ctx.connect_device() as device: - device.admin.factory_reset_app(application) + local_print("Please touch the device to confirm the operation", file=sys.stderr) + if not device.admin.factory_reset_app(application): + local_critical( + "Application Factory reset is not supported by the firmware version on the device", + support_hint=False, + ) @nk3.command() diff --git a/pynitrokey/cli/nk3/secrets.py b/pynitrokey/cli/nk3/secrets.py index f66d2fb7..e412c503 100644 --- a/pynitrokey/cli/nk3/secrets.py +++ b/pynitrokey/cli/nk3/secrets.py @@ -8,10 +8,7 @@ import click from click_aliases import ClickAliasedGroup - -from pynitrokey.cli.nk3 import Context, nk3 -from pynitrokey.helpers import AskUser, local_critical, local_print -from pynitrokey.nk3.secrets_app import ( +from nitrokey.nk3.secrets_app import ( ALGORITHM_TO_KIND, STRING_TO_KIND, SecretsApp, @@ -20,6 +17,9 @@ SecretsAppHealthCheckException, ) +from pynitrokey.cli.nk3 import Context, nk3 +from pynitrokey.helpers import AskUser, local_critical, local_print + @nk3.group(cls=ClickAliasedGroup) @click.pass_context diff --git a/pynitrokey/cli/nk3/update.py b/pynitrokey/cli/nk3/update.py index de47891c..81cae703 100644 --- a/pynitrokey/cli/nk3/update.py +++ b/pynitrokey/cli/nk3/update.py @@ -12,12 +12,12 @@ from typing import Any, Callable, Iterator, List, Optional from click import Abort +from nitrokey.nk3.updates import Updater, UpdateUi +from nitrokey.trussed.utils import Version from pynitrokey.cli.exceptions import CliException from pynitrokey.cli.nk3 import Context from pynitrokey.helpers import DownloadProgressBar, ProgressBar, confirm, local_print -from pynitrokey.nk3.updates import Updater, UpdateUi -from pynitrokey.trussed.utils import Version logger = logging.getLogger(__name__) diff --git a/pynitrokey/cli/nkpk.py b/pynitrokey/cli/nkpk.py index facbeb57..ed8e6002 100644 --- a/pynitrokey/cli/nkpk.py +++ b/pynitrokey/cli/nkpk.py @@ -10,11 +10,11 @@ from typing import Optional import click +from nitrokey.nkpk import NKPK_DATA, NitrokeyPasskeyBootloader, NitrokeyPasskeyDevice +from nitrokey.trussed.base import NitrokeyTrussedBase +from nitrokey.trussed.bootloader import Device from pynitrokey.cli.trussed.test import TestCase -from pynitrokey.nkpk import NKPK_DATA, NitrokeyPasskeyBootloader, NitrokeyPasskeyDevice -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader import Device from . import trussed @@ -47,12 +47,12 @@ def device_name(self) -> str: return "Nitrokey Passkey" def open(self, path: str) -> Optional[NitrokeyTrussedBase]: - from pynitrokey.nkpk import open + from nitrokey.nkpk import open return open(path) def list_all(self) -> list[NitrokeyTrussedBase]: - from pynitrokey.nkpk import list + from nitrokey.nkpk import list return list() diff --git a/pynitrokey/cli/trussed/__init__.py b/pynitrokey/cli/trussed/__init__.py index 295d0eae..9fa0c4e5 100644 --- a/pynitrokey/cli/trussed/__init__.py +++ b/pynitrokey/cli/trussed/__init__.py @@ -18,6 +18,19 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from ecdsa import NIST256p, SigningKey +from nitrokey.trussed import DeviceData +from nitrokey.trussed.admin_app import BootMode +from nitrokey.trussed.base import NitrokeyTrussedBase +from nitrokey.trussed.bootloader import Device as BootloaderDevice +from nitrokey.trussed.bootloader import ( + FirmwareContainer, + NitrokeyTrussedBootloader, + parse_firmware_image, +) +from nitrokey.trussed.device import NitrokeyTrussedDevice +from nitrokey.trussed.exceptions import TimeoutException +from nitrokey.trussed.provisioner_app import ProvisionerApp +from nitrokey.updates import OverwriteError from pynitrokey.cli.exceptions import CliException from pynitrokey.helpers import ( @@ -26,19 +39,6 @@ local_print, require_windows_admin, ) -from pynitrokey.trussed import DeviceData -from pynitrokey.trussed.admin_app import BootMode -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader import Device as BootloaderDevice -from pynitrokey.trussed.bootloader import ( - FirmwareContainer, - NitrokeyTrussedBootloader, - parse_firmware_image, -) -from pynitrokey.trussed.device import NitrokeyTrussedDevice -from pynitrokey.trussed.exceptions import TimeoutException -from pynitrokey.trussed.provisioner_app import ProvisionerApp -from pynitrokey.updates import OverwriteError from .test import TestCase diff --git a/pynitrokey/cli/trussed/test.py b/pynitrokey/cli/trussed/test.py index 791de016..493d14ca 100644 --- a/pynitrokey/cli/trussed/test.py +++ b/pynitrokey/cli/trussed/test.py @@ -15,11 +15,12 @@ from types import TracebackType from typing import Callable, Iterable, Optional, Sequence, Tuple, Type, Union +from nitrokey.trussed.base import NitrokeyTrussedBase +from nitrokey.trussed.utils import Version + from pynitrokey.cli.exceptions import CliException from pynitrokey.fido2 import device_path_to_str from pynitrokey.helpers import local_print -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.utils import Version logger = logging.getLogger(__name__) diff --git a/pynitrokey/cli/trussed/tests.py b/pynitrokey/cli/trussed/tests.py index 019d4450..ca6492a7 100644 --- a/pynitrokey/cli/trussed/tests.py +++ b/pynitrokey/cli/trussed/tests.py @@ -14,14 +14,14 @@ from threading import Thread from typing import Any, Optional +from nitrokey.trussed.base import NitrokeyTrussedBase +from nitrokey.trussed.device import NitrokeyTrussedDevice +from nitrokey.trussed.utils import Fido2Certs, Uuid, Version from tqdm import tqdm from pynitrokey.cli.trussed.test import TestContext, TestResult, TestStatus, test_case from pynitrokey.fido2.client import NKFido2Client from pynitrokey.helpers import local_print -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.device import NitrokeyTrussedDevice -from pynitrokey.trussed.utils import Fido2Certs, Uuid, Version logger = logging.getLogger(__name__) diff --git a/pynitrokey/conftest.py b/pynitrokey/conftest.py index 5b2dc228..cd6b60f6 100644 --- a/pynitrokey/conftest.py +++ b/pynitrokey/conftest.py @@ -7,10 +7,10 @@ import pytest from _pytest.fixtures import FixtureRequest +from nitrokey.nk3.secrets_app import Instruction, SecretsApp from pynitrokey.cli import CliException from pynitrokey.cli.nk3 import Context -from pynitrokey.nk3.secrets_app import Instruction, SecretsApp CORPUS_PATH = "/tmp/corpus" diff --git a/pynitrokey/helpers.py b/pynitrokey/helpers.py index 28700332..f491d377 100644 --- a/pynitrokey/helpers.py +++ b/pynitrokey/helpers.py @@ -22,6 +22,7 @@ from typing import Any, Callable, Dict, List, NoReturn, Optional, Tuple, TypeVar, Union import click +from nitrokey.updates import Repository from semver.version import Version from tqdm import tqdm @@ -34,7 +35,6 @@ VERBOSE, Verbosity, ) -from pynitrokey.updates import Repository STDOUT_PRINT = True diff --git a/pynitrokey/nk3/__init__.py b/pynitrokey/nk3/__init__.py deleted file mode 100644 index 309d22cf..00000000 --- a/pynitrokey/nk3/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -from typing import List, Optional - -from pynitrokey.trussed import DeviceData -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader.nrf52 import SignatureKey - -PID_NITROKEY3_DEVICE = 0x42B2 -PID_NITROKEY3_LPC55_BOOTLOADER = 0x42DD -PID_NITROKEY3_NRF52_BOOTLOADER = 0x42E8 - -NK3_DATA = DeviceData( - name="Nitrokey 3", - firmware_repository_name="nitrokey-3-firmware", - firmware_pattern_string="firmware-nk3-v.*\\.zip$", - nrf52_signature_keys=[ - SignatureKey( - name="Nitrokey", - is_official=True, - der="3059301306072a8648ce3d020106082a8648ce3d03010703420004a0849b19007ccd4661c01c533804b7fd0c4d8c0e7583653f1f36a8331afff298b542bd00a3dc47c16bf428ac4d2864137d63f702d89e5b42674e0549b4232618", - ), - SignatureKey( - name="Nitrokey Test", - is_official=False, - der="3059301306072a8648ce3d020106082a8648ce3d0301070342000493e461ab0582bda1f45b0ce47d66bc4e8623e289c31af2098cde6ebd8631da85acf17e412d406c1e38c2de654a8fd0196506a85b169a756aeac2505a541cdd5d", - ), - ], -) - - -def list() -> List[NitrokeyTrussedBase]: - from . import bootloader - from .device import Nitrokey3Device - - devices: List[NitrokeyTrussedBase] = [] - devices.extend(bootloader.list()) - devices.extend(Nitrokey3Device.list()) - return devices - - -def open(path: str) -> Optional[NitrokeyTrussedBase]: - from . import bootloader - from .device import Nitrokey3Device - - device = Nitrokey3Device.open(path) - bootloader_device = bootloader.open(path) - if device and bootloader_device: - raise Exception(f"Found multiple devices at path {path}") - if device: - return device - if bootloader_device: - return bootloader_device - return None diff --git a/pynitrokey/nk3/bootloader.py b/pynitrokey/nk3/bootloader.py deleted file mode 100644 index 8944394e..00000000 --- a/pynitrokey/nk3/bootloader.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -from typing import List, Optional, Sequence - -from pynitrokey.trussed import VID_NITROKEY -from pynitrokey.trussed.bootloader import NitrokeyTrussedBootloader -from pynitrokey.trussed.bootloader.lpc55 import NitrokeyTrussedBootloaderLpc55 -from pynitrokey.trussed.bootloader.nrf52 import ( - NitrokeyTrussedBootloaderNrf52, - SignatureKey, -) - -from . import NK3_DATA - - -class Nitrokey3Bootloader(NitrokeyTrussedBootloader): - pass - - -class Nitrokey3BootloaderLpc55(NitrokeyTrussedBootloaderLpc55, Nitrokey3Bootloader): - @property - def name(self) -> str: - return "Nitrokey 3 Bootloader (LPC55)" - - @property - def pid(self) -> int: - from . import PID_NITROKEY3_LPC55_BOOTLOADER - - return PID_NITROKEY3_LPC55_BOOTLOADER - - @classmethod - def list(cls) -> List["Nitrokey3BootloaderLpc55"]: - from . import PID_NITROKEY3_LPC55_BOOTLOADER - - return cls.list_vid_pid(VID_NITROKEY, PID_NITROKEY3_LPC55_BOOTLOADER) - - -class Nitrokey3BootloaderNrf52(NitrokeyTrussedBootloaderNrf52, Nitrokey3Bootloader): - @property - def name(self) -> str: - return "Nitrokey 3 Bootloader (NRF52)" - - @property - def pid(self) -> int: - from . import PID_NITROKEY3_NRF52_BOOTLOADER - - return PID_NITROKEY3_NRF52_BOOTLOADER - - @classmethod - def list(cls) -> List["Nitrokey3BootloaderNrf52"]: - from . import PID_NITROKEY3_NRF52_BOOTLOADER - - return cls.list_vid_pid(VID_NITROKEY, PID_NITROKEY3_NRF52_BOOTLOADER) - - @classmethod - def open(cls, path: str) -> Optional["Nitrokey3BootloaderNrf52"]: - from . import PID_NITROKEY3_NRF52_BOOTLOADER - - return cls.open_vid_pid(VID_NITROKEY, PID_NITROKEY3_NRF52_BOOTLOADER, path) - - @property - def signature_keys(self) -> Sequence[SignatureKey]: - return NK3_DATA.nrf52_signature_keys - - -def list() -> List[Nitrokey3Bootloader]: - devices: List[Nitrokey3Bootloader] = [] - devices.extend(Nitrokey3BootloaderLpc55.list()) - devices.extend(Nitrokey3BootloaderNrf52.list()) - return devices - - -def open(path: str) -> Optional[Nitrokey3Bootloader]: - lpc55 = Nitrokey3BootloaderLpc55.open(path) - if lpc55: - return lpc55 - - nrf52 = Nitrokey3BootloaderNrf52.open(path) - if nrf52: - return nrf52 - - return None diff --git a/pynitrokey/nk3/device.py b/pynitrokey/nk3/device.py deleted file mode 100644 index 436b5c48..00000000 --- a/pynitrokey/nk3/device.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -from fido2.hid import CtapHidDevice - -from pynitrokey.trussed.device import NitrokeyTrussedDevice -from pynitrokey.trussed.utils import Fido2Certs, Version - -FIDO2_CERTS = [ - Fido2Certs( - start=Version(0, 1, 0), - hashes=[ - "ad8fd1d16f59104b9e06ef323cc03f777ed5303cd421a101c9cb00bb3fdf722d", - ], - ), - Fido2Certs( - start=Version(1, 0, 3), - hashes=[ - "aa1cb760c2879530e7d7fed3da75345d25774be9cfdbbcbd36fdee767025f34b", # NK3xN/lpc55 - "4c331d7af869fd1d8217198b917a33d1fa503e9778da7638504a64a438661ae0", # NK3AM/nrf52 - "f1ed1aba24b16e8e3fabcda72b10cbfa54488d3b778bda552162d60c6dd7b4fa", # NK3AM/nrf52 test - ], - ), -] - - -class Nitrokey3Device(NitrokeyTrussedDevice): - """A Nitrokey 3 device running the firmware.""" - - def __init__(self, device: CtapHidDevice) -> None: - super().__init__(device, FIDO2_CERTS) - - @property - def pid(self) -> int: - from . import PID_NITROKEY3_DEVICE - - return PID_NITROKEY3_DEVICE - - @property - def name(self) -> str: - return "Nitrokey 3" - - @classmethod - def from_device(cls, device: CtapHidDevice) -> "Nitrokey3Device": - return cls(device) diff --git a/pynitrokey/nk3/secrets_app.py b/pynitrokey/nk3/secrets_app.py deleted file mode 100644 index a2e1e193..00000000 --- a/pynitrokey/nk3/secrets_app.py +++ /dev/null @@ -1,849 +0,0 @@ -""" -Oath Authenticator client - -Used through CTAPHID transport, via the custom vendor command. -Can be used directly over CCID as well. -""" -import dataclasses -import hmac -import logging -import typing -from enum import Enum, IntEnum -from hashlib import pbkdf2_hmac -from secrets import token_bytes -from struct import pack -from typing import Any, Callable, List, Optional, Sequence, Tuple, Union - -import tlv8 -from semver.version import Version - -from pynitrokey.nk3.device import Nitrokey3Device -from pynitrokey.start.gnuk_token import iso7816_compose -from pynitrokey.trussed.device import App - -LogFn = Callable[[str], Any] -WriteCorpusFn = Callable[[typing.Union["Instruction", "CCIDInstruction"], bytes], Any] - - -@dataclasses.dataclass -class ListItemProperties: - touch_required: bool - secret_encryption: bool - pws_data_exist: bool - - @classmethod - def _get_bit(cls, x: int, n: int) -> bool: - return ((x >> n) & 1) == 1 - - @classmethod - def from_byte(cls, b: int) -> "ListItemProperties": - return ListItemProperties( - touch_required=cls._get_bit(b, 0), - secret_encryption=cls._get_bit(b, 1), - pws_data_exist=cls._get_bit(b, 2), - ) - - def __str__(self) -> str: - data = [ - "touch required" if self.touch_required else "", - "PIN required" if self.secret_encryption else "", - "PWS data available" if self.pws_data_exist else "", - ] - return ",".join([d for d in data if d]) - - -@dataclasses.dataclass -class ListItem: - kind: "Kind" - algorithm: "Algorithm" - label: bytes - properties: ListItemProperties - - @classmethod - def get_type_name(cls, x: typing.Any) -> str: - return str(x).split(".")[-1] - - def __str__(self) -> str: - return ( - f"{self.label.decode()}" - f"\t{self.get_type_name(self.kind)}/{self.get_type_name(self.algorithm)}" - f"\t{self.properties}" - ) - - -@dataclasses.dataclass -class PasswordSafeEntry: - login: Optional[bytes] - password: Optional[bytes] - metadata: Optional[bytes] - properties: Optional[bytes] = None - name: Optional[bytes] = None - - def tlv_encode(self) -> list[tlv8.Entry]: - entries = [ - tlv8.Entry(Tag.PwsLogin.value, self.login) - if self.login is not None - else None, - tlv8.Entry(Tag.PwsPassword.value, self.password) - if self.password is not None - else None, - tlv8.Entry(Tag.PwsMetadata.value, self.metadata) - if self.metadata is not None - else None, - ] - # Filter out empty entries - entries = [r for r in entries if r is not None] - return entries - - -@dataclasses.dataclass -class RawBytes: - data: list[int] - - -@dataclasses.dataclass -class SelectResponse: - # Application version - version: Optional[bytes] - # PIN attempt counter - pin_attempt_counter: Optional[int] - # Salt, challenge-response auth only, tag Name - salt: Optional[bytes] - # Challenge field, challenge-response auth only - challenge: Optional[bytes] - # Selected algorithm, challenge-response auth only - algorithm: Optional[bytes] - # Serial number of the device - serial_number: Optional[bytes] - - def version_str(self) -> str: - if self.version: - return f"{self.version[0]}.{self.version[1]}.{self.version[2]}" - else: - return "unknown" - - def __str__(self) -> str: - return ( - "Nitrokey Secrets\n" - f"\tVersion: {self.version_str()}\n" - f"\tPIN attempt counter: {self.pin_attempt_counter}\n" - f"\tSerial number: {self.serial_number.hex() if self.serial_number else 'None'}" - ) - - -class SecretsAppExceptionID(IntEnum): - MoreDataAvailable = 0x61FF - VerificationFailed = 0x6300 - UnspecifiedNonpersistentExecutionError = 0x6400 - UnspecifiedPersistentExecutionError = 0x6500 - WrongLength = 0x6700 - LogicalChannelNotSupported = 0x6881 - SecureMessagingNotSupported = 0x6882 - CommandChainingNotSupported = 0x6884 - SecurityStatusNotSatisfied = 0x6982 - ConditionsOfUseNotSatisfied = 0x6985 - OperationBlocked = 0x6983 - IncorrectDataParameter = 0x6A80 - FunctionNotSupported = 0x6A81 - NotFound = 0x6A82 - NotEnoughMemory = 0x6A84 - IncorrectP1OrP2Parameter = 0x6A86 - KeyReferenceNotFound = 0x6A88 - InstructionNotSupportedOrInvalid = 0x6D00 - ClassNotSupported = 0x6E00 - UnspecifiedCheckingError = 0x6F00 - Success = 0x9000 - - -class SecretsAppHealthCheckException(Exception): - pass - - -@dataclasses.dataclass -class SecretsAppException(Exception): - code: str - context: str - - def to_id(self) -> SecretsAppExceptionID: - return SecretsAppExceptionID(int(self.code, 16)) - - def to_string(self) -> str: - d = { - "61FF": "MoreDataAvailable", - "6300": "VerificationFailed", - "6400": "UnspecifiedNonpersistentExecutionError", - "6500": "UnspecifiedPersistentExecutionError", - "6700": "WrongLength", - "6881": "LogicalChannelNotSupported", - "6882": "SecureMessagingNotSupported", - "6884": "CommandChainingNotSupported", - "6982": "SecurityStatusNotSatisfied", - "6985": "ConditionsOfUseNotSatisfied", - "6983": "OperationBlocked", - "6a80": "IncorrectDataParameter", - "6a81": "FunctionNotSupported", - "6a82": "NotFound", - "6a84": "NotEnoughMemory", - "6a86": "IncorrectP1OrP2Parameter", - "6a88": "KeyReferenceNotFound", - "6d00": "InstructionNotSupportedOrInvalid", - "6e00": "ClassNotSupported", - "6f00": "UnspecifiedCheckingError", - "9000": "Success", - } - return d.get(self.code, "Unknown SW code") - - def __repr__(self) -> str: - return f"SecretsAppException(code={self.code}/{self.to_string()})" - - def __str__(self) -> str: - return self.__repr__() - - -class CCIDInstruction(Enum): - Select = 0xA4 - - -class Instruction(Enum): - Put = 0x1 - Delete = 0x2 - SetCode = 0x3 - Reset = 0x4 - List = 0xA1 - Calculate = 0xA2 - Validate = 0xA3 - CalculateAll = 0xA4 # 0xA4 is Select as well # Unused - SendRemaining = 0xA5 - VerifyCode = 0xB1 - # Place extending commands in 0xBx space - VerifyPIN = 0xB2 - ChangePIN = 0xB3 - SetPIN = 0xB4 - GetCredential = 0xB5 - UpdateCredential = 0xB7 - - -class Tag(Enum): - CredentialId = 0x71 # also known as Name - NameList = 0x72 - Key = 0x73 - Challenge = 0x74 - Response = 0x75 - Properties = 0x78 - InitialCounter = 0x7A - Version = 0x79 - Algorithm = 0x7B - # Touch = 0x7c, - # Extension starting from 0x80 - Password = 0x80 - NewPassword = 0x81 - PINCounter = 0x82 - PwsLogin = 0x83 - PwsPassword = 0x84 - PwsMetadata = 0x85 - SerialNumber = 0x8F - - -class Kind(Enum): - Hotp = 0x10 - Totp = 0x20 - HotpReverse = 0x30 - Hmac = 0x40 - NotSet = 0xF0 - - @classmethod - def from_attribute_byte(cls, attribute_byte: bytes) -> str: - a = int(attribute_byte) - k = cls.from_attribute_byte_type(a) - if k != Kind.NotSet: - return str(k).split(".")[-1].upper() - else: - return "PWS" - - @classmethod - def from_attribute_byte_type(cls, a: int) -> "Kind": - v = a & 0xF0 - for k in Kind: - if k.value == v: - return k - raise ValueError("Invalid attribute byte") - - -STRING_TO_KIND = { - "HOTP": Kind.Hotp, - "TOTP": Kind.Totp, - "HOTP_REVERSE": Kind.HotpReverse, - "HMAC": Kind.Hmac, -} - - -class Algorithm(Enum): - Sha1 = 0x01 - Sha256 = 0x02 - Sha512 = 0x03 - - -ALGORITHM_TO_KIND = { - "SHA1": Algorithm.Sha1, - "SHA256": Algorithm.Sha256, -} - - -class SecretsApp: - """ - This is a Secrets App client - https://github.com/Nitrokey/trussed-secrets-app - """ - - log: logging.Logger - logfn: LogFn - dev: Nitrokey3Device - write_corpus_fn: Optional[WriteCorpusFn] - _cache_status: Optional[SelectResponse] - _metadata: dict[Any, Any] - - def __init__(self, dev: Nitrokey3Device, logfn: Optional[LogFn] = None): - self._cache_status = None - self.write_corpus_fn = None - self.log = logging.getLogger("otpapp") - if logfn is not None: - self.logfn = logfn - else: - self.logfn = self.log.info - self.dev = dev - self._metadata = {} - - def _custom_encode( - self, structure: Optional[Sequence[Union[tlv8.Entry, RawBytes, None]]] = None - ) -> bytes: - if not structure: - return b"" - - def transform(d: Union[tlv8.Entry, RawBytes, None]) -> bytes: - if not d: - return b"" - if isinstance(d, RawBytes): - res = bytes(d.data) - # self.logfn(f"Transforming {d} -> {res.hex()}") - return res - elif isinstance(d, tlv8.Entry): - res = tlv8.encode([d]) - # self.logfn(f"Transforming {d} -> {res.hex()}") - return res - return b"" - - encoded_structure = b"".join(map(transform, structure)) - return encoded_structure - - def _send_receive( - self, - ins: typing.Union[Instruction, CCIDInstruction], - structure: Optional[Sequence[Union[tlv8.Entry, RawBytes, None]]] = None, - ) -> bytes: - encoded_structure = self._custom_encode(structure) - ins_b, p1, p2 = self._encode_command(ins) - bytes_data = iso7816_compose(ins_b, p1, p2, data=encoded_structure) - if self.write_corpus_fn: - self.write_corpus_fn(ins, bytes_data) - return self._send_receive_inner(bytes_data, log_info=f"{ins}") - - def _send_receive_inner(self, data: bytes, log_info: str = "") -> bytes: - self.logfn(f"Sending {log_info if log_info else ''} (data: {len(data)} bytes)") - - try: - result = self.dev._call_app(App.SECRETS, data=data) - except Exception as e: - self.logfn(f"Got exception: {e}") - raise - - status_bytes, result = result[:2], result[2:] - self.logfn(f"Received [{status_bytes.hex()}] (data: {len(result)} bytes)") - - log_multipacket = False - data_final = result - MORE_DATA_STATUS_BYTE = 0x61 - while status_bytes[0] == MORE_DATA_STATUS_BYTE: - if log_multipacket: - self.logfn(f"Got RemainingData status: [{status_bytes.hex()}]") - log_multipacket = True - ins_b, p1, p2 = self._encode_command(Instruction.SendRemaining) - bytes_data = iso7816_compose(ins_b, p1, p2) - try: - result = self.dev._call_app(App.SECRETS, data=bytes_data) - except Exception as e: - self.logfn(f"Got exception: {e}") - raise - # Data order is different here than in APDU - SW is first, then the data if any - status_bytes, result = result[:2], result[2:] - self.logfn(f"Received [{status_bytes.hex()}] (data: {len(result)} bytes)") - if status_bytes[0] in [0x90, MORE_DATA_STATUS_BYTE]: - data_final += result - - if status_bytes != b"\x90\x00" and status_bytes[0] != MORE_DATA_STATUS_BYTE: - raise SecretsAppException(status_bytes.hex(), "Received error") - - if log_multipacket: - self.logfn( - f"Received final data: [{status_bytes.hex()}] (data: {len(data_final)} bytes)" - ) - - if data_final: - try: - tlv8.decode(data_final) - self.logfn("TLV-decoding of data successful") - except Exception: - self.logfn("TLV-decoding of data failed") - pass - - return data_final - - @classmethod - def _encode_command( - cls, command: typing.Union[Instruction, CCIDInstruction] - ) -> bytes: - p1 = 0 - p2 = 0 - if command == Instruction.Reset: - p1 = 0xDE - p2 = 0xAD - elif command == CCIDInstruction.Select: - p1 = 0x04 - p2 = 0x00 - elif command == Instruction.Calculate or command == Instruction.CalculateAll: - p1 = 0x00 - p2 = 0x01 - return bytes([command.value, p1, p2]) - - def reset(self) -> None: - """ - Remove all credentials from the database - """ - self.logfn("Executing reset") - self._send_receive(Instruction.Reset) - - def list(self, extended: bool = False) -> list[Union[Tuple[bytes, bytes], bytes]]: - """ - Return a list of the registered credentials - :return: List of bytestrings, or tuple of bytestrings, if "extended" switch is provided - @deprecated - """ - raw_res = self._send_receive(Instruction.List) - resd: tlv8.EntryList = tlv8.decode(raw_res) - res: list[Union[Tuple[bytes, bytes], bytes]] = [] - for e in resd: - # e: tlv8.Entry - if extended: - res.append((e.data[0], e.data[1:])) - else: - res.append(e.data[1:]) - return res - - def list_with_properties(self, version: int = 1) -> List[ListItem]: - """ - Return a list of the registered credentials with properties - :return: List of ListItems - """ - data = [RawBytes([version])] - raw_res = self._send_receive(Instruction.List, data) - resd: tlv8.EntryList = tlv8.decode(raw_res) - res = [] - for e in resd: - # e: tlv8.Entry - if self.feature_extended_list(): - attribute_byte, label, properties = e.data[0], e.data[1:-1], e.data[-1] - else: - attribute_byte, label, properties = e.data[0], e.data[1:], 0 - res.append( - ListItem( - kind=Kind.from_attribute_byte_type(attribute_byte), - algorithm=Algorithm.Sha1, - label=label, - properties=ListItemProperties.from_byte(properties), - ) - ) - return res - - def get_credential(self, cred_id: bytes) -> PasswordSafeEntry: - structure = [ - tlv8.Entry(Tag.CredentialId.value, cred_id), - ] - raw_res = self._send_receive(Instruction.GetCredential, structure=structure) - resd: tlv8.EntryList = tlv8.decode(raw_res) - res = {} - self.logfn("Per field dissection:") - for e in resd: - # e: tlv8.Entry - res[e.type_id] = e.data - self.logfn(f"{hex(e.type_id)} {hex(len(e.data))}") - p = PasswordSafeEntry( - login=res.get(Tag.PwsLogin.value), - password=res.get(Tag.PwsPassword.value), - metadata=res.get(Tag.PwsMetadata.value), - name=res.get(Tag.CredentialId.value), - properties=res.get(Tag.Properties.value), - ) - p.properties = p.properties.hex().encode() if p.properties else None - return p - - def rename_credential(self, cred_id: bytes, new_name: bytes) -> None: - """ - Rename credential. - An alias for the update_credential() call. - @param cred_id: The credential ID to modify - @param new_name: New ID for the credential - """ - return self.update_credential(cred_id, new_name) - - def update_credential( - self, - cred_id: bytes, - new_name: Optional[bytes] = None, - login: Optional[bytes] = None, - password: Optional[bytes] = None, - metadata: Optional[bytes] = None, - touch_button: Optional[bool] = None, - ) -> None: - """ - Update credential fields - name, attributes, and PWS fields. - Unpopulated fields will not be encoded and used during the update process - (won't change the current value). - @param cred_id: The credential ID to modify - @param new_name: New ID for the credential - @param login: New login field content - @param password: New password field content - @param metadata: New metadata field content - @param touch_button: Set if the touch button use should be required - """ - structure = [ - tlv8.Entry(Tag.CredentialId.value, cred_id), - tlv8.Entry(Tag.CredentialId.value, new_name) if new_name else None, - self.encode_properties_to_send(touch_button, False, tlv=True) - if touch_button is not None - else None, - tlv8.Entry(Tag.PwsLogin.value, login) if login is not None else None, - tlv8.Entry(Tag.PwsPassword.value, password) - if password is not None - else None, - tlv8.Entry(Tag.PwsMetadata.value, metadata) - if metadata is not None - else None, - ] - structure = list(filter(lambda x: x is not None, structure)) - self._send_receive(Instruction.UpdateCredential, structure=structure) - - def delete(self, cred_id: bytes) -> None: - """ - Delete credential with the given id. Does not fail, if the given credential does not exist. - :param credid: Credential ID - """ - self.logfn(f"Sending delete request for {cred_id!r}") - structure = [ - tlv8.Entry(Tag.CredentialId.value, cred_id), - ] - self._send_receive(Instruction.Delete, structure) - - def register_yk_hmac(self, slot: int, secret: bytes) -> None: - """ - Register a Yubikey-compatible challenge-response slot. - @param slot: challenge-response slot - @param secret: the secret - """ - assert slot in [1, 2] - self.register( - f"HmacSlot{slot}".encode(), - secret, - kind=Kind.Hmac, - ) - - def register( - self, - credid: bytes, - secret: bytes = b"0" * 20, - digits: int = 6, - kind: Kind = Kind.NotSet, - algo: Algorithm = Algorithm.Sha1, - initial_counter_value: int = 0, - touch_button_required: bool = False, - pin_based_encryption: bool = False, - login: Optional[bytes] = None, - password: Optional[bytes] = None, - metadata: Optional[bytes] = None, - ) -> None: - """ - Register new OTP Credential - :param credid: Credential ID - :param secret: The shared key - :param digits: Digits of the produced code - :param kind: OTP variant - HOTP or TOTP - :param algo: The hash algorithm to use - SHA1, SHA256 or SHA512 - :param initial_counter_value: The counter's initial value for the HOTP Credential (HOTP only) - :param touch_button_required: User Presence confirmation is required to use this Credential - :param pin_based_encryption: User preference for additional PIN-based encryption - :param login: Login field for Password Safe - :param password: Password field for Password Safe - :param metadata: Metadata field for Password Safe - :return: None - """ - if initial_counter_value > 0xFFFFFFFF: - raise Exception("Initial counter value must be smaller than 4 bytes") - if algo == Algorithm.Sha512: - raise NotImplementedError( - "This hash algorithm is not supported by the firmware" - ) - - self.logfn( - f"Setting new credential: {credid!r}, {kind}, {algo}, counter: {initial_counter_value}, {touch_button_required=}, {pin_based_encryption=}" - ) - - structure = [ - tlv8.Entry(Tag.CredentialId.value, credid), - # header (2) + secret (N) - tlv8.Entry( - Tag.Key.value, bytes([kind.value | algo.value, digits]) + secret - ), - self.encode_properties_to_send(touch_button_required, pin_based_encryption), - tlv8.Entry( - Tag.InitialCounter.value, initial_counter_value.to_bytes(4, "big") - ) - if kind in [Kind.Hotp, Kind.HotpReverse] - else None, - *PasswordSafeEntry( - name=credid, login=login, password=password, metadata=metadata - ).tlv_encode(), - ] - structure = list(filter(lambda x: x is not None, structure)) - self._send_receive(Instruction.Put, structure) - - @classmethod - def encode_properties_to_send( - cls, touch_button_required: bool, pin_based_encryption: bool, tlv: bool = False - ) -> RawBytes: - """ - Encode properties structure into a single byte - @param touch_button_required: whether the touch button use is required - @param pin_based_encryption: whether the PIN-encryption is requested (only during registration) - @param tlv: set True, if this should be encoded as TLV, as opposed to the default "TV", w/o L - """ - structure = [ - Tag.Properties.value, - 1 if tlv else None, - (0x02 if touch_button_required else 0x00) - | (0x04 if pin_based_encryption else 0x00), - ] - structure = list(filter(lambda x: x is not None, structure)) - return RawBytes(structure) # type: ignore[arg-type] - - def calculate(self, cred_id: bytes, challenge: Optional[int] = None) -> bytes: - """ - Calculate the OTP code for the credential named `cred_id`, and with challenge `challenge`. - :param cred_id: The name of the credential - :param challenge: Challenge for the calculations (TOTP only). - Should be equal to: timestamp/period. The commonly used period value is 30. - :return: OTP code as a byte string - """ - if challenge is None: - challenge = 0 - self.logfn( - f"Sending calculate request for {cred_id!r} and challenge {challenge!r}" - ) - structure = [ - tlv8.Entry(Tag.CredentialId.value, cred_id), - tlv8.Entry(Tag.Challenge.value, pack(">Q", challenge)), - ] - res = self._send_receive(Instruction.Calculate, structure=structure) - header = res[:2] - assert header.hex() in ["7605", "7700"] - digits = res[2] - digest = res[3:] - truncated_code = int.from_bytes(digest, byteorder="big", signed=False) - code = (truncated_code & 0x7FFFFFFF) % pow(10, digits) - codes: bytes = str(code).zfill(digits).encode() - self.logfn( - f"Received digest: {digest.hex()}, for challenge {challenge}, digits: {digits}, truncated code: {truncated_code!r}, pre-code: {code!r}," - f" final code: {codes!r}" - ) - return codes - - def verify_code(self, cred_id: bytes, code: int) -> bool: - """ - Proceed with the incoming OTP code verification (aka reverse HOTP). - :param cred_id: The name of the credential - :param code: The HOTP code to verify. u32 representation. - :return: fails with OTPAppException error; returns True if code matches the value calculated internally. - """ - structure = [ - tlv8.Entry(Tag.CredentialId.value, cred_id), - tlv8.Entry(Tag.Response.value, pack(">L", code)), - ] - self._send_receive(Instruction.VerifyCode, structure=structure) - return True - - def set_code(self, passphrase: str) -> None: - """ - Set the code with the defaults as suggested in the protocol specification: - - https://developers.yubico.com/OATH/YKOATH_Protocol.html - """ - secret = self.get_secret_for_passphrase(passphrase) - challenge = token_bytes(8) - response = self.get_response_for_secret(challenge, secret) - self.set_code_raw(secret, challenge, response) - - def get_secret_for_passphrase(self, passphrase: str) -> bytes: - # secret = PBKDF2(USER_PASSPHRASE, DEVICEID, 1000)[:16] - # salt = self.select().name - # FIXME use the proper SALT - # FIXME USB/IP Sim changes its ID after each reset and after setting the code (??) - salt = b"a" * 8 - secret = pbkdf2_hmac("sha256", passphrase.encode(), salt, 1000) - return secret[:16] - - def get_response_for_secret(self, challenge: bytes, secret: bytes) -> bytes: - response = hmac.HMAC(key=secret, msg=challenge, digestmod="sha1").digest() - return response - - def set_code_raw(self, key: bytes, challenge: bytes, response: bytes) -> None: - """ - Set or clear the passphrase used to authenticate to other commands. Raw interface. - :param key: User passphrase processed through PBKDF2(ID,1000), and limited to the first 16 bytes. - :param challenge: The current challenge taken from the SELECT command. - :param response: The data calculated on the client, as a proof of a correct setup. - """ - algo = Algorithm.Sha1.value - kind = Kind.Totp.value - structure = [ - tlv8.Entry(Tag.Key.value, bytes([kind | algo]) + key), - tlv8.Entry(Tag.Challenge.value, challenge), - tlv8.Entry(Tag.Response.value, response), - ] - self._send_receive(Instruction.SetCode, structure=structure) - - def clear_code(self) -> None: - """ - Clear the passphrase used to authenticate to other commands. - """ - structure = [ - tlv8.Entry(Tag.Key.value, bytes()), - ] - self._send_receive(Instruction.SetCode, structure=structure) - - def validate(self, passphrase: str) -> None: - """ - Authenticate using a passphrase - """ - stat = self.select() - if stat.algorithm != bytes([Algorithm.Sha1.value]): - raise RuntimeError("For the authentication only SHA1 is supported") - challenge = stat.challenge - if challenge is None: - # This should never happen - raise RuntimeError( - "There is some problem with the device's state. Challenge is not available." - ) - secret = self.get_secret_for_passphrase(passphrase) - response = self.get_response_for_secret(challenge, secret) - self.validate_raw(challenge, response) - - def validate_raw(self, challenge: bytes, response: bytes) -> bytes: - """ - Authenticate using a passphrase. Raw interface. - :param challenge: The current challenge taken from the SELECT command. - :param response: The response calculated against the challenge and the secret - """ - structure = [ - tlv8.Entry(Tag.Response.value, response), - tlv8.Entry(Tag.Challenge.value, challenge), - ] - raw_res = self._send_receive(Instruction.Validate, structure=structure) - resd: tlv8.EntryList = tlv8.decode(raw_res) - return resd.data # type: ignore[no-any-return] - - def select(self) -> SelectResponse: - """ - Execute SELECT command, which returns details about the device, - including the challenge needed for the authentication. - :return SelectResponse Status structure. Challenge and Algorithm fields are None, if the passphrase is not set. - """ - AID = [0xA0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01] - structure = [RawBytes(AID)] - raw_res = self._send_receive(CCIDInstruction.Select, structure=structure) - resd: tlv8.EntryList = tlv8.decode(raw_res) - rd = {} - for e in resd: - # e: tlv8.Entry - rd[e.type_id] = e.data - - counter = rd.get(Tag.PINCounter.value) - if counter is not None: - # counter is passed as 1B array - convert it to int - counter = int.from_bytes(counter, byteorder="big") - - r = SelectResponse( - version=rd.get(Tag.Version.value), - pin_attempt_counter=counter, - salt=rd.get(Tag.CredentialId.value), - challenge=rd.get(Tag.Challenge.value), - algorithm=rd.get(Tag.Algorithm.value), - serial_number=rd.get(Tag.SerialNumber.value), - ) - return r - - def set_pin_raw(self, password: str) -> None: - structure = [ - tlv8.Entry(Tag.Password.value, password), - ] - self._send_receive(Instruction.SetPIN, structure=structure) - - def change_pin_raw(self, password: str, new_password: str) -> None: - structure = [ - tlv8.Entry(Tag.Password.value, password), - tlv8.Entry(Tag.NewPassword.value, new_password), - ] - self._send_receive(Instruction.ChangePIN, structure=structure) - - def verify_pin_raw(self, password: str) -> None: - structure = [ - tlv8.Entry(Tag.Password.value, password), - ] - self._send_receive(Instruction.VerifyPIN, structure=structure) - - def get_feature_status_cached(self) -> SelectResponse: - self._cache_status = ( - self.select() if self._cache_status is None else self._cache_status - ) - return self._cache_status - - def feature_active_PIN_authentication(self) -> bool: - return self.get_feature_status_cached().challenge is None - - def feature_old_application_version(self) -> bool: - v = self.get_feature_status_cached().version - return b"444" == v - - def feature_challenge_response_support(self) -> bool: - if self.get_feature_status_cached().challenge is not None: - return True - return False - - def feature_pws_support(self) -> bool: - return self._semver_equal_or_newer("4.11.0") - - def feature_extended_list(self) -> bool: - return self._semver_equal_or_newer("4.11.0") - - def protocol_v2_confirm_all_requests_with_pin(self) -> bool: - # 4.7.0 version requires providing PIN each request - return self.get_feature_status_cached().version_str() == "4.7.0" - - def protocol_v3_separate_pin_and_no_pin_space(self) -> bool: - # 4.10.0 makes logical separation between the PIN-encrypted and non-PIN encrypted spaces, except - # for overwriting the credentials - return self._semver_equal_or_newer("4.10.0") - - def is_pin_healthy(self) -> bool: - counter = self.select().pin_attempt_counter - return not (counter is None or counter == 0) - - def _semver_equal_or_newer(self, required_version: str) -> bool: - current = Version.parse(self.get_feature_status_cached().version_str()) - semver_req_version = Version.parse(required_version) - return current >= semver_req_version diff --git a/pynitrokey/nk3/updates.py b/pynitrokey/nk3/updates.py deleted file mode 100644 index 36120a05..00000000 --- a/pynitrokey/nk3/updates.py +++ /dev/null @@ -1,400 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import enum -import logging -import platform -import time -from abc import ABC, abstractmethod -from contextlib import contextmanager -from io import BytesIO -from typing import Any, Callable, Iterator, List, Optional - -from spsdk.mboot.exceptions import McuBootConnectionError - -import pynitrokey -from pynitrokey.helpers import Retries -from pynitrokey.nk3 import NK3_DATA -from pynitrokey.nk3.bootloader import Nitrokey3Bootloader -from pynitrokey.nk3.device import Nitrokey3Device -from pynitrokey.trussed.admin_app import BootMode -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader import ( - Device, - FirmwareContainer, - Variant, - validate_firmware_image, -) -from pynitrokey.trussed.exceptions import TimeoutException -from pynitrokey.trussed.utils import Version -from pynitrokey.updates import Asset, Release - -logger = logging.getLogger(__name__) - - -@enum.unique -class UpdatePath(enum.Enum): - default = enum.auto() - nRF_IFS_Migration_v1_3 = enum.auto() - - @classmethod - def create( - cls, variant: Optional[Variant], current: Optional[Version], new: Version - ) -> "UpdatePath": - if variant == Variant.NRF52: - if ( - current is None - or current <= Version(1, 2, 2) - and new >= Version(1, 3, 0) - ): - return cls.nRF_IFS_Migration_v1_3 - return cls.default - - -def get_firmware_update(release: Release) -> Asset: - return release.require_asset(NK3_DATA.firmware_pattern) - - -def get_extra_information(upath: UpdatePath) -> List[str]: - """Return additional information for the device after update based on update-path""" - - out = [] - if upath == UpdatePath.nRF_IFS_Migration_v1_3: - out += [ - "", - "During this update process the internal filesystem will be migrated!", - "- Migration will only work, if your internal filesystem does not contain more than 45 Resident Keys. If you have more please remove some.", - "- After the update it might take up to 3minutes for the first boot.", - "Never unplug the device while the LED is active!", - ] - return out - - -def get_finalization_wait_retries(upath: UpdatePath) -> int: - """Return number of retries to wait for the device after update based on update-path""" - - out = 60 - if upath == UpdatePath.nRF_IFS_Migration_v1_3: - # max time 150secs == 300 retries - out = 500 - return out - - -class UpdateUi(ABC): - @abstractmethod - def error(self, *msgs: Any) -> Exception: - pass - - @abstractmethod - def abort(self, *msgs: Any) -> Exception: - pass - - @abstractmethod - def abort_downgrade(self, current: Version, image: Version) -> Exception: - pass - - @abstractmethod - def abort_pynitrokey_version( - self, current: Version, required: Version - ) -> Exception: - pass - - @abstractmethod - def confirm_download(self, current: Optional[Version], new: Version) -> None: - pass - - @abstractmethod - def confirm_update(self, current: Optional[Version], new: Version) -> None: - pass - - @abstractmethod - def confirm_pynitrokey_version(self, current: Version, required: Version) -> None: - pass - - @abstractmethod - def confirm_extra_information(self, extra_info: List[str]) -> None: - pass - - @abstractmethod - def confirm_update_same_version(self, version: Version) -> None: - pass - - @abstractmethod - def request_repeated_update(self) -> Optional[Exception]: - pass - - @abstractmethod - def pre_bootloader_hint(self) -> None: - pass - - @abstractmethod - def request_bootloader_confirmation(self) -> None: - pass - - @abstractmethod - @contextmanager - def download_progress_bar(self, desc: str) -> Iterator[Callable[[int, int], None]]: - pass - - @abstractmethod - @contextmanager - def update_progress_bar(self) -> Iterator[Callable[[int, int], None]]: - pass - - @abstractmethod - @contextmanager - def finalization_progress_bar(self) -> Iterator[Callable[[int, int], None]]: - pass - - -class Updater: - def __init__( - self, - ui: UpdateUi, - await_bootloader: Callable[[], Nitrokey3Bootloader], - await_device: Callable[ - [Optional[int], Optional[Callable[[int, int], None]]], Nitrokey3Device - ], - ) -> None: - self.ui = ui - self.await_bootloader = await_bootloader - self.await_device = await_device - - def update( - self, - device: NitrokeyTrussedBase, - image: Optional[str], - update_version: Optional[str], - ignore_pynitrokey_version: bool = False, - ) -> Version: - current_version = ( - device.admin.version() if isinstance(device, Nitrokey3Device) else None - ) - logger.info(f"Firmware version before update: {current_version or ''}") - container = self._prepare_update(image, update_version, current_version) - - if container.pynitrokey: - pynitrokey_version = Version.from_str(pynitrokey.__version__) - if container.pynitrokey > pynitrokey_version: - if ignore_pynitrokey_version: - self.ui.confirm_pynitrokey_version( - current=pynitrokey_version, required=container.pynitrokey - ) - else: - raise self.ui.abort_pynitrokey_version( - current=pynitrokey_version, required=container.pynitrokey - ) - - self.ui.confirm_update(current_version, container.version) - - with self._get_bootloader(device) as bootloader: - if bootloader.variant not in container.images: - raise self.ui.error( - "The firmware release does not contain an image for the " - f"{bootloader.variant.value} hardware variant" - ) - try: - validate_firmware_image( - bootloader.variant, - container.images[bootloader.variant], - container.version, - NK3_DATA, - ) - except Exception as e: - raise self.ui.error("Failed to validate firmware image", e) - - update_path = UpdatePath.create( - bootloader.variant, current_version, container.version - ) - txt = get_extra_information(update_path) - self.ui.confirm_extra_information(txt) - - self._perform_update(bootloader, container) - - wait_retries = get_finalization_wait_retries(update_path) - with self.ui.finalization_progress_bar() as callback: - with self.await_device(wait_retries, callback) as device: - version = device.admin.version() - if version != container.version: - raise self.ui.error( - f"The firmware update to {container.version} was successful, but the " - f"firmware is still reporting version {version}." - ) - - return container.version - - def _prepare_update( - self, - image: Optional[str], - version: Optional[str], - current_version: Optional[Version], - ) -> FirmwareContainer: - if image: - try: - container = FirmwareContainer.parse(image, Device.NITROKEY3) - except Exception as e: - raise self.ui.error("Failed to parse firmware container", e) - self._validate_version(current_version, container.version) - return container - else: - repository = NK3_DATA.firmware_repository - if version: - try: - logger.info(f"Downloading firmare version {version}") - release = repository.get_release(version) - except Exception as e: - raise self.ui.error(f"Failed to get firmware release {version}", e) - else: - try: - release = repository.get_latest_release() - logger.info(f"Latest firmware version: {release}") - except Exception as e: - raise self.ui.error("Failed to find latest firmware release", e) - - try: - release_version = Version.from_v_str(release.tag) - except ValueError as e: - raise self.ui.error("Failed to parse version from release tag", e) - self._validate_version(current_version, release_version) - self.ui.confirm_download(current_version, release_version) - return self._download_update(release) - - def _download_update(self, release: Release) -> FirmwareContainer: - try: - update = get_firmware_update(release) - except Exception as e: - raise self.ui.error( - f"Failed to find firmware image for release {release}", - e, - ) - - try: - logger.info(f"Trying to download firmware update from URL: {update.url}") - - with self.ui.download_progress_bar(update.tag) as callback: - data = update.read(callback=callback) - except Exception as e: - raise self.ui.error( - f"Failed to download latest firmware update {update.tag}", e - ) - - try: - container = FirmwareContainer.parse(BytesIO(data), Device.NITROKEY3) - except Exception as e: - raise self.ui.error( - f"Failed to parse firmware container for {update.tag}", e - ) - - release_version = Version.from_v_str(release.tag) - if release_version != container.version: - raise self.ui.error( - f"The firmware container for {update.tag} has the version {container.version}" - ) - - return container - - def _validate_version( - self, - current_version: Optional[Version], - new_version: Version, - ) -> None: - logger.info(f"Current firmware version: {current_version}") - logger.info(f"Updated firmware version: {new_version}") - - if current_version: - if current_version.core() > new_version.core(): - raise self.ui.abort_downgrade(current_version, new_version) - elif current_version == new_version: - if current_version.complete and new_version.complete: - same_version = current_version - else: - same_version = current_version.core() - self.ui.confirm_update_same_version(same_version) - - @contextmanager - def _get_bootloader( - self, device: NitrokeyTrussedBase - ) -> Iterator[Nitrokey3Bootloader]: - if isinstance(device, Nitrokey3Device): - self.ui.request_bootloader_confirmation() - try: - device.admin.reboot(BootMode.BOOTROM) - except TimeoutException: - raise self.ui.abort( - "The reboot was not confirmed with the touch button" - ) - - # needed for udev to properly handle new device - time.sleep(1) - - maybe_exc = self.ui.request_repeated_update() - if platform.system() == "Darwin" and maybe_exc is not None: - # Currently there is an issue with device enumeration after reboot on macOS, see - # . To avoid this issue, we - # cancel the command now and ask the user to run it again. - raise maybe_exc - - self.ui.pre_bootloader_hint() - - exc = None - for t in Retries(3): - logger.debug(f"Trying to connect to bootloader ({t})") - try: - with self.await_bootloader() as bootloader: - # noop to test communication - bootloader.uuid - yield bootloader - break - except McuBootConnectionError as e: - logger.debug("Received connection error", exc_info=True) - exc = e - else: - msgs = ["Failed to connect to Nitrokey 3 bootloader"] - if platform.system() == "Linux": - msgs += ["Are the Nitrokey udev rules installed and active?"] - raise self.ui.error(*msgs, exc) - elif isinstance(device, Nitrokey3Bootloader): - yield device - else: - raise self.ui.error(f"Unexpected Nitrokey 3 device: {device}") - - def _perform_update( - self, device: Nitrokey3Bootloader, container: FirmwareContainer - ) -> None: - logger.debug("Starting firmware update") - image = container.images[device.variant] - with self.ui.update_progress_bar() as callback: - try: - device.update(image, callback=callback) - except Exception as e: - raise self.ui.error("Failed to perform firmware update", e) - logger.debug("Firmware update finished successfully") - - -def test_update_path_default() -> None: - assert ( - UpdatePath.create(Variant.NRF52, Version(1, 0, 0), Version(1, 1, 0)) - == UpdatePath.default - ) - assert UpdatePath.create(None, None, Version(1, 1, 0)) == UpdatePath.default - - -def test_update_path_match() -> None: - assert ( - UpdatePath.create(Variant.NRF52, Version(1, 2, 2), Version(1, 3, 0)) - == UpdatePath.nRF_IFS_Migration_v1_3 - ) - assert ( - UpdatePath.create(Variant.NRF52, Version(1, 0, 0), Version(1, 3, 0)) - == UpdatePath.nRF_IFS_Migration_v1_3 - ) - assert ( - UpdatePath.create(Variant.NRF52, None, Version(1, 3, 0)) - == UpdatePath.nRF_IFS_Migration_v1_3 - ) diff --git a/pynitrokey/nkpk.py b/pynitrokey/nkpk.py deleted file mode 100644 index 72e47d68..00000000 --- a/pynitrokey/nkpk.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -from typing import List, Optional, Sequence - -from fido2.hid import CtapHidDevice - -from pynitrokey.trussed import VID_NITROKEY, DeviceData -from pynitrokey.trussed.base import NitrokeyTrussedBase -from pynitrokey.trussed.bootloader.nrf52 import ( - NitrokeyTrussedBootloaderNrf52, - SignatureKey, -) -from pynitrokey.trussed.device import NitrokeyTrussedDevice -from pynitrokey.trussed.utils import Fido2Certs, Version - -PID_NITROKEY_PASSKEY_DEVICE = 0x42F3 -PID_NITROKEY_PASSKEY_BOOTLOADER = 0x42F4 - -FIDO2_CERTS = [ - Fido2Certs( - start=Version(0, 1, 0), - hashes=[ - "c7512dfcd15ffc5a7b4000e4898e5956ee858027794c5086cc137a02cd15d123", - ], - ), -] - -NKPK_DATA = DeviceData( - name="Nitrokey Passkey", - firmware_repository_name="nitrokey-passkey-firmware", - firmware_pattern_string="firmware-nkpk-v.*\\.zip$", - nrf52_signature_keys=[ - SignatureKey( - name="Nitrokey", - is_official=True, - der="3059301306072a8648ce3d020106082a8648ce3d0301070342000445121cdf7a10826faa58c8cbe7bb1a40fe71c85c7756324eac09610d4710e9dadd473c0c9d35838b5cce301e796b2e14a8c29c86f0eb15f36325096506e275e6", - ), - SignatureKey( - name="Nitrokey Test", - is_official=False, - der="3059301306072a8648ce3d020106082a8648ce3d03010703420004d9a355a2927bd6ecb7ed714294d4692ad31ae9dd21853bf99e2cf7182d1acd6c2ada4a9707ab43f9e6194480d94e477dce4de9be5c35119c714bac459b21cbdc", - ), - ], -) - - -class NitrokeyPasskeyDevice(NitrokeyTrussedDevice): - def __init__(self, device: CtapHidDevice) -> None: - super().__init__(device, FIDO2_CERTS) - - @property - def pid(self) -> int: - return PID_NITROKEY_PASSKEY_DEVICE - - @property - def name(self) -> str: - return "Nitrokey Passkey" - - @classmethod - def from_device(cls, device: CtapHidDevice) -> "NitrokeyPasskeyDevice": - return cls(device) - - -class NitrokeyPasskeyBootloader(NitrokeyTrussedBootloaderNrf52): - @property - def name(self) -> str: - return "Nitrokey Passkey Bootloader" - - @property - def pid(self) -> int: - return PID_NITROKEY_PASSKEY_BOOTLOADER - - @classmethod - def list(cls) -> List["NitrokeyPasskeyBootloader"]: - return cls.list_vid_pid(VID_NITROKEY, PID_NITROKEY_PASSKEY_BOOTLOADER) - - @classmethod - def open(cls, path: str) -> Optional["NitrokeyPasskeyBootloader"]: - return cls.open_vid_pid(VID_NITROKEY, PID_NITROKEY_PASSKEY_BOOTLOADER, path) - - @property - def signature_keys(self) -> Sequence[SignatureKey]: - return NKPK_DATA.nrf52_signature_keys - - -def list() -> List[NitrokeyTrussedBase]: - devices: List[NitrokeyTrussedBase] = [] - devices.extend(NitrokeyPasskeyBootloader.list()) - devices.extend(NitrokeyPasskeyDevice.list()) - return devices - - -def open(path: str) -> Optional[NitrokeyTrussedBase]: - device = NitrokeyPasskeyDevice.open(path) - bootloader_device = NitrokeyPasskeyBootloader.open(path) - if device and bootloader_device: - raise Exception(f"Found multiple devices at path {path}") - if device: - return device - if bootloader_device: - return bootloader_device - return None diff --git a/pynitrokey/test_secrets_app.py b/pynitrokey/test_secrets_app.py index 6423657e..fd333337 100644 --- a/pynitrokey/test_secrets_app.py +++ b/pynitrokey/test_secrets_app.py @@ -18,6 +18,18 @@ import fido2 import pytest import tlv8 +from nitrokey.nk3.secrets_app import ( + Algorithm, + CCIDInstruction, + Instruction, + Kind, + PasswordSafeEntry, + RawBytes, + SecretsApp, + SecretsAppException, + Tag, +) +from nitrokey.trussed.device import App from pynitrokey.conftest import ( CALCULATE_ALL_COMMANDS, @@ -36,18 +48,6 @@ SECRET, CredEncryptionType, ) -from pynitrokey.nk3.secrets_app import ( - Algorithm, - CCIDInstruction, - Instruction, - Kind, - PasswordSafeEntry, - RawBytes, - SecretsApp, - SecretsAppException, - Tag, -) -from pynitrokey.trussed.device import App CREDENTIAL_LABEL_MAX_SIZE = 127 diff --git a/pynitrokey/trussed/__init__.py b/pynitrokey/trussed/__init__.py deleted file mode 100644 index 3fc395be..00000000 --- a/pynitrokey/trussed/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import re -from dataclasses import dataclass -from re import Pattern -from typing import TYPE_CHECKING - -from pynitrokey.updates import Repository - -if TYPE_CHECKING: - from .bootloader.nrf52 import SignatureKey - -VID_NITROKEY = 0x20A0 - - -@dataclass -class DeviceData: - name: str - firmware_repository_name: str - firmware_pattern_string: str - nrf52_signature_keys: list["SignatureKey"] - - @property - def firmware_repository(self) -> Repository: - return Repository(owner="Nitrokey", name=self.firmware_repository_name) - - @property - def firmware_pattern(self) -> Pattern[str]: - return re.compile(self.firmware_pattern_string) diff --git a/pynitrokey/trussed/admin_app.py b/pynitrokey/trussed/admin_app.py deleted file mode 100644 index d11621ef..00000000 --- a/pynitrokey/trussed/admin_app.py +++ /dev/null @@ -1,305 +0,0 @@ -import enum -import sys -from dataclasses import dataclass -from enum import Enum, IntFlag -from typing import Optional - -from fido2 import cbor -from fido2.ctap import CtapError - -from pynitrokey.helpers import local_critical, local_print - -from .device import App, NitrokeyTrussedDevice -from .exceptions import TimeoutException -from .utils import Uuid, Version - -RNG_LEN = 57 -UUID_LEN = 16 -VERSION_LEN = 4 - - -@enum.unique -class AdminCommand(Enum): - # legacy commands -- can be called directly or using the admin namespace - UPDATE = 0x51 - REBOOT = 0x53 - RNG = 0x60 - VERSION = 0x61 - UUID = 0x62 - LOCKED = 0x63 - - # new commands -- can only be called using the admin namespace - STATUS = 0x80 - TEST_SE050 = 0x81 - GET_CONFIG = 0x82 - SET_CONFIG = 0x83 - FACTORY_RESET = 0x84 - FACTORY_RESET_APP = 0x85 - - def is_legacy_command(self) -> bool: - if self == AdminCommand.UPDATE: - return True - if self == AdminCommand.REBOOT: - return True - if self == AdminCommand.RNG: - return True - if self == AdminCommand.VERSION: - return True - if self == AdminCommand.UUID: - return True - if self == AdminCommand.LOCKED: - return True - return False - - -@enum.unique -class BootMode(Enum): - FIRMWARE = enum.auto() - BOOTROM = enum.auto() - - -@enum.unique -class InitStatus(IntFlag): - NFC_ERROR = 0b0001 - INTERNAL_FLASH_ERROR = 0b0010 - EXTERNAL_FLASH_ERROR = 0b0100 - MIGRATION_ERROR = 0b1000 - SE050_ERROR = 0b00010000 - - def is_error(self) -> bool: - return self.value != 0 - - def __str__(self) -> str: - if self == 0: - return "ok" - errors = [error for error in InitStatus if error in self if error.name] - value = sum(errors) - messages = [error.name for error in errors if error.name] - if self.value != value: - messages.append("UNKNOWN") - return ", ".join(messages) + " (" + hex(self.value) + ")" - - -@enum.unique -class Variant(Enum): - USBIP = 0 - LPC55 = 1 - NRF52 = 2 - - -@dataclass -class Status: - init_status: Optional[InitStatus] = None - ifs_blocks: Optional[int] = None - efs_blocks: Optional[int] = None - variant: Optional[Variant] = None - - -@enum.unique -class FactoryResetStatus(Enum): - SUCCESS = 0 - NOT_CONFIRMED = 0x01 - APP_NOT_ALLOWED = 0x02 - APP_FAILED_PARSE = 0x03 - - @classmethod - def from_int(cls, i: int) -> Optional["FactoryResetStatus"]: - for status in FactoryResetStatus: - if status.value == i: - return status - return None - - @classmethod - def check(cls, i: int, msg: str) -> None: - status = FactoryResetStatus.from_int(i) - if status != FactoryResetStatus.SUCCESS: - if status is None: - raise Exception(f"Unknown error {i:x}") - if status == FactoryResetStatus.NOT_CONFIRMED: - error = "Operation was not confirmed with touch" - elif status == FactoryResetStatus.APP_NOT_ALLOWED: - error = "The application does not support factory reset through nitropy" - elif status == FactoryResetStatus.APP_FAILED_PARSE: - error = "The application name must be utf-8" - local_critical(f"{msg}: {error}", support_hint=False) - - -@enum.unique -class ConfigStatus(Enum): - SUCCESS = 0 - READ_FAILED = 1 - WRITE_FAILED = 2 - DESERIALIZATION_FAILED = 3 - SERIALIZATION_FAILED = 4 - INVALID_KEY = 5 - INVALID_VALUE = 6 - DATA_TOO_LONG = 7 - - @classmethod - def from_int(cls, i: int) -> Optional["ConfigStatus"]: - for status in ConfigStatus: - if status.value == i: - return status - return None - - @classmethod - def check(cls, i: int, msg: str) -> None: - status = ConfigStatus.from_int(i) - if status != ConfigStatus.SUCCESS: - if status: - error = str(status) - else: - error = f"unknown error {i:x}" - raise Exception(f"{msg}: {error}") - - -class AdminApp: - def __init__(self, device: NitrokeyTrussedDevice) -> None: - self.device = device - - def _call( - self, - command: AdminCommand, - response_len: Optional[int] = None, - data: bytes = b"", - ) -> Optional[bytes]: - try: - if command.is_legacy_command(): - return self.device._call( - command.value, - command.name, - response_len=response_len, - data=data, - ) - else: - return self.device._call_app( - App.ADMIN, - response_len=response_len, - data=command.value.to_bytes(1, "big") + data, - ) - except CtapError as e: - if e.code == CtapError.ERR.INVALID_COMMAND: - return None - else: - raise - - def is_locked(self) -> bool: - response = self._call(AdminCommand.LOCKED, response_len=1) - assert response is not None - return response[0] == 1 - - def reboot(self, mode: BootMode = BootMode.FIRMWARE) -> bool: - try: - if mode == BootMode.FIRMWARE: - self._call(AdminCommand.REBOOT) - elif mode == BootMode.BOOTROM: - try: - self._call(AdminCommand.UPDATE) - except CtapError as e: - # The admin app returns an Invalid Length error if the user confirmation - # request times out - if e.code == CtapError.ERR.INVALID_LENGTH: - raise TimeoutException() - else: - raise e - except OSError as e: - # OS error is expected as the device does not respond during the reboot - self.device.logger.debug("ignoring OSError after reboot", exc_info=e) - return True - - def rng(self) -> bytes: - data = self._call(AdminCommand.RNG, response_len=RNG_LEN) - assert data is not None - return data - - def status(self) -> Status: - status = Status() - reply = self._call(AdminCommand.STATUS) - if reply is not None: - if not reply: - raise ValueError("The device returned an empty status") - status.init_status = InitStatus(reply[0]) - if len(reply) >= 4: - status.ifs_blocks = reply[1] - status.efs_blocks = int.from_bytes(reply[2:4], "big") - if len(reply) >= 5: - try: - status.variant = Variant(reply[4]) - except ValueError: - pass - return status - - def uuid(self) -> Optional[Uuid]: - uuid = self._call(AdminCommand.UUID) - if uuid is None or len(uuid) == 0: - # Firmware version 1.0.0 does not support querying the UUID - return None - if len(uuid) != UUID_LEN: - raise ValueError(f"UUID response has invalid length {len(uuid)}") - return Uuid(int.from_bytes(uuid, byteorder="big")) - - def version(self) -> Version: - reply = self._call(AdminCommand.VERSION, data=bytes([0x01])) - assert reply is not None - if len(reply) == VERSION_LEN: - version = int.from_bytes(reply, "big") - return Version.from_int(version) - else: - return Version.from_str(reply.decode("utf-8")) - - def se050_tests(self) -> Optional[bytes]: - return self._call(AdminCommand.TEST_SE050) - - def has_config(self, key: str) -> bool: - reply = self._call(AdminCommand.GET_CONFIG, data=key.encode()) - if not reply or len(reply) < 1: - return False - return ConfigStatus.from_int(reply[0]) == ConfigStatus.SUCCESS - - def get_config(self, key: str) -> str: - reply = self._call(AdminCommand.GET_CONFIG, data=key.encode()) - if not reply or len(reply) < 1: - raise ValueError("The device returned an empty response") - ConfigStatus.check(reply[0], "Failed to get config value") - return reply[1:].decode() - - def set_config(self, key: str, value: str) -> None: - request = cbor.encode({"key": key, "value": value}) - reply = self._call(AdminCommand.SET_CONFIG, data=request, response_len=1) - assert reply - ConfigStatus.check(reply[0], "Failed to set config value") - - def factory_reset(self) -> None: - try: - local_print( - "Please touch the device to confirm the operation", file=sys.stderr - ) - reply = self._call(AdminCommand.FACTORY_RESET, response_len=1) - if reply is None: - local_critical( - "Factory reset is not supported by the firmware version on the device", - support_hint=False, - ) - return - except OSError as e: - if e.errno == 5: - self.device.logger.debug("ignoring OSError after reboot", exc_info=e) - return - else: - raise e - FactoryResetStatus.check(reply[0], "Failed to factory reset the device") - - def factory_reset_app(self, application: str) -> None: - local_print("Please touch the device to confirm the operation", file=sys.stderr) - reply = self._call( - AdminCommand.FACTORY_RESET_APP, - data=application.encode("ascii"), - response_len=1, - ) - if reply is None: - local_critical( - "Application Factory reset is not supported by the firmware version on the device", - support_hint=False, - ) - return - FactoryResetStatus.check(reply[0], "Failed to factory reset the device") diff --git a/pynitrokey/trussed/base.py b/pynitrokey/trussed/base.py deleted file mode 100644 index 3608ddf2..00000000 --- a/pynitrokey/trussed/base.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -from abc import ABC, abstractmethod -from typing import Optional, TypeVar - -from . import VID_NITROKEY -from .utils import Uuid - -T = TypeVar("T", bound="NitrokeyTrussedBase") - - -class NitrokeyTrussedBase(ABC): - """ - Base class for Nitrokey devices using the Trussed framework and running - the firmware or the bootloader. - """ - - def __enter__(self: T) -> T: - return self - - def __exit__(self, exc_type: None, exc_val: None, exc_tb: None) -> None: - self.close() - - def validate_vid_pid(self, vid: int, pid: int) -> None: - if (vid, pid) != (self.vid, self.pid): - raise ValueError( - f"Not a {self.name} device: expected VID:PID " - f"{self.vid:x}:{self.pid:x}, got {vid:x}:{pid:x}" - ) - - @property - def vid(self) -> int: - return VID_NITROKEY - - @property - @abstractmethod - def pid(self) -> int: - ... - - @property - @abstractmethod - def path(self) -> str: - ... - - @property - @abstractmethod - def name(self) -> str: - ... - - @abstractmethod - def close(self) -> None: - ... - - @abstractmethod - def reboot(self) -> bool: - ... - - @abstractmethod - def uuid(self) -> Optional[Uuid]: - ... diff --git a/pynitrokey/trussed/bootloader/__init__.py b/pynitrokey/trussed/bootloader/__init__.py deleted file mode 100644 index 1e60afeb..00000000 --- a/pynitrokey/trussed/bootloader/__init__.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import enum -import hashlib -import json -import logging -import sys -from abc import abstractmethod -from dataclasses import dataclass -from io import BytesIO -from re import Pattern -from typing import Callable, Dict, Optional, Tuple, Union -from zipfile import ZipFile - -from .. import DeviceData -from ..base import NitrokeyTrussedBase -from ..utils import Version - -logger = logging.getLogger(__name__) - - -ProgressCallback = Callable[[int, int], None] - - -class Device(enum.Enum): - NITROKEY3 = "Nitrokey 3" - NITROKEY_PASSKEY = "Nitrokey Passkey" - - @classmethod - def from_str(cls, s: str) -> "Device": - for device in cls: - if device.value == s: - return device - raise ValueError(f"Unknown device {s}") - - -class Variant(enum.Enum): - LPC55 = "lpc55" - NRF52 = "nrf52" - - @classmethod - def from_str(cls, s: str) -> "Variant": - for variant in cls: - if variant.value == s: - return variant - raise ValueError(f"Unknown variant {s}") - - -def _validate_checksum(checksums: dict[str, str], path: str, data: bytes) -> None: - if path not in checksums: - raise ValueError(f"Missing checksum for file {path} in firmware container") - m = hashlib.sha256() - m.update(data) - checksum = m.hexdigest() - if checksum != checksums[path]: - raise ValueError(f"Invalid checksum for file {path} in firmware container") - - -@dataclass -class FirmwareContainer: - version: Version - pynitrokey: Optional[Version] - images: Dict[Variant, bytes] - - @classmethod - def parse(cls, path: Union[str, BytesIO], device: Device) -> "FirmwareContainer": - with ZipFile(path) as z: - checksum_lines = z.read("sha256sums").decode("utf-8").splitlines() - checksum_pairs = [line.split(" ", maxsplit=1) for line in checksum_lines] - checksums = {path: checksum for checksum, path in checksum_pairs} - - manifest_bytes = z.read("manifest.json") - _validate_checksum(checksums, "manifest.json", manifest_bytes) - manifest = json.loads(manifest_bytes) - actual_device = Device.from_str(manifest["device"]) - if actual_device != device: - raise ValueError( - f"Expected firmware container for {device.value}, got {actual_device.value}" - ) - version = Version.from_v_str(manifest["version"]) - pynitrokey = None - if "pynitrokey" in manifest: - pynitrokey = Version.from_v_str(manifest["pynitrokey"]) - - images = {} - for variant, image in manifest["images"].items(): - image_bytes = z.read(image) - _validate_checksum(checksums, image, image_bytes) - images[Variant.from_str(variant)] = image_bytes - - return cls( - version=version, - pynitrokey=pynitrokey, - images=images, - ) - - -@dataclass -class FirmwareMetadata: - version: Version - signed_by: Optional[str] = None - signed_by_nitrokey: bool = False - - -class NitrokeyTrussedBootloader(NitrokeyTrussedBase): - @abstractmethod - def update( - self, - image: bytes, - callback: Optional[ProgressCallback] = None, - ) -> None: - ... - - @property - @abstractmethod - def variant(self) -> Variant: - ... - - -def get_firmware_filename_pattern(variant: Variant) -> Pattern[str]: - from .lpc55 import FILENAME_PATTERN as FILENAME_PATTERN_LPC55 - from .nrf52 import FILENAME_PATTERN as FILENAME_PATTERN_NRF52 - - if variant == Variant.LPC55: - return FILENAME_PATTERN_LPC55 - elif variant == Variant.NRF52: - return FILENAME_PATTERN_NRF52 - else: - raise ValueError(f"Unexpected variant {variant}") - - -def parse_filename(filename: str) -> Optional[Tuple[Variant, Version]]: - for variant in Variant: - pattern = get_firmware_filename_pattern(variant) - match = pattern.search(filename) - if match: - version = Version.from_v_str(match.group("version")) - return (variant, version) - return None - - -def validate_firmware_image( - variant: Variant, - data: bytes, - version: Optional[Version], - device: DeviceData, -) -> FirmwareMetadata: - try: - metadata = parse_firmware_image(variant, data, device) - except Exception: - logger.exception("Failed to parse firmware image", exc_info=sys.exc_info()) - raise Exception("Failed to parse firmware image") - - if version: - if version.core() != metadata.version: - raise Exception( - f"The firmware image for the release {version} has an unexpected product " - f"version ({metadata.version})." - ) - - if not metadata.signed_by: - raise Exception("Firmware image is not signed") - - if not metadata.signed_by_nitrokey: - raise Exception( - f"Firmware image is not signed by Nitrokey (signed by: {metadata.signed_by})" - ) - - return metadata - - -def parse_firmware_image( - variant: Variant, data: bytes, device: DeviceData -) -> FirmwareMetadata: - from .lpc55 import parse_firmware_image as parse_firmware_image_lpc55 - from .nrf52 import parse_firmware_image as parse_firmware_image_nrf52 - - if variant == Variant.LPC55: - return parse_firmware_image_lpc55(data) - elif variant == Variant.NRF52: - return parse_firmware_image_nrf52(data, device.nrf52_signature_keys) - else: - raise ValueError(f"Unexpected variant {variant}") diff --git a/pynitrokey/trussed/bootloader/lpc55.py b/pynitrokey/trussed/bootloader/lpc55.py deleted file mode 100644 index c3f0e005..00000000 --- a/pynitrokey/trussed/bootloader/lpc55.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import logging -import platform -import re -import sys -from typing import Optional, TypeVar - -from spsdk.mboot.interfaces.usb import MbootUSBInterface -from spsdk.mboot.mcuboot import McuBoot -from spsdk.mboot.properties import PropertyTag -from spsdk.sbfile.sb2.images import BootImageV21 -from spsdk.utils.interfaces.device.usb_device import UsbDevice -from spsdk.utils.usbfilter import USBDeviceFilter - -from pynitrokey.trussed.utils import Uuid, Version - -from . import FirmwareMetadata, NitrokeyTrussedBootloader, ProgressCallback, Variant - -RKTH = bytes.fromhex("050aad3e77791a81e59c5b2ba5a158937e9460ee325d8ccba09734b8fdebb171") -KEK = bytes([0xAA] * 32) -UUID_LEN = 4 -FILENAME_PATTERN = re.compile("(firmware|alpha)-nk3..-lpc55-(?P.*)\\.sb2$") - -T = TypeVar("T", bound="NitrokeyTrussedBootloaderLpc55") - -logger = logging.getLogger(__name__) - - -class NitrokeyTrussedBootloaderLpc55(NitrokeyTrussedBootloader): - """A Nitrokey 3 device running the LPC55 bootloader.""" - - def __init__(self, device: UsbDevice): - self.validate_vid_pid(device.vid, device.pid) - self._path = device.path - self.device = McuBoot(MbootUSBInterface(device)) - - def __enter__(self: T) -> T: - self.device.open() - return self - - @property - def variant(self) -> Variant: - return Variant.LPC55 - - @property - def path(self) -> str: - if isinstance(self._path, bytes): - return self._path.decode("UTF-8") - return self._path - - @property - def status(self) -> str: - return self.device.status_string - - def close(self) -> None: - self.device.close() - - def reboot(self) -> bool: - if not self.device.reset(reopen=False): - # On Windows, this function returns false even if the reset was successful - if platform.system() == "Windows": - logger.warning("Failed to reboot Nitrokey 3 bootloader") - else: - raise Exception("Failed to reboot Nitrokey 3 bootloader") - return True - - def uuid(self) -> Optional[Uuid]: - uuid = self.device.get_property(PropertyTag.UNIQUE_DEVICE_IDENT) - if not uuid: - raise ValueError("Missing response for UUID property query") - if len(uuid) != UUID_LEN: - raise ValueError(f"UUID response has invalid length {len(uuid)}") - - # See GetProperties::device_uuid in the lpc55 crate: - # https://github.com/lpc55/lpc55-host/blob/main/src/bootloader/property.rs#L222 - wrong_endian = (uuid[3] << 96) + (uuid[2] << 64) + (uuid[1] << 32) + uuid[0] - right_endian = wrong_endian.to_bytes(16, byteorder="little") - return Uuid(int.from_bytes(right_endian, byteorder="big")) - - def update( - self, - image: bytes, - callback: Optional[ProgressCallback] = None, - check_errors: bool = False, - ) -> None: - success = self.device.receive_sb_file( - image, - progress_callback=callback, - check_errors=check_errors, - ) - logger.debug(f"Firmware update finished with status {self.status}") - if success: - self.reboot() - else: - raise Exception(f"Firmware update failed with status {self.status}") - - @classmethod - def list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]: - device_filter = USBDeviceFilter(f"0x{vid:x}:0x{pid:x}") - devices = [] - for device in UsbDevice.enumerate(device_filter): - try: - devices.append(cls(device)) - except ValueError: - logger.warn( - f"Invalid Nitrokey 3 LPC55 bootloader returned by enumeration: {device}" - ) - return devices - - @classmethod - def open(cls: type[T], path: str) -> Optional[T]: - device_filter = USBDeviceFilter(path) - devices = UsbDevice.enumerate(device_filter) - if len(devices) == 0: - logger.warn(f"No HID device at {path}") - return None - if len(devices) > 1: - logger.warn(f"Multiple HID devices at {path}: {devices}") - return None - - try: - return cls(devices[0]) - except ValueError: - logger.warn( - f"No Nitrokey 3 bootloader at path {path}", exc_info=sys.exc_info() - ) - return None - - -def parse_firmware_image(data: bytes) -> FirmwareMetadata: - image = BootImageV21.parse(data, kek=KEK) - version = Version.from_bcd_version(image.header.product_version) - metadata = FirmwareMetadata(version=version) - if image.cert_block: - if image.cert_block.rkth == RKTH: - metadata.signed_by = "Nitrokey" - metadata.signed_by_nitrokey = True - else: - metadata.signed_by = f"unknown issuer (RKTH: {image.cert_block.rkth.hex()})" - return metadata diff --git a/pynitrokey/trussed/bootloader/nrf52.py b/pynitrokey/trussed/bootloader/nrf52.py deleted file mode 100644 index 5b76c0e7..00000000 --- a/pynitrokey/trussed/bootloader/nrf52.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import hashlib -import logging -import re -import time -from abc import abstractmethod -from dataclasses import dataclass -from io import BytesIO -from typing import Optional, Sequence, TypeVar -from zipfile import ZipFile - -import ecdsa -import ecdsa.curves -from ecdsa.keys import BadSignatureError - -from pynitrokey.trussed.utils import Uuid, Version - -from . import FirmwareMetadata, NitrokeyTrussedBootloader, ProgressCallback, Variant -from .nrf52_upload.dfu.dfu_transport import DfuEvent -from .nrf52_upload.dfu.dfu_transport_serial import DfuTransportSerial -from .nrf52_upload.dfu.init_packet_pb import InitPacketPB -from .nrf52_upload.dfu.manifest import Manifest -from .nrf52_upload.dfu.package import Package -from .nrf52_upload.lister.device_lister import DeviceLister - -logger = logging.getLogger(__name__) - -FILENAME_PATTERN = re.compile( - "(firmware|alpha)-(nk3..|nkpk)-nrf52-(?P.*)\\.zip$" -) - -T = TypeVar("T", bound="NitrokeyTrussedBootloaderNrf52") - - -@dataclass -class SignatureKey: - name: str - is_official: bool - # generate with: - # $ openssl ec -in dfu_public.pem -inform pem -pubin -outform der | xxd -p - der: str - - def vk(self) -> ecdsa.VerifyingKey: - return ecdsa.VerifyingKey.from_der(bytes.fromhex(self.der)) - - def verify(self, signature: str, message: str) -> bool: - try: - self.vk().verify( - signature, - message, - hashfunc=hashlib.sha256, - ) - return True - except BadSignatureError: - return False - - -@dataclass -class Image: - init_packet: InitPacketPB - firmware_dat: bytes - firmware_bin: bytes - is_signed: bool = False - signature_key: Optional[SignatureKey] = None - - @classmethod - def parse(cls, data: bytes, keys: Sequence[SignatureKey]) -> "Image": - io = BytesIO(data) - with ZipFile(io) as pkg: - with pkg.open(Package.MANIFEST_FILENAME) as f: - manifest = Manifest.from_json(f.read()) - if not manifest.application: - raise Exception("Missing application in firmware package manifest") - if not manifest.application.dat_file: - raise Exception( - "Missing dat file for application in firmware package manifest" - ) - if not manifest.application.bin_file: - raise Exception( - "Missing bin file for application in firmware package manifest" - ) - with pkg.open(manifest.application.dat_file, "r") as f: - firmware_dat = f.read() - with pkg.open(manifest.application.bin_file, "r") as f: - firmware_bin = f.read() - init_packet = InitPacketPB(from_bytes=firmware_dat) - - if init_packet.init_command.app_size != len(firmware_bin): - raise Exception("Invalid app size") - - h = hashlib.sha256() - h.update(firmware_bin) - hash = bytes(reversed(h.digest())) - if hash != init_packet.init_command.hash.hash: - raise Exception("Invalid hash for firmware image") - - image = cls( - init_packet=init_packet, - firmware_dat=firmware_dat, - firmware_bin=firmware_bin, - ) - - if init_packet.packet.signed_command: - image.is_signed = True - signature = init_packet.packet.signed_command.signature - # see nordicsemi.dfu.signing.Signing.sign - signature = signature[31::-1] + signature[63:31:-1] - message = init_packet.get_init_command_bytes() - for key in keys: - if key.verify(signature, message): - image.signature_key = key - - return image - - -class NitrokeyTrussedBootloaderNrf52(NitrokeyTrussedBootloader): - def __init__(self, path: str, uuid: int) -> None: - self._path = path - self._uuid = uuid - - @property - def variant(self) -> Variant: - return Variant.NRF52 - - @property - def path(self) -> str: - return self._path - - @property - @abstractmethod - def signature_keys(self) -> Sequence[SignatureKey]: - ... - - def close(self) -> None: - pass - - def reboot(self) -> bool: - return False - - def uuid(self) -> Optional[Uuid]: - return Uuid(self._uuid) - - def update(self, data: bytes, callback: Optional[ProgressCallback] = None) -> None: - # based on https://github.com/NordicSemiconductor/pc-nrfutil/blob/1caa347b1cca3896f4695823f48abba15fbef76b/nordicsemi/dfu/dfu.py - # we have to implement this ourselves because we want to read the files - # from memory, not from the filesystem - - image = Image.parse(data, self.signature_keys) - - time.sleep(3) - - dfu = DfuTransportSerial(self.path) - - if callback: - total = len(image.firmware_bin) - callback(0, total) - dfu.register_events_callback( - DfuEvent.PROGRESS_EVENT, - CallbackWrapper(callback, total), - ) - - dfu.open() - dfu.send_init_packet(image.firmware_dat) - dfu.send_firmware(image.firmware_bin) - dfu.close() - - @classmethod - def list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]: - return [cls(port, serial) for port, serial in _list_ports(vid, pid)] - - @classmethod - def open_vid_pid(cls: type[T], vid: int, pid: int, path: str) -> Optional[T]: - for port, serial in _list_ports(vid, pid): - if path == port: - return cls(path, serial) - return None - - -@dataclass -class CallbackWrapper: - callback: ProgressCallback - total: int - n: int = 0 - - def __call__(self, progress: int) -> None: - self.n += progress - self.callback(self.n, self.total) - - -def _list_ports(vid: int, pid: int) -> list[tuple[str, int]]: - ports = [] - for device in DeviceLister().enumerate(): - vendor_id = int(device.vendor_id, base=16) - product_id = int(device.product_id, base=16) - assert device.com_ports - if len(device.com_ports) > 1: - logger.warn( - f"Nitrokey 3 NRF52 bootloader has multiple com ports: {device.com_ports}" - ) - if vendor_id == vid and product_id == pid: - port = device.com_ports[0] - serial = int(device.serial_number, base=16) - logger.debug(f"Found Nitrokey 3 NRF52 bootloader with port {port}") - ports.append((port, serial)) - else: - logger.debug( - f"Skipping device {vendor_id:x}:{product_id:x} with ports {device.com_ports}" - ) - return ports - - -def parse_firmware_image(data: bytes, keys: Sequence[SignatureKey]) -> FirmwareMetadata: - image = Image.parse(data, keys) - version = Version.from_int(image.init_packet.init_command.fw_version) - metadata = FirmwareMetadata(version=version) - - if image.is_signed: - metadata.signed_by = ( - image.signature_key.name if image.signature_key else "unknown" - ) - if image.signature_key: - metadata.signed_by_nitrokey = image.signature_key.is_official - - return metadata diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/README.md b/pynitrokey/trussed/bootloader/nrf52_upload/README.md deleted file mode 100644 index 402709b8..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# nRF52 Bootloader Firmware Upload Module - -Anything inside this directory is originally extracted from: https://github.com/NordicSemiconductor/pc-nrfutil. -In detail anything that is needed to upload a signed firmware image to a Nitrokey 3 Mini with an nRF MCU. - - diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/__init__.py b/pynitrokey/trussed/bootloader/nrf52_upload/__init__.py deleted file mode 100644 index c2d2a09a..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -"""Package implementing Zigbee OTA DFU functionality.""" diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/__init__.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/__init__.py deleted file mode 100644 index bc11c967..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -"""Package marker file.""" diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/crc16.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/crc16.py deleted file mode 100644 index 5277638c..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/crc16.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - - -def calc_crc16(binary_data: bytes, crc=0xFFFF): - """ - Calculates CRC16 on binary_data - - :param int crc: CRC value to start calculation with - :param bytearray binary_data: Array with data to run CRC16 calculation on - :return int: Calculated CRC value of binary_data - """ - - for b in binary_data: - crc = (crc >> 8 & 0x00FF) | (crc << 8 & 0xFF00) - crc ^= ord(b) - crc ^= (crc & 0x00FF) >> 4 - crc ^= (crc << 8) << 4 - crc ^= ((crc & 0x00FF) << 4) << 1 - return crc & 0xFFFF diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_cc_pb2.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_cc_pb2.py deleted file mode 100644 index ee1e260a..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_cc_pb2.py +++ /dev/null @@ -1,866 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: nordicsemi/dfu/dfu-cc.proto - -import sys - -_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 -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import enum_type_wrapper - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor.FileDescriptor( - name="nordicsemi/dfu/dfu-cc.proto", - package="dfu", - syntax="proto2", - serialized_options=None, - serialized_pb=_b( - '\n\x1bnordicsemi/dfu/dfu-cc.proto\x12\x03\x64\x66u"6\n\x04Hash\x12 \n\thash_type\x18\x01 \x02(\x0e\x32\r.dfu.HashType\x12\x0c\n\x04hash\x18\x02 \x02(\x0c"B\n\x0e\x42ootValidation\x12!\n\x04type\x18\x01 \x02(\x0e\x32\x13.dfu.ValidationType\x12\r\n\x05\x62ytes\x18\x02 \x02(\x0c"\xf8\x01\n\x0bInitCommand\x12\x12\n\nfw_version\x18\x01 \x01(\r\x12\x12\n\nhw_version\x18\x02 \x01(\r\x12\x12\n\x06sd_req\x18\x03 \x03(\rB\x02\x10\x01\x12\x19\n\x04type\x18\x04 \x01(\x0e\x32\x0b.dfu.FwType\x12\x0f\n\x07sd_size\x18\x05 \x01(\r\x12\x0f\n\x07\x62l_size\x18\x06 \x01(\r\x12\x10\n\x08\x61pp_size\x18\x07 \x01(\r\x12\x17\n\x04hash\x18\x08 \x01(\x0b\x32\t.dfu.Hash\x12\x17\n\x08is_debug\x18\t \x01(\x08:\x05\x66\x61lse\x12,\n\x0f\x62oot_validation\x18\n \x03(\x0b\x32\x13.dfu.BootValidation"\x1f\n\x0cResetCommand\x12\x0f\n\x07timeout\x18\x01 \x02(\r"i\n\x07\x43ommand\x12\x1c\n\x07op_code\x18\x01 \x01(\x0e\x32\x0b.dfu.OpCode\x12\x1e\n\x04init\x18\x02 \x01(\x0b\x32\x10.dfu.InitCommand\x12 \n\x05reset\x18\x03 \x01(\x0b\x32\x11.dfu.ResetCommand"m\n\rSignedCommand\x12\x1d\n\x07\x63ommand\x18\x01 \x02(\x0b\x32\x0c.dfu.Command\x12*\n\x0esignature_type\x18\x02 \x02(\x0e\x32\x12.dfu.SignatureType\x12\x11\n\tsignature\x18\x03 \x02(\x0c"S\n\x06Packet\x12\x1d\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x0c.dfu.Command\x12*\n\x0esigned_command\x18\x02 \x01(\x0b\x32\x12.dfu.SignedCommand*\x1d\n\x06OpCode\x12\t\n\x05RESET\x10\x00\x12\x08\n\x04INIT\x10\x01*n\n\x06\x46wType\x12\x0f\n\x0b\x41PPLICATION\x10\x00\x12\x0e\n\nSOFTDEVICE\x10\x01\x12\x0e\n\nBOOTLOADER\x10\x02\x12\x19\n\x15SOFTDEVICE_BOOTLOADER\x10\x03\x12\x18\n\x14\x45XTERNAL_APPLICATION\x10\x04*D\n\x08HashType\x12\x0b\n\x07NO_HASH\x10\x00\x12\x07\n\x03\x43RC\x10\x01\x12\n\n\x06SHA128\x10\x02\x12\n\n\x06SHA256\x10\x03\x12\n\n\x06SHA512\x10\x04*t\n\x0eValidationType\x12\x11\n\rNO_VALIDATION\x10\x00\x12\x1a\n\x16VALIDATE_GENERATED_CRC\x10\x01\x12\x13\n\x0fVALIDATE_SHA256\x10\x02\x12\x1e\n\x1aVALIDATE_ECDSA_P256_SHA256\x10\x03*3\n\rSignatureType\x12\x15\n\x11\x45\x43\x44SA_P256_SHA256\x10\x00\x12\x0b\n\x07\x45\x44\x32\x35\x35\x31\x39\x10\x01' - ), -) - -_OPCODE = _descriptor.EnumDescriptor( - name="OpCode", - full_name="dfu.OpCode", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="RESET", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="INIT", index=1, number=1, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=747, - serialized_end=776, -) -_sym_db.RegisterEnumDescriptor(_OPCODE) - -OpCode = enum_type_wrapper.EnumTypeWrapper(_OPCODE) -_FWTYPE = _descriptor.EnumDescriptor( - name="FwType", - full_name="dfu.FwType", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="APPLICATION", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SOFTDEVICE", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="BOOTLOADER", index=2, number=2, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SOFTDEVICE_BOOTLOADER", - index=3, - number=3, - serialized_options=None, - type=None, - ), - _descriptor.EnumValueDescriptor( - name="EXTERNAL_APPLICATION", - index=4, - number=4, - serialized_options=None, - type=None, - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=778, - serialized_end=888, -) -_sym_db.RegisterEnumDescriptor(_FWTYPE) - -FwType = enum_type_wrapper.EnumTypeWrapper(_FWTYPE) -_HASHTYPE = _descriptor.EnumDescriptor( - name="HashType", - full_name="dfu.HashType", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="NO_HASH", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="CRC", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SHA128", index=2, number=2, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SHA256", index=3, number=3, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SHA512", index=4, number=4, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=890, - serialized_end=958, -) -_sym_db.RegisterEnumDescriptor(_HASHTYPE) - -HashType = enum_type_wrapper.EnumTypeWrapper(_HASHTYPE) -_VALIDATIONTYPE = _descriptor.EnumDescriptor( - name="ValidationType", - full_name="dfu.ValidationType", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="NO_VALIDATION", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="VALIDATE_GENERATED_CRC", - index=1, - number=1, - serialized_options=None, - type=None, - ), - _descriptor.EnumValueDescriptor( - name="VALIDATE_SHA256", - index=2, - number=2, - serialized_options=None, - type=None, - ), - _descriptor.EnumValueDescriptor( - name="VALIDATE_ECDSA_P256_SHA256", - index=3, - number=3, - serialized_options=None, - type=None, - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=960, - serialized_end=1076, -) -_sym_db.RegisterEnumDescriptor(_VALIDATIONTYPE) - -ValidationType = enum_type_wrapper.EnumTypeWrapper(_VALIDATIONTYPE) -_SIGNATURETYPE = _descriptor.EnumDescriptor( - name="SignatureType", - full_name="dfu.SignatureType", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="ECDSA_P256_SHA256", - index=0, - number=0, - serialized_options=None, - type=None, - ), - _descriptor.EnumValueDescriptor( - name="ED25519", index=1, number=1, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=1078, - serialized_end=1129, -) -_sym_db.RegisterEnumDescriptor(_SIGNATURETYPE) - -SignatureType = enum_type_wrapper.EnumTypeWrapper(_SIGNATURETYPE) -RESET = 0 -INIT = 1 -APPLICATION = 0 -SOFTDEVICE = 1 -BOOTLOADER = 2 -SOFTDEVICE_BOOTLOADER = 3 -EXTERNAL_APPLICATION = 4 -NO_HASH = 0 -CRC = 1 -SHA128 = 2 -SHA256 = 3 -SHA512 = 4 -NO_VALIDATION = 0 -VALIDATE_GENERATED_CRC = 1 -VALIDATE_SHA256 = 2 -VALIDATE_ECDSA_P256_SHA256 = 3 -ECDSA_P256_SHA256 = 0 -ED25519 = 1 - - -_HASH = _descriptor.Descriptor( - name="Hash", - full_name="dfu.Hash", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="hash_type", - full_name="dfu.Hash.hash_type", - index=0, - number=1, - type=14, - cpp_type=8, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="hash", - full_name="dfu.Hash.hash", - index=1, - number=2, - type=12, - cpp_type=9, - label=2, - has_default_value=False, - default_value=_b(""), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=36, - serialized_end=90, -) - - -_BOOTVALIDATION = _descriptor.Descriptor( - name="BootValidation", - full_name="dfu.BootValidation", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="type", - full_name="dfu.BootValidation.type", - index=0, - number=1, - type=14, - cpp_type=8, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="bytes", - full_name="dfu.BootValidation.bytes", - index=1, - number=2, - type=12, - cpp_type=9, - label=2, - has_default_value=False, - default_value=_b(""), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=92, - serialized_end=158, -) - - -_INITCOMMAND = _descriptor.Descriptor( - name="InitCommand", - full_name="dfu.InitCommand", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="fw_version", - full_name="dfu.InitCommand.fw_version", - index=0, - number=1, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="hw_version", - full_name="dfu.InitCommand.hw_version", - index=1, - number=2, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="sd_req", - full_name="dfu.InitCommand.sd_req", - index=2, - number=3, - type=13, - cpp_type=3, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=_b("\020\001"), - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="type", - full_name="dfu.InitCommand.type", - index=3, - number=4, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="sd_size", - full_name="dfu.InitCommand.sd_size", - index=4, - number=5, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="bl_size", - full_name="dfu.InitCommand.bl_size", - index=5, - number=6, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="app_size", - full_name="dfu.InitCommand.app_size", - index=6, - number=7, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="hash", - full_name="dfu.InitCommand.hash", - index=7, - number=8, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="is_debug", - full_name="dfu.InitCommand.is_debug", - index=8, - number=9, - type=8, - cpp_type=7, - label=1, - has_default_value=True, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="boot_validation", - full_name="dfu.InitCommand.boot_validation", - index=9, - number=10, - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=161, - serialized_end=409, -) - - -_RESETCOMMAND = _descriptor.Descriptor( - name="ResetCommand", - full_name="dfu.ResetCommand", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="timeout", - full_name="dfu.ResetCommand.timeout", - index=0, - number=1, - type=13, - cpp_type=3, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=411, - serialized_end=442, -) - - -_COMMAND = _descriptor.Descriptor( - name="Command", - full_name="dfu.Command", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="op_code", - full_name="dfu.Command.op_code", - index=0, - number=1, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="init", - full_name="dfu.Command.init", - index=1, - number=2, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="reset", - full_name="dfu.Command.reset", - index=2, - number=3, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=444, - serialized_end=549, -) - - -_SIGNEDCOMMAND = _descriptor.Descriptor( - name="SignedCommand", - full_name="dfu.SignedCommand", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="command", - full_name="dfu.SignedCommand.command", - index=0, - number=1, - type=11, - cpp_type=10, - label=2, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="signature_type", - full_name="dfu.SignedCommand.signature_type", - index=1, - number=2, - type=14, - cpp_type=8, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="signature", - full_name="dfu.SignedCommand.signature", - index=2, - number=3, - type=12, - cpp_type=9, - label=2, - has_default_value=False, - default_value=_b(""), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=551, - serialized_end=660, -) - - -_PACKET = _descriptor.Descriptor( - name="Packet", - full_name="dfu.Packet", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="command", - full_name="dfu.Packet.command", - index=0, - number=1, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="signed_command", - full_name="dfu.Packet.signed_command", - index=1, - number=2, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=662, - serialized_end=745, -) - -_HASH.fields_by_name["hash_type"].enum_type = _HASHTYPE -_BOOTVALIDATION.fields_by_name["type"].enum_type = _VALIDATIONTYPE -_INITCOMMAND.fields_by_name["type"].enum_type = _FWTYPE -_INITCOMMAND.fields_by_name["hash"].message_type = _HASH -_INITCOMMAND.fields_by_name["boot_validation"].message_type = _BOOTVALIDATION -_COMMAND.fields_by_name["op_code"].enum_type = _OPCODE -_COMMAND.fields_by_name["init"].message_type = _INITCOMMAND -_COMMAND.fields_by_name["reset"].message_type = _RESETCOMMAND -_SIGNEDCOMMAND.fields_by_name["command"].message_type = _COMMAND -_SIGNEDCOMMAND.fields_by_name["signature_type"].enum_type = _SIGNATURETYPE -_PACKET.fields_by_name["command"].message_type = _COMMAND -_PACKET.fields_by_name["signed_command"].message_type = _SIGNEDCOMMAND -DESCRIPTOR.message_types_by_name["Hash"] = _HASH -DESCRIPTOR.message_types_by_name["BootValidation"] = _BOOTVALIDATION -DESCRIPTOR.message_types_by_name["InitCommand"] = _INITCOMMAND -DESCRIPTOR.message_types_by_name["ResetCommand"] = _RESETCOMMAND -DESCRIPTOR.message_types_by_name["Command"] = _COMMAND -DESCRIPTOR.message_types_by_name["SignedCommand"] = _SIGNEDCOMMAND -DESCRIPTOR.message_types_by_name["Packet"] = _PACKET -DESCRIPTOR.enum_types_by_name["OpCode"] = _OPCODE -DESCRIPTOR.enum_types_by_name["FwType"] = _FWTYPE -DESCRIPTOR.enum_types_by_name["HashType"] = _HASHTYPE -DESCRIPTOR.enum_types_by_name["ValidationType"] = _VALIDATIONTYPE -DESCRIPTOR.enum_types_by_name["SignatureType"] = _SIGNATURETYPE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Hash = _reflection.GeneratedProtocolMessageType( - "Hash", - (_message.Message,), - dict( - DESCRIPTOR=_HASH, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.Hash) - ), -) -_sym_db.RegisterMessage(Hash) - -BootValidation = _reflection.GeneratedProtocolMessageType( - "BootValidation", - (_message.Message,), - dict( - DESCRIPTOR=_BOOTVALIDATION, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.BootValidation) - ), -) -_sym_db.RegisterMessage(BootValidation) - -InitCommand = _reflection.GeneratedProtocolMessageType( - "InitCommand", - (_message.Message,), - dict( - DESCRIPTOR=_INITCOMMAND, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.InitCommand) - ), -) -_sym_db.RegisterMessage(InitCommand) - -ResetCommand = _reflection.GeneratedProtocolMessageType( - "ResetCommand", - (_message.Message,), - dict( - DESCRIPTOR=_RESETCOMMAND, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.ResetCommand) - ), -) -_sym_db.RegisterMessage(ResetCommand) - -Command = _reflection.GeneratedProtocolMessageType( - "Command", - (_message.Message,), - dict( - DESCRIPTOR=_COMMAND, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.Command) - ), -) -_sym_db.RegisterMessage(Command) - -SignedCommand = _reflection.GeneratedProtocolMessageType( - "SignedCommand", - (_message.Message,), - dict( - DESCRIPTOR=_SIGNEDCOMMAND, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.SignedCommand) - ), -) -_sym_db.RegisterMessage(SignedCommand) - -Packet = _reflection.GeneratedProtocolMessageType( - "Packet", - (_message.Message,), - dict( - DESCRIPTOR=_PACKET, - __module__="nordicsemi.dfu.dfu_cc_pb2" - # @@protoc_insertion_point(class_scope:dfu.Packet) - ), -) -_sym_db.RegisterMessage(Packet) - - -_INITCOMMAND.fields_by_name["sd_req"]._options = None -# @@protoc_insertion_point(module_scope) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport.py deleted file mode 100644 index 1621234f..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (c) 2016 - 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import logging -from abc import ABC, abstractmethod - -# Nordic Semiconductor imports - -logger = logging.getLogger(__name__) - - -# Custom logging level for logging all transport events, including all bytes -# being transported over the wire or over the air. -# Note that this logging level is more verbose than logging.DEBUG. -TRANSPORT_LOGGING_LEVEL = 5 - - -class DfuEvent: - PROGRESS_EVENT = 1 - - -class DfuTransport(ABC): - """ - This class as an abstract base class inherited from when implementing transports. - - The class is generic in nature, the underlying implementation may have missing semantic - than this class describes. But the intent is that the implementer shall follow the semantic as - best as she can. - """ - - OP_CODE = { - "CreateObject": 0x01, - "SetPRN": 0x02, - "CalcChecSum": 0x03, - "Execute": 0x04, - "ReadObject": 0x06, - "Response": 0x60, - } - - RES_CODE = { - "InvalidCode": 0x00, - "Success": 0x01, - "NotSupported": 0x02, - "InvalidParameter": 0x03, - "InsufficientResources": 0x04, - "InvalidObject": 0x05, - "InvalidSignature": 0x06, - "UnsupportedType": 0x07, - "OperationNotPermitted": 0x08, - "OperationFailed": 0x0A, - "ExtendedError": 0x0B, - } - - EXT_ERROR_CODE = [ - "No extended error code has been set. This error indicates an implementation problem.", - "Invalid error code. This error code should never be used outside of development.", - "The format of the command was incorrect. This error code is not used in the current implementation, because @ref NRF_DFU_RES_CODE_OP_CODE_NOT_SUPPORTED and @ref NRF_DFU_RES_CODE_INVALID_PARAMETER cover all possible format errors.", - "The command was successfully parsed, but it is not supported or unknown.", - "The init command is invalid. The init packet either has an invalid update type or it is missing required fields for the update type (for example, the init packet for a SoftDevice update is missing the SoftDevice size field).", - "The firmware version is too low. For an application, the version must be greater than or equal to the current application. For a bootloader, it must be greater than the current version. This requirement prevents downgrade attacks." - "", - "The hardware version of the device does not match the required hardware version for the update.", - "The array of supported SoftDevices for the update does not contain the FWID of the current SoftDevice.", - "The init packet does not contain a signature, but this bootloader requires all updates to have one.", - "The hash type that is specified by the init packet is not supported by the DFU bootloader.", - "The hash of the firmware image cannot be calculated.", - "The type of the signature is unknown or not supported by the DFU bootloader.", - "The hash of the received firmware image does not match the hash in the init packet.", - "The available space on the device is insufficient to hold the firmware.", - "The requested firmware to update was already present on the system.", - ] - - @abstractmethod - def __init__(self): - self.callbacks = {} - - @abstractmethod - def open(self): - """ - Open a port if appropriate for the transport. - :return: - """ - pass - - @abstractmethod - def close(self): - """ - Close a port if appropriate for the transport. - :return: - """ - pass - - @abstractmethod - def send_init_packet(self, init_packet): - """ - Send init_packet to device. - - This call will block until init_packet is sent and transfer of packet is complete. - - :param init_packet: Init packet as a str. - :return: - """ - pass - - @abstractmethod - def send_firmware(self, firmware): - """ - Start sending firmware to device. - - This call will block until transfer of firmware is complete. - - :param firmware: - :return: - """ - pass - - def register_events_callback(self, event_type, callback): - """ - Register a callback. - - :param DfuEvent callback: - :return: None - """ - if event_type not in self.callbacks: - self.callbacks[event_type] = [] - - self.callbacks[event_type].append(callback) - - def _send_event(self, event_type, **kwargs): - """ - Method for sending events to registered callbacks. - - If callbacks throws exceptions event propagation will stop and this method be part of the track trace. - - :param DfuEvent event_type: - :param kwargs: Arguments to callback function - :return: - """ - if event_type in list(self.callbacks.keys()): - for callback in self.callbacks[event_type]: - callback(**kwargs) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport_serial.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport_serial.py deleted file mode 100644 index 36c9e6bc..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/dfu_transport_serial.py +++ /dev/null @@ -1,589 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import binascii -import logging -import struct - -# Python imports -import time -from datetime import datetime, timedelta - -# Python 3rd party imports -from serial import Serial -from serial.serialutil import SerialException - -# from pc_ble_driver_py.exceptions import NordicSemiException -from ..exceptions import NordicSemiException -from ..lister.device_lister import DeviceLister - -# Nordic Semiconductor imports -from .dfu_transport import TRANSPORT_LOGGING_LEVEL, DfuEvent, DfuTransport -from .dfu_trigger import DFUTrigger - - -class ValidationException(NordicSemiException): - """ " - Exception used when validation failed - """ - - pass - - -logger = logging.getLogger(__name__) - - -class Slip: - SLIP_BYTE_END = 0o300 - SLIP_BYTE_ESC = 0o333 - SLIP_BYTE_ESC_END = 0o334 - SLIP_BYTE_ESC_ESC = 0o335 - - SLIP_STATE_DECODING = 1 - SLIP_STATE_ESC_RECEIVED = 2 - SLIP_STATE_CLEARING_INVALID_PACKET = 3 - - @staticmethod - def encode(data): - newData = [] - for elem in data: - if elem == Slip.SLIP_BYTE_END: - newData.append(Slip.SLIP_BYTE_ESC) - newData.append(Slip.SLIP_BYTE_ESC_END) - elif elem == Slip.SLIP_BYTE_ESC: - newData.append(Slip.SLIP_BYTE_ESC) - newData.append(Slip.SLIP_BYTE_ESC_ESC) - else: - newData.append(elem) - newData.append(Slip.SLIP_BYTE_END) - return newData - - @staticmethod - def decode_add_byte(c, decoded_data, current_state): - finished = False - if current_state == Slip.SLIP_STATE_DECODING: - if c == Slip.SLIP_BYTE_END: - finished = True - elif c == Slip.SLIP_BYTE_ESC: - current_state = Slip.SLIP_STATE_ESC_RECEIVED - else: - decoded_data.append(c) - elif current_state == Slip.SLIP_STATE_ESC_RECEIVED: - if c == Slip.SLIP_BYTE_ESC_END: - decoded_data.append(Slip.SLIP_BYTE_END) - current_state = Slip.SLIP_STATE_DECODING - elif c == Slip.SLIP_BYTE_ESC_ESC: - decoded_data.append(Slip.SLIP_BYTE_ESC) - current_state = Slip.SLIP_STATE_DECODING - else: - current_state = Slip.SLIP_STATE_CLEARING_INVALID_PACKET - elif current_state == Slip.SLIP_STATE_CLEARING_INVALID_PACKET: - if c == Slip.SLIP_BYTE_END: - current_state = Slip.SLIP_STATE_DECODING - decoded_data = [] - - return (finished, current_state, decoded_data) - - -class DFUAdapter: - def __init__(self, serial_port): - self.serial_port = serial_port - - def send_message(self, data): - packet = Slip.encode(data) - logger.log(TRANSPORT_LOGGING_LEVEL, "SLIP: --> " + str(data)) - try: - self.serial_port.write(packet) - except SerialException as e: - raise NordicSemiException( - "Writing to serial port failed: " + str(e) + ". " - "If MSD is enabled on the target device, try to disable it ref. " - "https://wiki.segger.com/index.php?title=J-Link-OB_SAM3U" - ) - - def get_message(self): - current_state = Slip.SLIP_STATE_DECODING - finished = False - decoded_data = [] - - while finished == False: - byte = self.serial_port.read(1) - if byte: - (byte) = struct.unpack("B", byte)[0] - (finished, current_state, decoded_data) = Slip.decode_add_byte( - byte, decoded_data, current_state - ) - else: - current_state = Slip.SLIP_STATE_CLEARING_INVALID_PACKET - return None - - logger.log(TRANSPORT_LOGGING_LEVEL, "SLIP: <-- " + str(decoded_data)) - - return decoded_data - - -class DfuTransportSerial(DfuTransport): - - DEFAULT_BAUD_RATE = 115200 - DEFAULT_FLOW_CONTROL = True - DEFAULT_TIMEOUT = 30.0 # Timeout time for board response - DEFAULT_SERIAL_PORT_TIMEOUT = 1.0 # Timeout time on serial port read - DEFAULT_PRN = 0 - DEFAULT_DO_PING = True - - OP_CODE = { - "CreateObject": 0x01, - "SetPRN": 0x02, - "CalcChecSum": 0x03, - "Execute": 0x04, - "ReadError": 0x05, - "ReadObject": 0x06, - "GetSerialMTU": 0x07, - "WriteObject": 0x08, - "Ping": 0x09, - "Response": 0x60, - } - - def __init__( - self, - com_port, - baud_rate=DEFAULT_BAUD_RATE, - flow_control=DEFAULT_FLOW_CONTROL, - timeout=DEFAULT_TIMEOUT, - prn=DEFAULT_PRN, - do_ping=DEFAULT_DO_PING, - ): - - super().__init__() - self.com_port = com_port - self.baud_rate = baud_rate - self.flow_control = 1 if flow_control else 0 - self.timeout = timeout - self.prn = prn - self.serial_port = None - self.dfu_adapter = None - self.ping_id = 0 - self.do_ping = do_ping - - self.mtu = 0 - - """:type: serial.Serial """ - - def open(self): - super().open() - try: - self.__ensure_bootloader() - self.serial_port = Serial( - port=self.com_port, - baudrate=self.baud_rate, - rtscts=self.flow_control, - timeout=self.DEFAULT_SERIAL_PORT_TIMEOUT, - ) - self.dfu_adapter = DFUAdapter(self.serial_port) - except OSError as e: - raise NordicSemiException( - "Serial port could not be opened on {0}" - ". Reason: {1}".format(self.com_port, e.strerror) - ) - - if self.do_ping: - ping_success = False - start = datetime.now() - while ( - datetime.now() - start < timedelta(seconds=self.timeout) - and ping_success == False - ): - if self.__ping() == True: - ping_success = True - - if ping_success == False: - raise NordicSemiException("No ping response after opening COM port") - - self.__set_prn() - self.__get_mtu() - - def close(self): - super().close() - self.serial_port.close() - - def send_init_packet(self, init_packet): - def try_to_recover(): - if response["offset"] == 0 or response["offset"] > len(init_packet): - # There is no init packet or present init packet is too long. - return False - - expected_crc = ( - binascii.crc32(init_packet[: response["offset"]]) & 0xFFFFFFFF - ) - - if expected_crc != response["crc"]: - # Present init packet is invalid. - return False - - if len(init_packet) > response["offset"]: - # Send missing part. - try: - self.__stream_data( - data=init_packet[response["offset"] :], - crc=expected_crc, - offset=response["offset"], - ) - except ValidationException: - return False - - self.__execute() - return True - - response = self.__select_command() - assert len(init_packet) <= response["max_size"], "Init command is too long" - - if try_to_recover(): - return - - try: - self.__create_command(len(init_packet)) - self.__stream_data(data=init_packet) - self.__execute() - except ValidationException: - raise NordicSemiException("Failed to send init packet") - - def send_firmware(self, firmware): - def try_to_recover(): - if response["offset"] == 0: - # Nothing to recover - return - - expected_crc = binascii.crc32(firmware[: response["offset"]]) & 0xFFFFFFFF - remainder = response["offset"] % response["max_size"] - - if expected_crc != response["crc"]: - # Invalid CRC. Remove corrupted data. - response["offset"] -= ( - remainder if remainder != 0 else response["max_size"] - ) - response["crc"] = ( - binascii.crc32(firmware[: response["offset"]]) & 0xFFFFFFFF - ) - return - - if (remainder != 0) and (response["offset"] != len(firmware)): - # Send rest of the page. - try: - to_send = firmware[ - response["offset"] : response["offset"] - + response["max_size"] - - remainder - ] - response["crc"] = self.__stream_data( - data=to_send, crc=response["crc"], offset=response["offset"] - ) - response["offset"] += len(to_send) - except ValidationException: - # Remove corrupted data. - response["offset"] -= remainder - response["crc"] = ( - binascii.crc32(firmware[: response["offset"]]) & 0xFFFFFFFF - ) - return - - self.__execute() - self._send_event( - event_type=DfuEvent.PROGRESS_EVENT, progress=response["offset"] - ) - - response = self.__select_data() - try_to_recover() - for i in range(response["offset"], len(firmware), response["max_size"]): - data = firmware[i : i + response["max_size"]] - try: - self.__create_data(len(data)) - response["crc"] = self.__stream_data( - data=data, crc=response["crc"], offset=i - ) - self.__execute() - except ValidationException: - raise NordicSemiException("Failed to send firmware") - - self._send_event(event_type=DfuEvent.PROGRESS_EVENT, progress=len(data)) - - def __ensure_bootloader(self): - lister = DeviceLister() - - device = None - start = datetime.now() - while not device and datetime.now() - start < timedelta(seconds=self.timeout): - time.sleep(0.5) - device = lister.get_device(com=self.com_port) - - if device: - device_serial_number = device.serial_number - - if not self.__is_device_in_bootloader_mode(device): - retry_count = 10 - wait_time_ms = 500 - - trigger = DFUTrigger() - try: - trigger.enter_bootloader_mode(device) - logger.info("Serial: DFU bootloader was triggered") - except NordicSemiException as err: - logger.error(err) - - for checks in range(retry_count): - logger.info( - "Serial: Waiting {} ms for device to enter bootloader {}/{} time".format( - 500, checks + 1, retry_count - ) - ) - - time.sleep(wait_time_ms / 1000.0) - - device = lister.get_device(serial_number=device_serial_number) - if self.__is_device_in_bootloader_mode(device): - self.com_port = device.get_first_available_com_port() - break - - trigger.clean() - if not self.__is_device_in_bootloader_mode(device): - logger.info( - "Serial: Device is either not in bootloader mode, or using an unsupported bootloader." - ) - - def __is_device_in_bootloader_mode(self, device): - if not device: - return False - - # Return true if nrf bootloader or Jlink interface detected. - return ( - ( - device.vendor_id.lower() == "1915" - and device.product_id.lower() == "521f" - ) # nRF52 SDFU USB - or ( - device.vendor_id.lower() == "1366" - and device.product_id.lower() == "0105" - ) # JLink CDC UART Port - or ( - device.vendor_id.lower() == "1366" - and device.product_id.lower() == "1015" - ) - ) # JLink CDC UART Port (MSD) - - def __set_prn(self): - logger.debug("Serial: Set Packet Receipt Notification {}".format(self.prn)) - self.dfu_adapter.send_message( - [DfuTransportSerial.OP_CODE["SetPRN"]] + list(struct.pack(" 0: - logger.debug( - "DFU trigger: Could not find trigger interface for device with serial number {}. " - "{}/{} devices with same VID/PID were missing a trigger interface.".format( - listed_device.serial_number, - triggerless_devices, - len(filtered_devices), - ) - ) - - if access_error: - raise NordicSemiException( - "LIBUSB_ERROR_ACCESS: Unable to connect to trigger interface." - ) - - def get_dfu_interface_num(self, libusb_device): - for setting in libusb_device.iterSettings(): - if ( - setting.getClass() == 255 - and setting.getSubClass() == 1 - and setting.getProtocol() == 1 - ): - return setting.getNumber() - - def no_trigger_exception(self, device): - return NordicSemiException( - "No trigger interface found for device with serial number: {}, Product ID: 0x{} and Vendor ID: 0x{}\n".format( - device.serial_number, device.product_id, device.vendor_id - ) - ) - - def enter_bootloader_mode(self, listed_device): - if self.context is None: - raise NordicSemiException( - "No Libusb1 context found, but is required to use DFU trigger. " - "This likely happens because the libusb1-0 binaries are missing from your system, " - "or Python is unable to locate them." - ) - libusb_device = self.select_device(listed_device) - if libusb_device is None: - raise self.no_trigger_exception(listed_device) - device_handle = libusb_device.open() - dfu_iface = self.get_dfu_interface_num(libusb_device) - - if dfu_iface is None: - raise self.no_trigger_exception(listed_device) - - with device_handle.claimInterface(dfu_iface): - arr = bytearray("0", "utf-8") - try: - device_handle.controlWrite( - ReqTypeOUT, DFU_DETACH_REQUEST, 0, dfu_iface, arr - ) - except Exception as err: - if "LIBUSB_ERROR_PIPE" in str(err): - return - raise NordicSemiException( - "A diconnection event from libusb is expected when the usb device restarts after triggering bootloder. " - "The event was not received. This can be an indication that the device was unable to leave application mode. " - "Serial number: {}, Product ID: 0x{}, Vendor ID: 0x{}\n\n".format( - listed_device.serial_number, - listed_device.product_id, - listed_device.vendor_id, - ) - ) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/init_packet_pb.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/init_packet_pb.py deleted file mode 100644 index 74eefaab..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/init_packet_pb.py +++ /dev/null @@ -1,193 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -from enum import Enum - -from . import dfu_cc_pb2 as pb - - -class SigningTypes(Enum): - ECDSA_P256_SHA256 = pb.ECDSA_P256_SHA256 - ED25519 = pb.ED25519 - - -class CommandTypes(Enum): - INIT = pb.INIT - - -class HashTypes(Enum): - NONE = pb.NO_HASH - CRC = pb.CRC - SHA128 = pb.SHA128 - SHA256 = pb.SHA256 - SHA512 = pb.SHA512 - - -class DFUType(Enum): - APPLICATION = pb.APPLICATION - SOFTDEVICE = pb.SOFTDEVICE - BOOTLOADER = pb.BOOTLOADER - SOFTDEVICE_BOOTLOADER = pb.SOFTDEVICE_BOOTLOADER - EXTERNAL_APPLICATION = pb.EXTERNAL_APPLICATION - - -class ValidationTypes(Enum): - NO_VALIDATION = pb.NO_VALIDATION - VALIDATE_GENERATED_CRC = pb.VALIDATE_GENERATED_CRC - VALIDATE_GENERATED_SHA256 = pb.VALIDATE_SHA256 - VALIDATE_ECDSA_P256_SHA256 = pb.VALIDATE_ECDSA_P256_SHA256 - - -class InitPacketPB: - def __init__( - self, - from_bytes=None, - hash_bytes=None, - hash_type=None, - boot_validation_type=[], - boot_validation_bytes=[], - dfu_type=None, - is_debug=False, - fw_version=0xFFFFFFFF, - hw_version=0xFFFFFFFF, - sd_size=0, - app_size=0, - bl_size=0, - sd_req=None, - ): - - if from_bytes is not None: - # construct from a protobuf string/buffer - self.packet = pb.Packet() - self.packet.ParseFromString(from_bytes) - - if self.packet.HasField("signed_command"): - self.init_command = self.packet.signed_command.command.init - else: - self.init_command = self.packet.command.init - - else: - # construct from input variables - if not sd_req: - sd_req = [0xFFFE] # Set to default value - self.packet = pb.Packet() - - boot_validation = [] - for i, x in enumerate(boot_validation_type): - boot_validation.append( - pb.BootValidation(type=x.value, bytes=boot_validation_bytes[i]) - ) - - # By default, set the packet's command to an unsigned command - # If a signature is set (via set_signature), this will get overwritten - # with an instance of SignedCommand instead. - self.packet.command.op_code = pb.INIT - - self.init_command = pb.InitCommand() - self.init_command.hash.hash_type = hash_type.value - self.init_command.type = dfu_type.value - self.init_command.hash.hash = hash_bytes - self.init_command.is_debug = is_debug - self.init_command.fw_version = fw_version - self.init_command.hw_version = hw_version - self.init_command.sd_req.extend(list(set(sd_req))) - self.init_command.sd_size = sd_size - self.init_command.bl_size = bl_size - self.init_command.app_size = app_size - - self.init_command.boot_validation.extend(boot_validation) - self.packet.command.init.CopyFrom(self.init_command) - - self._validate() - - def _validate(self): - if ( - self.init_command.type == pb.APPLICATION - or self.init_command.type == pb.EXTERNAL_APPLICATION - ) and self.init_command.app_size == 0: - raise RuntimeError( - "app_size is not set. It must be set when type is APPLICATION/EXTERNAL_APPLICATION" - ) - elif self.init_command.type == pb.SOFTDEVICE and self.init_command.sd_size == 0: - raise RuntimeError( - "sd_size is not set. It must be set when type is SOFTDEVICE" - ) - elif self.init_command.type == pb.BOOTLOADER and self.init_command.bl_size == 0: - raise RuntimeError( - "bl_size is not set. It must be set when type is BOOTLOADER" - ) - elif self.init_command.type == pb.SOFTDEVICE_BOOTLOADER and ( - self.init_command.sd_size == 0 or self.init_command.bl_size == 0 - ): - raise RuntimeError( - "Either sd_size or bl_size is not set. Both must be set when type " - "is SOFTDEVICE_BOOTLOADER" - ) - - if ( - self.init_command.fw_version < 0 - or self.init_command.fw_version > 0xFFFFFFFF - or self.init_command.hw_version < 0 - or self.init_command.hw_version > 0xFFFFFFFF - ): - raise RuntimeError( - "Invalid range of firmware argument. [0 - 0xffffffff] is valid range" - ) - - def _is_valid(self): - try: - self._validate() - except RuntimeError: - return False - - return self.signed_command.signature is not None - - def get_init_packet_pb_bytes(self): - return self.packet.SerializeToString() - - def get_init_command_bytes(self): - return self.init_command.SerializeToString() - - def set_signature(self, signature, signature_type): - new_packet = pb.Packet() - new_packet.signed_command.signature = signature - new_packet.signed_command.signature_type = signature_type.value - new_packet.signed_command.command.CopyFrom(self.packet.command) - - self.packet = new_packet - - def __str__(self): - return str(self.init_command) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/manifest.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/manifest.py deleted file mode 100644 index e1ec4c3e..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/manifest.py +++ /dev/null @@ -1,212 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -# Python libraries -import json -import os - -# Nordic libraries -from .model import FirmwareKeys, HexType - - -class ManifestGenerator: - def __init__(self, firmwares_data): - """ - The Manifest Generator constructor. Needs a data structure to generate a manifest from. - - :type dict firmwares_data: The firmwares data structure describing the Nordic DFU package - """ - self.firmwares_data = firmwares_data - self.manifest = None - - def generate_manifest(self): - self.manifest = Manifest() - - for key in self.firmwares_data: - firmware_dict = self.firmwares_data[key] - - if key == HexType.SD_BL: - _firmware = SoftdeviceBootloaderFirmware() - _firmware.info_read_only_metadata = FWMetaData() - _firmware.info_read_only_metadata.bl_size = firmware_dict[ - FirmwareKeys.BL_SIZE - ] - _firmware.info_read_only_metadata.sd_size = firmware_dict[ - FirmwareKeys.SD_SIZE - ] - else: - _firmware = Firmware() - - # Strip path, add only filename - _firmware.bin_file = os.path.basename( - firmware_dict[FirmwareKeys.BIN_FILENAME] - ) - _firmware.dat_file = os.path.basename( - firmware_dict[FirmwareKeys.DAT_FILENAME] - ) - - if key == HexType.APPLICATION or key == HexType.EXTERNAL_APPLICATION: - self.manifest.application = _firmware - elif key == HexType.BOOTLOADER: - self.manifest.bootloader = _firmware - elif key == HexType.SOFTDEVICE: - self.manifest.softdevice = _firmware - elif key == HexType.SD_BL: - self.manifest.softdevice_bootloader = _firmware - else: - raise NotImplementedError( - "Support for firmware type {0} not implemented yet.".format(key) - ) - - return self.to_json() - - def to_json(self): - def remove_none_entries(d): - if not isinstance(d, dict): - return d - - return dict( - (k, remove_none_entries(v)) for k, v in d.items() if v is not None - ) - - return json.dumps( - {"manifest": self.manifest}, - default=lambda o: remove_none_entries(o.__dict__), - sort_keys=True, - indent=4, - separators=(",", ": "), - ) - - -class FWMetaData: - def __init__( - self, - is_debug=None, - hw_version=None, - fw_version=None, - softdevice_req=None, - sd_size=None, - bl_size=None, - ): - """ - The FWMetaData data model. - - :param bool is_debug: debug mode on - :param int hw_version: hardware version - :param int fw_version: application or bootloader version - :param list softdevice_req: softdevice requirements - :param int sd_size SoftDevice size - :param int bl_size Bootloader size - :return:FWMetaData - """ - self.is_debug = is_debug - self.hw_version = hw_version - self.fw_version = fw_version - self.softdevice_req = softdevice_req - self.sd_size = sd_size - self.bl_size = bl_size - - -class Firmware: - def __init__(self, bin_file=None, dat_file=None, info_read_only_metadata=None): - """ - The firmware datamodel - - :param str bin_file: Firmware binary file - :param str dat_file: Firmware .dat file (init packet for Nordic DFU) - :param int info_read_only_metadata: The metadata about this firwmare image - :return: - """ - self.dat_file = dat_file - self.bin_file = bin_file - - if info_read_only_metadata: - self.info_read_only_metadata = FWMetaData(**info_read_only_metadata) - else: - self.info_read_only_metadata = None - - -class SoftdeviceBootloaderFirmware(Firmware): - def __init__(self, bin_file=None, dat_file=None, info_read_only_metadata=None): - """ - The SoftdeviceBootloaderFirmware data model - - :param str bin_file: Firmware binary file - :param str dat_file: Firmware .dat file (init packet for Nordic DFU) - :param int info_read_only_metadata: The metadata about this firwmare image - :return: SoftdeviceBootloaderFirmware - """ - super().__init__(bin_file, dat_file, info_read_only_metadata) - - -class Manifest: - def __init__( - self, - application=None, - bootloader=None, - softdevice=None, - softdevice_bootloader=None, - ): - """ - The Manifest data model. - - :param dict application: Application firmware in package - :param dict bootloader: Bootloader firmware in package - :param dict softdevice: Softdevice firmware in package - :param dict softdevice_bootloader: Combined softdevice and bootloader firmware in package - :return: Manifest - """ - self.softdevice_bootloader = ( - SoftdeviceBootloaderFirmware(**softdevice_bootloader) - if softdevice_bootloader - else None - ) - - self.softdevice = Firmware(**softdevice) if softdevice else None - self.bootloader = Firmware(**bootloader) if bootloader else None - self.application = Firmware(**application) if application else None - - @staticmethod - def from_json(data): - """ - Parses a manifest according to Nordic DFU package specification. - - :param str data: The manifest in string format - :return: Manifest - """ - kwargs = json.loads(data) - return Manifest(**kwargs["manifest"]) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/model.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/model.py deleted file mode 100644 index a811114b..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/model.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -from enum import Enum - - -class HexType(Enum): - SOFTDEVICE = 1 - BOOTLOADER = 2 - SD_BL = 3 - APPLICATION = 4 - EXTERNAL_APPLICATION = 5 - - -class FirmwareKeys(Enum): - ENCRYPT = 1 - FIRMWARE_FILENAME = 2 - BIN_FILENAME = 3 - DAT_FILENAME = 4 - INIT_PACKET_DATA = 5 - SD_SIZE = 6 - BL_SIZE = 7 - BOOT_VALIDATION_TYPE = 8 diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/nrfhex.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/nrfhex.py deleted file mode 100644 index 23bb8fb2..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/nrfhex.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright (c) 2016 - 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from enum import Enum -from struct import unpack - -import intelhex - - -class nRFArch(Enum): - NRF51 = 1 - NRF52 = 2 - NRF52840 = 3 - - -class nRFHex(intelhex.IntelHex): - """ - Converts and merges .hex and .bin files into one .bin file. - """ - - info_struct_address_base = 0x00003000 - info_struct_address_offset = 0x1000 - - info_struct_magic_number = 0x51B1E5DB - info_struct_magic_number_offset = 0x004 - - s1x0_mbr_end_address = 0x1000 - s132_mbr_end_address = 0x3000 - - def __init__(self, source, bootloader=None, arch=None): - """ - Constructor that requires a firmware file path. - Softdevices can take an optional bootloader file path as parameter. - - :param str source: The file path for the firmware - :param str bootloader: Optional file path to bootloader firmware - :return: None - """ - super().__init__() - self.arch = arch - self.file_format = "hex" - - if source.endswith(".bin"): - self.file_format = "bin" - - self.loadfile(source, self.file_format) - - if self.file_format == "hex": - self._removeuicr() - self._removembr() - - self.bootloaderhex = None - - if bootloader is not None: - self.bootloaderhex = nRFHex(bootloader) - - def _removeuicr(self): - uicr_start_address = 0x10000000 - self._buf = {k: v for k, v in self._buf.items() if k < uicr_start_address} - - def _removembr(self): - mbr_end_address = 0x1000 - self._buf = {k: v for k, v in self._buf.items() if k >= mbr_end_address} - - def address_has_magic_number(self, address): - try: - potential_magic_number = self.gets(address, 4) - potential_magic_number = unpack("I", potential_magic_number)[0] - return nRFHex.info_struct_magic_number == potential_magic_number - except Exception: - return False - - def get_softdevice_variant(self): - potential_magic_number_address = ( - nRFHex.info_struct_address_base + nRFHex.info_struct_magic_number_offset - ) - - if self.address_has_magic_number(potential_magic_number_address): - return "s1x0" - - for i in range(4): - potential_magic_number_address += nRFHex.info_struct_address_offset - - if self.address_has_magic_number(potential_magic_number_address): - return "s132" - - return "unknown" - - def get_mbr_end_address(self): - softdevice_variant = self.get_softdevice_variant() - - if softdevice_variant == "s132": - return nRFHex.s132_mbr_end_address - else: - return nRFHex.s1x0_mbr_end_address - - def minaddr(self): - min_address = super().minaddr() - - # Lower addresses are reserved for master boot record - if self.file_format != "bin": - min_address = max(self.get_mbr_end_address(), min_address) - - return min_address - - def size(self): - """ - Returns the size of the source. - :return: int - """ - min_address = self.minaddr() - max_address = self.maxaddr() - - size = max_address - min_address + 1 - - # Round up to nearest word - word_size = 4 - number_of_words = (size + (word_size - 1)) // word_size - size = number_of_words * word_size - - return size - - def bootloadersize(self): - """ - Returns the size of the bootloader. - :return: int - """ - if self.bootloaderhex is None: - return 0 - - return self.bootloaderhex.size() - - def tobinfile(self, fobj, start=None, end=None, pad=None, size=None): - """ - Writes a binary version of source and bootloader respectively to fobj which could be a - file object or a file path. - - :param str fobj: File path or object the function writes to - :return: None - """ - # If there is a bootloader this will make the recursion call use the samme file object. - if getattr(fobj, "write", None) is None: - fobj = open(fobj, "wb") - close_fd = True - else: - close_fd = False - - start_address = self.minaddr() - size = self.size() - super().tobinfile(fobj, start=start_address, size=size) - - if self.bootloaderhex is not None: - self.bootloaderhex.tobinfile(fobj) - - if close_fd: - fobj.close() diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/package.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/package.py deleted file mode 100644 index c3e9eae2..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/package.py +++ /dev/null @@ -1,764 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import binascii -import hashlib - -# Python standard library -import os -import shutil -import tempfile -from enum import Enum - -# 3rd party libraries -from zipfile import ZipFile - -from .crc16 import calc_crc16 -from .init_packet_pb import ( - CommandTypes, - DFUType, - HashTypes, - InitPacketPB, - SigningTypes, - ValidationTypes, -) -from .manifest import Manifest, ManifestGenerator -from .model import FirmwareKeys, HexType - -# Nordic libraries -from .nrfhex import nRFHex -from .signing import Signing - -# from csemi.zigbee.ota_file import OTA_file - - -HexTypeToInitPacketFwTypemap = { - HexType.APPLICATION: DFUType.APPLICATION, - HexType.BOOTLOADER: DFUType.BOOTLOADER, - HexType.SOFTDEVICE: DFUType.SOFTDEVICE, - HexType.SD_BL: DFUType.SOFTDEVICE_BOOTLOADER, - HexType.EXTERNAL_APPLICATION: DFUType.EXTERNAL_APPLICATION, -} - - -class PackageException(Exception): - pass - - -class PacketField(Enum): - DEBUG_MODE = 1 - HW_VERSION = 2 - FW_VERSION = 3 - REQUIRED_SOFTDEVICES_ARRAY = 4 - - -class Package: - """ - Packages and unpacks Nordic DFU packages. Nordic DFU packages are zip files that contains firmware and meta-information - necessary for utilities to perform a DFU on nRF5X devices. - - The internal data model used in Package is a dictionary. The dictionary is expressed like this in - json format: - - { - "manifest": { - "bootloader": { - "bin_file": "asdf.bin", - "dat_file": "asdf.dat", - "init_packet_data": { - "application_version": null, - "device_revision": null, - "device_type": 5, - "firmware_hash": "asdfasdkfjhasdkfjashfkjasfhaskjfhkjsdfhasjkhf", - "softdevice_req": [ - 17, - 18 - ] - } - } - } - - Attributes application, bootloader, softdevice, softdevice_bootloader shall not be put into the manifest if they are null - - """ - - DEFAULT_DEBUG_MODE = False - DEFAULT_HW_VERSION = 0xFFFFFFFF - DEFAULT_APP_VERSION = 0xFFFFFFFF - DEFAULT_BL_VERSION = 0xFFFFFFFF - DEFAULT_SD_REQ = [0xFFFE] - DEFAULT_SD_ID = [0xFFFE] - DEFAULT_DFU_VER = 0.5 - MANIFEST_FILENAME = "manifest.json" - DEFAULT_BOOT_VALIDATION_TYPE = ValidationTypes.VALIDATE_GENERATED_CRC.name - - def __init__( - self, - debug_mode=DEFAULT_DEBUG_MODE, - hw_version=DEFAULT_HW_VERSION, - app_version=DEFAULT_APP_VERSION, - bl_version=DEFAULT_BL_VERSION, - sd_req=DEFAULT_SD_REQ, - sd_id=DEFAULT_SD_ID, - app_fw=None, - bootloader_fw=None, - softdevice_fw=None, - sd_boot_validation=DEFAULT_BOOT_VALIDATION_TYPE, - app_boot_validation=DEFAULT_BOOT_VALIDATION_TYPE, - signer=None, - is_external=False, - zigbee_format=False, - manufacturer_id=0, - image_type=0, - comment="", - zigbee_ota_min_hw_version=None, - zigbee_ota_max_hw_version=None, - ): - - """ - Constructor that requires values used for generating a Nordic DFU package. - - :param int debug_mode: Debug init-packet field - :param int hw_version: Hardware version init-packet field - :param int app_version: App version init-packet field - :param int bl_version: Bootloader version init-packet field - :param list sd_req: Softdevice Requirement init-packet field - :param list sd_id: Softdevice Requirement init-packet field for the Application if softdevice_fw is set - :param str app_fw: Path to application firmware file - :param str bootloader_fw: Path to bootloader firmware file - :param str softdevice_fw: Path to softdevice firmware file - :param Signing signer: Instance of Signing() for Signing key file (PEM) - :param int zigbee_ota_min_hw_version: Minimal zigbee ota hardware version - :param int zigbee_ota_max_hw_version: Maximum zigbee ota hardware version - :return: None - """ - - init_packet_vars = {} - if debug_mode is not None: - init_packet_vars[PacketField.DEBUG_MODE] = debug_mode - - if hw_version is not None: - init_packet_vars[PacketField.HW_VERSION] = hw_version - - if sd_id is not None: - init_packet_vars[PacketField.REQUIRED_SOFTDEVICES_ARRAY] = sd_id - - if sd_boot_validation is not None: - sd_boot_validation_type = [ValidationTypes[sd_boot_validation]] - else: - sd_boot_validation_type = [ValidationTypes.VALIDATE_GENERATED_CRC] - - if app_boot_validation is not None: - app_boot_validation_type = [ValidationTypes[app_boot_validation]] - else: - app_boot_validation_type = [ValidationTypes.VALIDATE_GENERATED_CRC] - - self.firmwares_data = {} - - if app_fw: - firmware_type = ( - HexType.EXTERNAL_APPLICATION if is_external else HexType.APPLICATION - ) - self.__add_firmware_info( - firmware_type=firmware_type, - firmware_version=app_version, - filename=app_fw, - boot_validation_type=app_boot_validation_type, - init_packet_data=init_packet_vars, - ) - - # WARNING - # Do not move the setting of the `REQUIRED_SOFTDEVICES_ARRAY` - # field to be `sd_req` to before the `self__.add_firmware_info` call - # for HexType.EXTERNAL_APPLICATION. - # - # When doing a dfu update, the sd_req, specifies that whatever is being - # updated requires some version of the softdevice. In the case when - # somebody does a an update with both an application and a softdevice, - # both sd_req and sd_id are used at the same time. Moving assignment up - # will cause the versions accepted for the softdevice to also be - # accepted for the application, which can lead to invalid updates. If - # the value 0x00 is provided, it can also lead to the softdevice being - # deleted. - # - # Moving was tried https://github.com/NordicSemiconductor/pc-nrfutil/pull/349, but a - # stable solution is currently favored over one solving this particular - # issue. Any changes will have to be sufficiently tested to enusre a - # similar bug has not been introduced. - if sd_req is not None: - init_packet_vars[PacketField.REQUIRED_SOFTDEVICES_ARRAY] = sd_req - - if bootloader_fw: - self.__add_firmware_info( - firmware_type=HexType.BOOTLOADER, - firmware_version=bl_version, - filename=bootloader_fw, - boot_validation_type=[ValidationTypes.VALIDATE_GENERATED_CRC], - init_packet_data=init_packet_vars, - ) - - if softdevice_fw: - self.__add_firmware_info( - firmware_type=HexType.SOFTDEVICE, - firmware_version=0xFFFFFFFF, - filename=softdevice_fw, - boot_validation_type=sd_boot_validation_type, - init_packet_data=init_packet_vars, - ) - - assert not signer or isinstance(signer, Signing) - self.signer = signer - - self.work_dir = None - self.manifest = None - - if zigbee_format: - self.is_zigbee = True - self.image_type = image_type - self.manufacturer_id = manufacturer_id - self.comment = comment - self.zigbee_ota_min_hw_version = zigbee_ota_min_hw_version - self.zigbee_ota_max_hw_version = zigbee_ota_max_hw_version - else: - self.is_zigbee = False - self.image_type = None - self.manufacturer_id = None - self.comment = None - - def __del__(self): - """ - Destructor removes the temporary working directory - :return: - """ - if self.work_dir is not None: - shutil.rmtree(self.work_dir) - self.work_dir = None - - def rm_work_dir(self, preserve): - # Delete the temporary directory - if self.work_dir is not None: - if not preserve: - shutil.rmtree(self.work_dir) - - self.work_dir = None - - def parse_package(self, filename, preserve_work_dir=False): - self.work_dir = self.__create_temp_workspace() - - self.zip_file = filename - self.zip_dir = os.path.join(self.work_dir, "unpacked_zip") - self.manifest = Package.unpack_package(filename, self.zip_dir) - - self.rm_work_dir(preserve_work_dir) - - def image_str(self, index, hex_type, img): - type_strs = { - HexType.SD_BL: "sd_bl", - HexType.SOFTDEVICE: "softdevice", - HexType.BOOTLOADER: "bootloader", - HexType.APPLICATION: "application", - HexType.EXTERNAL_APPLICATION: "external application", - } - - # parse init packet - with open(os.path.join(self.zip_dir, img.dat_file), "rb") as imgf: - initp_bytes = imgf.read() - - initp = InitPacketPB(from_bytes=initp_bytes) - - sd_req = "" - for x in initp.init_command.sd_req: - sd_req = sd_req + "0x{0:02X}, ".format(x) - - if len(sd_req) != 0: - sd_req = sd_req[:-2] - - if initp.packet.HasField("signed_command"): - cmd = initp.packet.signed_command.command - signature_type = SigningTypes( - initp.packet.signed_command.signature_type - ).name - signature_hex = binascii.hexlify(initp.packet.signed_command.signature) - else: - cmd = initp.packet.command - signature_type = "UNSIGNED" - signature_hex = "N/A" - - boot_validation_type = [] - boot_validation_bytes = [] - for x in cmd.init.boot_validation: - boot_validation_type.append(ValidationTypes(x.type).name) - boot_validation_bytes.append(binascii.hexlify(x.bytes)) - - s = """| -|- Image #{0}: - |- Type: {1} - |- Image file: {2} - |- Init packet file: {3} - | - |- op_code: {4} - |- signature_type: {5} - |- signature (little-endian): {6} - | - |- fw_version: 0x{7:08X} ({7}) - |- hw_version 0x{8:08X} ({8}) - |- sd_req: {9} - |- type: {10} - |- sd_size: {11} - |- bl_size: {12} - |- app_size: {13} - | - |- hash_type: {14} - |- hash (little-endian): {15} - | - |- boot_validation_type: {16} - |- boot_validation_signature (little-endian): {17} - | - |- is_debug: {18} - -""".format( - index, - type_strs[hex_type], - img.bin_file, - img.dat_file, - CommandTypes(cmd.op_code).name, - signature_type, - signature_hex, - cmd.init.fw_version, - cmd.init.hw_version, - sd_req, - DFUType(cmd.init.type).name, - cmd.init.sd_size, - cmd.init.bl_size, - cmd.init.app_size, - HashTypes(cmd.init.hash.hash_type).name, - binascii.hexlify(cmd.init.hash.hash), - boot_validation_type, - boot_validation_bytes, - cmd.init.is_debug, - ) - - return s - - def __str__(self): - - imgs = "" - i = 0 - if self.manifest.softdevice_bootloader: - imgs = imgs + self.image_str( - i, HexType.SD_BL, self.manifest.softdevice_bootloader - ) - i = i + 1 - - if self.manifest.softdevice: - imgs = imgs + self.image_str( - i, HexType.SOFTDEVICE, self.manifest.softdevice - ) - i = i + 1 - - if self.manifest.bootloader: - imgs = imgs + self.image_str( - i, HexType.BOOTLOADER, self.manifest.bootloader - ) - i = i + 1 - - if self.manifest.application: - imgs = imgs + self.image_str( - i, HexType.APPLICATION, self.manifest.application - ) - i = i + 1 - - s = """ -DFU Package: <{0}>: -| -|- Image count: {1} -""".format( - self.zip_file, i - ) - - s = s + imgs - return s - - def generate_package(self, filename, preserve_work_dir=False): - """ - Generates a Nordic DFU package. The package is a zip file containing firmware(s) and metadata required - for Nordic DFU applications to perform DFU onn nRF5X devices. - - :param str filename: Filename for generated package. - :param bool preserve_work_dir: True to preserve the temporary working directory. - Useful for debugging of a package, and if the user wants to look at the generated package without having to - unzip it. - :return: None - """ - self.zip_file = filename - self.work_dir = self.__create_temp_workspace() - - sd_bin_created = False - if Package._is_bootloader_softdevice_combination(self.firmwares_data): - # Removing softdevice and bootloader data from dictionary and adding the combined later - softdevice_fw_data = self.firmwares_data.pop(HexType.SOFTDEVICE) - bootloader_fw_data = self.firmwares_data.pop(HexType.BOOTLOADER) - - softdevice_fw_name = softdevice_fw_data[FirmwareKeys.FIRMWARE_FILENAME] - bootloader_fw_name = bootloader_fw_data[FirmwareKeys.FIRMWARE_FILENAME] - - new_filename = "sd_bl.bin" - sd_bl_file_path = os.path.join(self.work_dir, new_filename) - - nrf_hex = nRFHex(softdevice_fw_name, bootloader_fw_name) - nrf_hex.tobinfile(sd_bl_file_path) - - softdevice_size = nrf_hex.size() - bootloader_size = nrf_hex.bootloadersize() - - boot_validation_type = [] - boot_validation_type.extend( - softdevice_fw_data[FirmwareKeys.BOOT_VALIDATION_TYPE] - ) - boot_validation_type.extend( - bootloader_fw_data[FirmwareKeys.BOOT_VALIDATION_TYPE] - ) - - self.__add_firmware_info( - firmware_type=HexType.SD_BL, - firmware_version=bootloader_fw_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.FW_VERSION - ], # use bootloader version in combination with SD - filename=sd_bl_file_path, - init_packet_data=softdevice_fw_data[FirmwareKeys.INIT_PACKET_DATA], - boot_validation_type=boot_validation_type, - sd_size=softdevice_size, - bl_size=bootloader_size, - ) - - # Need to generate SD only bin for boot validation signature - sd_bin = Package.normalize_firmware_to_bin( - self.work_dir, softdevice_fw_data[FirmwareKeys.FIRMWARE_FILENAME] - ) - sd_bin_path = os.path.join(self.work_dir, sd_bin) - sd_bin_created = True - - for key, firmware_data in self.firmwares_data.items(): - - # Normalize the firmware file and store it in the work directory - firmware_data[ - FirmwareKeys.BIN_FILENAME - ] = Package.normalize_firmware_to_bin( - self.work_dir, firmware_data[FirmwareKeys.FIRMWARE_FILENAME] - ) - - # Calculate the hash for the .bin file located in the work directory - bin_file_path = os.path.join( - self.work_dir, firmware_data[FirmwareKeys.BIN_FILENAME] - ) - firmware_hash = Package.calculate_sha256_hash(bin_file_path) - bin_length = int(Package.calculate_file_size(bin_file_path)) - - sd_size = 0 - bl_size = 0 - app_size = 0 - if key in [HexType.APPLICATION, HexType.EXTERNAL_APPLICATION]: - app_size = bin_length - elif key == HexType.SOFTDEVICE: - sd_size = bin_length - elif key == HexType.BOOTLOADER: - bl_size = bin_length - elif key == HexType.SD_BL: - bl_size = firmware_data[FirmwareKeys.BL_SIZE] - sd_size = firmware_data[FirmwareKeys.SD_SIZE] - - boot_validation_type_array = firmware_data[ - FirmwareKeys.BOOT_VALIDATION_TYPE - ] - boot_validation_bytes_array = [] - for x in boot_validation_type_array: - if x == ValidationTypes.VALIDATE_ECDSA_P256_SHA256: - if key == HexType.SD_BL: - boot_validation_bytes_array.append( - Package.sign_firmware(self.signer, sd_bin_path) - ) - else: - boot_validation_bytes_array.append( - Package.sign_firmware(self.signer, bin_file_path) - ) - else: - boot_validation_bytes_array.append(b"") - - init_packet = InitPacketPB( - from_bytes=None, - hash_bytes=firmware_hash, - hash_type=HashTypes.SHA256, - boot_validation_type=boot_validation_type_array, - boot_validation_bytes=boot_validation_bytes_array, - dfu_type=HexTypeToInitPacketFwTypemap[key], - is_debug=firmware_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.DEBUG_MODE - ], - fw_version=firmware_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.FW_VERSION - ], - hw_version=firmware_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.HW_VERSION - ], - sd_size=sd_size, - app_size=app_size, - bl_size=bl_size, - sd_req=firmware_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.REQUIRED_SOFTDEVICES_ARRAY - ], - ) - - if self.signer is not None: - signature = self.signer.sign(init_packet.get_init_command_bytes()) - init_packet.set_signature(signature, SigningTypes.ECDSA_P256_SHA256) - - # Store the .dat file in the work directory - init_packet_filename = firmware_data[FirmwareKeys.BIN_FILENAME].replace( - ".bin", ".dat" - ) - - with open( - os.path.join(self.work_dir, init_packet_filename), "wb" - ) as init_packet_file: - init_packet_file.write(init_packet.get_init_packet_pb_bytes()) - - firmware_data[FirmwareKeys.DAT_FILENAME] = init_packet_filename - - if self.is_zigbee: - firmware_version = firmware_data[FirmwareKeys.INIT_PACKET_DATA][ - PacketField.FW_VERSION - ] - file_name = firmware_data[FirmwareKeys.BIN_FILENAME] - - self.zigbee_ota_file = OTA_file( - firmware_version, - len(init_packet.get_init_packet_pb_bytes()), - binascii.crc32(init_packet.get_init_packet_pb_bytes()) & 0xFFFFFFFF, - init_packet.get_init_packet_pb_bytes(), - os.path.getsize(file_name), - self.calculate_crc(32, file_name) & 0xFFFFFFFF, - bytes(open(file_name, "rb").read()), - self.manufacturer_id, - self.image_type, - self.comment, - self.zigbee_ota_min_hw_version, - self.zigbee_ota_max_hw_version, - ) - - ota_file_handle = open(self.zigbee_ota_file.filename, "wb") - ota_file_handle.write(self.zigbee_ota_file.binary) - ota_file_handle.close() - - # Remove SD binary file created for boot validation - if sd_bin_created: - os.remove(sd_bin_path) - - # Store the manifest to manifest.json - manifest = self.create_manifest() - - with open( - os.path.join(self.work_dir, Package.MANIFEST_FILENAME), "w" - ) as manifest_file: - manifest_file.write(manifest) - - # Package the work_dir to a zip file - Package.create_zip_package(self.work_dir, filename) - - # Delete the temporary directory - self.rm_work_dir(preserve_work_dir) - - @staticmethod - def __create_temp_workspace(): - return tempfile.mkdtemp(prefix="nrf_dfu_pkg_") - - @staticmethod - def create_zip_package(work_dir, filename): - files = os.listdir(work_dir) - - with ZipFile(filename, "w") as package: - for _file in files: - file_path = os.path.join(work_dir, _file) - package.write(file_path, _file) - - @staticmethod - def calculate_file_size(firmware_filename): - b = os.path.getsize(firmware_filename) - return b - - @staticmethod - def calculate_sha256_hash(firmware_filename): - read_buffer = 4096 - - digest = hashlib.sha256() - - with open(firmware_filename, "rb") as firmware_file: - while True: - data = firmware_file.read(read_buffer) - - if data: - digest.update(data) - else: - break - - # return hash in little endian - sha256 = digest.digest() - return sha256[31::-1] - - @staticmethod - def calculate_crc(crc, firmware_filename): - """ - Calculates CRC16 has on provided firmware filename - - :type str firmware_filename: - """ - data_buffer = b"" - read_size = 4096 - - with open(firmware_filename, "rb") as firmware_file: - while True: - data = firmware_file.read(read_size) - - if data: - data_buffer += data - else: - break - if crc == 16: - return calc_crc16(data_buffer, 0xFFFF) - elif crc == 32: - return binascii.crc32(data_buffer) - else: - raise ValueError("Invalid CRC type") - - @staticmethod - def sign_firmware(signer, firmware_filename): - assert isinstance(signer, Signing) - data_buffer = b"" - with open(firmware_filename, "rb") as firmware_file: - data_buffer = firmware_file.read() - return signer.sign(data_buffer) - - def create_manifest(self): - manifest = ManifestGenerator(self.firmwares_data) - return manifest.generate_manifest() - - @staticmethod - def _is_bootloader_softdevice_combination(firmwares): - return (HexType.BOOTLOADER in firmwares) and (HexType.SOFTDEVICE in firmwares) - - def __add_firmware_info( - self, - firmware_type, - firmware_version, - filename, - init_packet_data, - boot_validation_type, - sd_size=None, - bl_size=None, - ): - self.firmwares_data[firmware_type] = { - FirmwareKeys.FIRMWARE_FILENAME: filename, - FirmwareKeys.INIT_PACKET_DATA: init_packet_data.copy(), - # Copying init packet to avoid using the same for all firmware - FirmwareKeys.BOOT_VALIDATION_TYPE: boot_validation_type, - } - - if firmware_type == HexType.SD_BL: - self.firmwares_data[firmware_type][FirmwareKeys.SD_SIZE] = sd_size - self.firmwares_data[firmware_type][FirmwareKeys.BL_SIZE] = bl_size - - if firmware_version is not None: - self.firmwares_data[firmware_type][FirmwareKeys.INIT_PACKET_DATA][ - PacketField.FW_VERSION - ] = firmware_version - - @staticmethod - def normalize_firmware_to_bin(work_dir, firmware_path): - firmware_filename = os.path.basename(firmware_path) - new_filename = firmware_filename.replace(".hex", ".bin") - new_filepath = os.path.join(work_dir, new_filename) - - if not os.path.exists(new_filepath): - temp = nRFHex(firmware_path) - temp.tobinfile(new_filepath) - - return new_filepath - - @staticmethod - def unpack_package(package_path, target_dir): - """ - Unpacks a Nordic DFU package. - - :param str package_path: Path to the package - :param str target_dir: Target directory to unpack the package to - :return: Manifest Manifest: Returns a manifest back to the user. The manifest is a parse datamodel - of the manifest found in the Nordic DFU package. - """ - - if not os.path.isfile(package_path): - raise PackageException("Package {0} not found.".format(package_path)) - - target_dir = os.path.abspath(target_dir) - target_base_path = os.path.dirname(target_dir) - - if not os.path.exists(target_base_path): - raise PackageException( - "Base path to target directory {0} does not exist.".format( - target_base_path - ) - ) - - if not os.path.isdir(target_base_path): - raise PackageException( - "Base path to target directory {0} is not a directory.".format( - target_base_path - ) - ) - - if os.path.exists(target_dir): - raise PackageException( - "Target directory {0} exists, not able to unpack to that directory.", - target_dir, - ) - - with ZipFile(package_path, "r") as pkg: - pkg.extractall(target_dir) - - with open(os.path.join(target_dir, Package.MANIFEST_FILENAME), "r") as f: - _json = f.read() - """:type :str """ - - return Manifest.from_json(_json) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/signing.py b/pynitrokey/trussed/bootloader/nrf52_upload/dfu/signing.py deleted file mode 100644 index 4e461502..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/dfu/signing.py +++ /dev/null @@ -1,256 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Copyright (c) 2015 Nordic Semiconductor. All Rights Reserved. -# -# The information contained herein is property of Nordic Semiconductor ASA. -# Terms and conditions of usage are described in detail in NORDIC -# SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. -# -# Licensees are granted free, non-transferable use of the information. NO -# WARRANTY of ANY KIND is provided. This heading must NOT be removed from -# the file. - -import datetime -import hashlib - -try: - from ecdsa import SigningKey - from ecdsa.curves import NIST256p - from ecdsa.keys import sigencode_string -except Exception: - print("Failed to import ecdsa, cannot do signing") - - -keys_default_pem = """-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIGvsrpXh8m/E9bj1dq/0o1aBPQVAFJQ6Pzusx685URE0oAoGCCqGSM49 -AwEHoUQDQgAEaHYrUu/oFKIXN457GH+8IOuv6OIPBRLqoHjaEKM0wIzJZ0lhfO/A -53hKGjKEjYT3VNTQ3Zq1YB3o5QSQMP/LRg== ------END EC PRIVATE KEY-----""" - - -class Signing: - """ - Class for singing of hex-files - """ - - def gen_key(self, filename): - """ - Generate a new Signing key using NIST P-256 curve - """ - self.sk = SigningKey.generate(curve=NIST256p) - - with open(filename, "wb") as sk_file: - sk_file.write(self.sk.to_pem()) - - def load_key(self, filename): - """ - Load signing key (from pem file) - """ - default_sk = SigningKey.from_pem(keys_default_pem) - - with open(filename, "r") as sk_file: - sk_pem = sk_file.read() - - self.sk = SigningKey.from_pem(sk_pem) - return default_sk.to_string() == self.sk.to_string() - - def sign(self, init_packet_data): - """ - Create signature for init package using P-256 curve and SHA-256 as hashing algorithm - Returns R and S keys combined in a 64 byte array - """ - # Add assertion of init_packet - if self.sk is None: - raise AssertionError("Can't save key. No key created/loaded") - - # Sign the init-packet - signature = self.sk.sign( - init_packet_data, hashfunc=hashlib.sha256, sigencode=sigencode_string - ) - return signature[31::-1] + signature[63:31:-1] - - def verify(self, init_packet, signature): - """ - Verify init packet - """ - # Add assertion of init_packet - if self.sk is None: - raise AssertionError("Can't save key. No key created/loaded") - - vk = self.sk.get_verifying_key() - - # Verify init packet - try: - vk.verify(signature, init_packet, hashfunc=hashlib.sha256) - except: - return False - - return True - - def get_vk(self, output_type, dbg) -> str: - """ - Get public key (as hex, code or pem) - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - if output_type is None: - raise ValueError("Invalid output type for public key.") - elif output_type == "hex": - return self.get_vk_hex() - elif output_type == "code": - return self.get_vk_code(dbg) - elif output_type == "pem": - return self.get_vk_pem() - else: - raise ValueError("Invalid argument. Can't get key") - - def get_sk(self, output_type, dbg) -> str: - """ - Get private key (as hex, code or pem) - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - if output_type is None: - raise ValueError("Invalid output type for private key.") - elif output_type == "hex": - return self.get_sk_hex() - elif output_type == "code": - raise ValueError("Private key cannot be shown as code") - elif output_type == "pem": - # Return pem as str to conform in type with the other cases. - return self.sk.to_pem().decode() - else: - raise ValueError("Invalid argument. Can't get key") - - def get_sk_hex(self): - """ - Get the verification key as hex - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - # Reverse the key for display. This emulates a memory - # dump of the key interpreted a 256bit little endian - # integer. - key = self.sk.to_string() - displayed_key = key[::-1].hex() - - return f"Private (signing) key sk:\n{displayed_key}" - - def get_vk_hex(self): - """ - Get the verification key as hex - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - # Reverse the two halves of key for display. This - # emulates a memory dump of the key interpreted as two - # 256bit little endian integers. - key = self.sk.get_verifying_key().to_string() - displayed_key = (key[:32][::-1] + key[32:][::-1]).hex() - - return f"Public (verification) key pk:\n{displayed_key}" - - def wrap_code(self, key_code, dbg): - - header = """ -/* This file was automatically generated by nrfutil on {0} */ - -#include "stdint.h" -#include "compiler_abstraction.h" -""".format( - datetime.datetime.now().strftime("%Y-%m-%d (YY-MM-DD) at %H:%M:%S") - ) - - dbg_header = """ -/* This file was generated with a throwaway private key, that is only intended for a debug version of the DFU project. - Please see https://github.com/NordicSemiconductor/pc-nrfutil/blob/master/README.md to generate a valid public key. */ - -#ifdef NRF_DFU_DEBUG_VERSION -""" - dbg_footer = """ -#else -#error "Debug public key not valid for production. Please see https://github.com/NordicSemiconductor/pc-nrfutil/blob/master/README.md to generate it" -#endif -""" - if dbg: - code = header + dbg_header + key_code + dbg_footer - else: - code = header + key_code - return code - - def get_vk_code(self, dbg): - """ - Get the verification key as code - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - to_two_digit_hex_with_0x = "0x{0:02x}".format - - key = self.sk.get_verifying_key().to_string() - vk_x_separated = ", ".join(map(to_two_digit_hex_with_0x, key[:32][::-1])) - vk_y_separated = ", ".join(map(to_two_digit_hex_with_0x, key[32:][::-1])) - - key_code = """ -/** @brief Public key used to verify DFU images */ -__ALIGN(4) const uint8_t pk[64] = -{{ - {0}, - {1} -}}; -""" - key_code = key_code.format(vk_x_separated, vk_y_separated) - vk_code = self.wrap_code(key_code, dbg) - - return vk_code - - def get_vk_pem(self) -> str: - """ - Get the verification key as PEM - """ - if self.sk is None: - raise AssertionError("Can't get key. No key created/loaded") - - vk = self.sk.get_verifying_key() - vk_pem = vk.to_pem() - - # Return pem as str to conform in type with the other cases. - return vk_pem.decode() diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/exceptions.py b/pynitrokey/trussed/bootloader/nrf52_upload/exceptions.py deleted file mode 100644 index b1c6b2b3..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/exceptions.py +++ /dev/null @@ -1,9 +0,0 @@ -class NordicSemiException(Exception): - """ - Exception used as based exception for other exceptions defined in this package. - """ - - def __init__(self, msg, error_code=None): - super(NordicSemiException, self).__init__(msg) - self.msg = msg - self.error_code = error_code diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/__init__.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/__init__.py deleted file mode 100644 index bc11c967..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright (c) 2016 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -"""Package marker file.""" diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/device_lister.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/device_lister.py deleted file mode 100644 index 8eea350a..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/device_lister.py +++ /dev/null @@ -1,88 +0,0 @@ -# -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import sys - -from .unix.unix_lister import UnixLister -from .windows.lister_win32 import Win32Lister - - -class DeviceLister: - def __init__(self): - if sys.platform == "win32": - self.lister_backend = Win32Lister() - elif "linux" in sys.platform: - self.lister_backend = UnixLister() - elif sys.platform == "darwin": - self.lister_backend = UnixLister() - else: - self.lister_backend = None - - def enumerate(self): - if self.lister_backend: - return self.lister_backend.enumerate() - return [] - - def get_device(self, get_all=False, **kwargs): - devices = self.enumerate() - matching_devices = [] - for dev in devices: - if ( - "vendor_id" in kwargs - and kwargs["vendor_id"].lower() != dev.vendor_id.lower() - ): - continue - if ( - "product_id" in kwargs - and kwargs["product_id"].lower() != dev.product_id.lower() - ): - continue - if "serial_number" in kwargs and ( - kwargs["serial_number"].lower().lstrip("0") - != dev.serial_number.lower().lstrip("0") - ): - continue - if "com" in kwargs and not dev.has_com_port(kwargs["com"]): - continue - - matching_devices.append(dev) - - if not get_all: - if len(matching_devices) == 0: - return - return matching_devices[0] - return matching_devices diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/enumerated_device.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/enumerated_device.py deleted file mode 100644 index 5fb7a895..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/enumerated_device.py +++ /dev/null @@ -1,71 +0,0 @@ -# -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import sys - - -class EnumeratedDevice: - def __init__(self, vendor_id, product_id, serial_number, com_ports): - self.vendor_id = vendor_id - self.product_id = product_id - self.serial_number = serial_number - self.com_ports = [] - for port in com_ports: - self.add_com_port(port) - - def add_com_port(self, port): - if sys.platform == "darwin": - # Ports are sometimes listed under /dev/cu on MacOS, - # but pyserial can only open /dev/tty* ports. - port = port.replace("/dev/cu.", "/dev/tty.") - self.com_ports.append(port) - - def has_com_port(self, checkPort): - for port in self.com_ports: - if port.lower() == checkPort.lower(): - return True - return False - - def get_first_available_com_port(self): - return self.com_ports[0] - - def __repr__(self): - return ( - "{{\nvendor_id: {}\nproduct_id: {}\nserial_number: {}\nCOM: {}\n}}".format( - self.vendor_id, self.product_id, self.serial_number, self.com_ports - ) - ) diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/lister_backend.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/lister_backend.py deleted file mode 100644 index 7d6be802..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/lister_backend.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from abc import ABC, abstractmethod - - -class AbstractLister(ABC): - @abstractmethod - def enumerate(self): - """ - Enumerate all usb devices - """ - pass diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/unix/__init__.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/unix/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/unix/unix_lister.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/unix/unix_lister.py deleted file mode 100644 index a4772d23..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/unix/unix_lister.py +++ /dev/null @@ -1,74 +0,0 @@ -# -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import sys - -from ..lister_backend import AbstractLister - -if "linux" in sys.platform or sys.platform == "darwin": - import serial.tools.list_ports - - from ..enumerated_device import EnumeratedDevice - - -def create_id_string(sno, PID, VID): - return "{}-{}-{}".format(sno, PID, VID) - - -class UnixLister(AbstractLister): - def enumerate(self): - device_identities = {} - available_ports = serial.tools.list_ports.comports() - - for port in available_ports: - if port.pid is None or port.vid is None or port.serial_number is None: - continue - - serial_number = port.serial_number - product_id = hex(port.pid).upper()[2:] - vendor_id = hex(port.vid).upper()[2:] - com_port = port.device - - id = create_id_string(serial_number, product_id, vendor_id) - if id in device_identities: - device_identities[id].add_com_port(com_port) - else: - device_identities[id] = EnumeratedDevice( - vendor_id, product_id, serial_number, [com_port] - ) - - return [device for device in list(device_identities.values())] diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/__init__.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/constants.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/constants.py deleted file mode 100644 index 7a77888b..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/constants.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -MIT License - -Copyright (c) 2016 gwangyi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import enum - -from .structures import DevicePropertyKey - - -# noinspection SpellCheckingInspection -class DiOpenDeviceInfo(enum.IntEnum): - """DIOD_xxx constants""" - - InheritClassDrvs = 2 - CancelRemove = 4 - - -# noinspection SpellCheckingInspection -class DiGetClassDevsFlags(enum.IntEnum): - """DIGCF_xxx constants""" - - Default = 0x00000001 - Present = (0x00000002,) - AllClasses = (0x00000004,) - Profile = (0x00000008,) - DeviceInterface = (0x00000010,) - - -# noinspection SpellCheckingInspection -class DevicePropertyKeys: - """DEVPKEY_xxx constants""" - - NAME = DevicePropertyKey( - "{b725f130-47ef-101a-a5f1-02608c9eebac}", 10, "DEVPKEY_NAME" - ) - Numa_Proximity_Domain = DevicePropertyKey( - "{540b947e-8b40-45bc-a8a2-6a0b894cbda2}", 1, "DEVPKEY_Numa_Proximity_Domain" - ) - - # noinspection SpellCheckingInspection - class Device: - """DEVPKEY_Device_xxx constants""" - - ContainerId = DevicePropertyKey( - "{8c7ed206-3f8a-4827-b3ab-ae9e1faefc6c}", 2, "DEVPKEY_Device_ContainerId" - ) - DeviceAddress = DevicePropertyKey( - "{a45c254e-df1c-4efd-8020-67d146a850e0}", 30, "DEVPKEY_Device_Address" - ) - - -# noinspection SpellCheckingInspection -DIGCF_DEFAULT = DiGetClassDevsFlags.Default -# noinspection SpellCheckingInspection -DIGCF_PRESENT = DiGetClassDevsFlags.Present -# noinspection SpellCheckingInspection -DIGCF_ALLCLASSES = DiGetClassDevsFlags.AllClasses -# noinspection SpellCheckingInspection -DIGCF_PROFILE = DiGetClassDevsFlags.Profile -# noinspection SpellCheckingInspection -DIGCF_DEVICEINTERFACE = DiGetClassDevsFlags.DeviceInterface - -# noinspection SpellCheckingInspection -DIOD_INHERIT_CLASSDRVS = DiOpenDeviceInfo.InheritClassDrvs -# noinspection SpellCheckingInspection -DIOD_CANCEL_REMOVE = DiOpenDeviceInfo.CancelRemove - -# noinspection SpellCheckingInspection -DEVPKEY = DevicePropertyKeys -DEVPKEY_Device_ContainerId = DevicePropertyKeys.Device.ContainerId diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/lister_win32.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/lister_win32.py deleted file mode 100644 index 1a9523f0..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/lister_win32.py +++ /dev/null @@ -1,318 +0,0 @@ -# -# Copyright (c) 2019 Nordic Semiconductor ASA -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of Nordic Semiconductor ASA nor the names of other -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# 4. This software must only be used in or with a processor manufactured by Nordic -# Semiconductor ASA, or in or with a processor manufactured by a third party that -# is used in combination with a processor manufactured by Nordic Semiconductor. -# -# 5. Any software provided in binary or object form under this license must not be -# reverse engineered, decompiled, modified and/or disassembled. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - -import sys - -from ..enumerated_device import EnumeratedDevice -from ..lister_backend import AbstractLister - -if sys.platform == "win32": - import ctypes - import winreg - - from .constants import DEVPKEY, DIGCF_DEVICEINTERFACE, DIGCF_PRESENT - from .structures import _GUID, GUID, DeviceInfoData, ValidHandle, ctypesInternalGUID - - setup_api = ctypes.windll.setupapi - - SetupDiGetClassDevs = setup_api.SetupDiGetClassDevsW - SetupDiGetClassDevs.argtypes = [ - ctypes.POINTER(_GUID), - ctypes.c_wchar_p, - ctypes.c_void_p, - ctypes.c_uint32, - ] - SetupDiGetClassDevs.restype = ctypes.c_void_p - SetupDiGetClassDevs.errcheck = ValidHandle - - SetupDiEnumDeviceInfo = setup_api.SetupDiEnumDeviceInfo - SetupDiEnumDeviceInfo.argtypes = [ - ctypes.c_void_p, - ctypes.c_uint32, - ctypes.POINTER(DeviceInfoData), - ] - SetupDiEnumDeviceInfo.restype = ctypes.c_bool - - SetupDiGetDeviceInstanceId = setup_api.SetupDiGetDeviceInstanceIdW - SetupDiGetDeviceInstanceId.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(DeviceInfoData), - ctypes.c_wchar_p, - ctypes.c_uint32, - ctypes.POINTER(ctypes.c_uint32), - ] - SetupDiGetDeviceInstanceId.restype = ctypes.c_bool - - SetupDiGetDeviceProperty = setup_api.SetupDiGetDevicePropertyW - SetupDiGetDeviceProperty.argtypes = [ - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_void_p, - ctypes.c_uint, - ] - SetupDiGetDeviceProperty.restype = ctypes.c_bool - -# constants -DICS_FLAG_GLOBAL = 1 -DIREG_DEV = 1 -INVALID_HANDLE_VALUE = -1 -MAX_BUFSIZE = 1000 - - -def get_serial_serial_no(vendor_id, product_id, h_dev_info, device_info_data): - prop_type = ctypes.c_ulong() - required_size = ctypes.c_ulong() - - instance_id_buffer = ctypes.create_unicode_buffer(MAX_BUFSIZE) - res = SetupDiGetDeviceProperty( - h_dev_info, - ctypes.byref(device_info_data), - ctypes.byref(DEVPKEY.Device.ContainerId), - ctypes.byref(prop_type), - instance_id_buffer, - MAX_BUFSIZE, - ctypes.byref(required_size), - 0, - ) - - wanted_GUID = GUID(ctypesInternalGUID(instance_id_buffer)) - - device_address = ctypes.c_int32() - res = setup_api.SetupDiGetDevicePropertyW( - h_dev_info, - ctypes.byref(device_info_data), - ctypes.byref(DEVPKEY.Device.DeviceAddress), - ctypes.byref(prop_type), - ctypes.byref(device_address), - ctypes.sizeof(ctypes.c_int32), - ctypes.byref(required_size), - 0, - ) - - hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}".format( - vendor_id, product_id - ) - try: - vendor_product_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - except EnvironmentError as err: - return - - serial_numbers_count = winreg.QueryInfoKey(vendor_product_hkey)[0] - - for serial_number_idx in range(serial_numbers_count): - try: - serial_number = winreg.EnumKey(vendor_product_hkey, serial_number_idx) - except EnvironmentError as err: - continue - - hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}".format( - vendor_id, product_id, serial_number - ) - - try: - device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - except EnvironmentError as err: - continue - - try: - queried_container_id = winreg.QueryValueEx(device_hkey, "ContainerID")[0] - except EnvironmentError as err: - winreg.CloseKey(device_hkey) - continue - - try: - queried_address = winreg.QueryValueEx(device_hkey, "Address")[0] - except EnvironmentError as err: - winreg.CloseKey(device_hkey) - continue - - winreg.CloseKey(device_hkey) - - if ( - queried_container_id.lower() == str(wanted_GUID).lower() - and queried_address == device_address.value - ): - winreg.CloseKey(vendor_product_hkey) - return serial_number - - winreg.CloseKey(vendor_product_hkey) - - -def com_port_is_open(port): - hkey_path = "HARDWARE\\DEVICEMAP\\SERIALCOMM" - try: - device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - except EnvironmentError as err: - # Unable to check enumerated serialports. Assume open. - return True - next_key = 0 - while True: - try: - value = winreg.EnumValue(device_hkey, next_key)[1] - next_key += 1 - if port == value: - winreg.CloseKey(device_hkey) - return True - except WindowsError: - break - winreg.CloseKey(device_hkey) - return False - - -def list_all_com_ports(vendor_id, product_id, serial_number): - ports = [] - - hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}".format( - vendor_id, product_id, serial_number - ) - - try: - device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - except EnvironmentError as err: - return ports - - try: - parent_id = winreg.QueryValueEx(device_hkey, "ParentIdPrefix")[0] - except EnvironmentError as err: - winreg.CloseKey(device_hkey) - return ports - - winreg.CloseKey(device_hkey) - - hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}\\Device Parameters".format( - vendor_id, product_id, serial_number - ) - try: - device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - try: - COM_port = winreg.QueryValueEx(device_hkey, "PortName")[0] - ports.append(COM_port) - except EnvironmentError as err: - # No COM port for root device. - pass - winreg.CloseKey(device_hkey) - except EnvironmentError as err: - # Root device has no device parameters - pass - - iface_id = 0 - while True: - hkey_path = ( - "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{vid_val}&PID_{pid_val}&" - "MI_{mi_val}\\{parent_val}&{parent_iface}\\Device Parameters".format( - vid_val=vendor_id, - pid_val=product_id, - mi_val=str(iface_id).zfill(2), - parent_val=parent_id, - parent_iface=str(iface_id).zfill(4), - ) - ) - iface_id += 1 - try: - device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path) - except EnvironmentError as err: - break - - try: - port_name = winreg.QueryValueEx(device_hkey, "PortName")[0] - except EnvironmentError as err: - winreg.CloseKey(device_hkey) - continue - - winreg.CloseKey(device_hkey) - if com_port_is_open(port_name): - ports.append(port_name) - - return ports - - -class Win32Lister(AbstractLister): - def __init__(self): - self.GUID_DEVINTERFACE_USB_DEVICE = GUID( - "{A5DCBF10-6530-11D2-901F-00C04FB951ED}" - ) - - def enumerate(self): - enumerated_devices = [] - h_dev_info = SetupDiGetClassDevs( - ctypes.byref(self.GUID_DEVINTERFACE_USB_DEVICE._guid), - None, - 0, - DIGCF_PRESENT | DIGCF_DEVICEINTERFACE, - ) - dev_info_data = DeviceInfoData() - if h_dev_info == -1: - return enumerated_devices - - next_enum = 0 - while SetupDiEnumDeviceInfo(h_dev_info, next_enum, ctypes.byref(dev_info_data)): - next_enum += 1 - - sz_buffer = ctypes.create_unicode_buffer(MAX_BUFSIZE) - dw_size = ctypes.c_ulong() - res = SetupDiGetDeviceInstanceId( - h_dev_info, - ctypes.byref(dev_info_data), - sz_buffer, - MAX_BUFSIZE, - ctypes.byref(dw_size), - ) - if not res: - # failed to fetch pid vid - continue - vendor_id = sz_buffer.value[8:12] - product_id = sz_buffer.value[17:21] - - serial_number = get_serial_serial_no( - vendor_id, product_id, h_dev_info, dev_info_data - ) - if not serial_number: - continue - - COM_ports = list_all_com_ports(vendor_id, product_id, serial_number) - - if len(COM_ports) > 0: - device = EnumeratedDevice( - vendor_id, product_id, serial_number, COM_ports - ) - enumerated_devices.append(device) - - return enumerated_devices diff --git a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/structures.py b/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/structures.py deleted file mode 100644 index 5a32cc3f..00000000 --- a/pynitrokey/trussed/bootloader/nrf52_upload/lister/windows/structures.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -MIT License - -Copyright (c) 2016 gwangyi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import ctypes - -_ole32 = ctypes.WinDLL("ole32") - - -class _GUID(ctypes.Structure): - _fields_ = [ - ("Data1", ctypes.c_uint32), - ("Data2", ctypes.c_uint16), - ("Data3", ctypes.c_uint16), - ("Data4", ctypes.c_ubyte * 8), - ] - - def __init__(self, guid="{00000000-0000-0000-0000-000000000000}"): - super().__init__() - if isinstance(guid, str): - ret = _ole32.CLSIDFromString( - ctypes.create_unicode_buffer(guid), ctypes.byref(self) - ) - if ret < 0: - err_no = ctypes.GetLastError() - raise WindowsError(err_no, ctypes.FormatError(err_no), guid) - else: - ctypes.memmove(ctypes.byref(self), bytes(guid), ctypes.sizeof(self)) - - def __str__(self): - s = ctypes.c_wchar_p() - ret = _ole32.StringFromCLSID(ctypes.byref(self), ctypes.byref(s)) - if ret < 0: - err_no = ctypes.GetLastError() - raise WindowsError(err_no, ctypes.FormatError(err_no)) - ret = str(s.value) - _ole32.CoTaskMemFree(s) - return ret - - def __repr__(self): - return "".format(str(self)) - - -assert ctypes.sizeof(_GUID) == 16 - - -class GUID: - def __init__(self, guid="{00000000-0000-0000-0000-000000000000}"): - self._guid = _GUID(guid) - - def __bytes__(self): - return bytes(self._guid) - - def __str__(self): - return str(self._guid) - - def __repr__(self): - return repr(self._guid) - - -class DevicePropertyKey(ctypes.Structure): - # noinspection SpellCheckingInspection - _fields_ = [("fmtid", _GUID), ("pid", ctypes.c_ulong)] - - def __init__(self, guid, pid, name=None): - super().__init__() - self.fmtid.__init__(guid) - self.pid = pid - self.name = name - self.__doc__ = str(self) - - def __repr__(self): - return "".format(str(self)) - - def __str__(self): - if not hasattr(self, "name") or self.name is None: - # noinspection SpellCheckingInspection - return "{} {}".format(self.fmtid, self.pid) - else: - # noinspection SpellCheckingInspection - return "{}, {} {}".format(self.name, self.fmtid, self.pid) - - def __eq__(self, key): - if not isinstance(key, DevicePropertyKey): - return False - return bytes(self) == bytes(key) - - -class DeviceInfoData(ctypes.Structure): - _fields_ = [ - ("cbSize", ctypes.c_ulong), - ("ClassGuid", _GUID), - ("DevInst", ctypes.c_ulong), - ("Reserved", ctypes.c_void_p), - ] - - def __init__(self): - super().__init__() - self.cbSize = ctypes.sizeof(self) - - def __str__(self): - return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst) - - -class ctypesInternalGUID: - def __init__(self, bytes): - self._internal = bytes - - def __bytes__(self): - return bytes(self._internal) - - -def ValidHandle(value, func, arguments): - if value == 0: - raise ctypes.WinError() - return value - - -DeviceInfoData.size = DeviceInfoData.cbSize -DeviceInfoData.dev_inst = DeviceInfoData.DevInst -DeviceInfoData.class_guid = DeviceInfoData.ClassGuid -# noinspection SpellCheckingInspection -SP_DEVINFO_DATA = DeviceInfoData -# noinspection SpellCheckingInspection -DEVPROPKEY = DevicePropertyKey diff --git a/pynitrokey/trussed/device.py b/pynitrokey/trussed/device.py deleted file mode 100644 index 33eced8b..00000000 --- a/pynitrokey/trussed/device.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import enum -import logging -import platform -import sys -from abc import abstractmethod -from enum import Enum -from typing import Optional, Sequence, TypeVar - -from fido2.hid import CtapHidDevice, open_device - -from pynitrokey.fido2 import device_path_to_str - -from .base import NitrokeyTrussedBase -from .utils import Fido2Certs, Uuid - -T = TypeVar("T", bound="NitrokeyTrussedDevice") - -logger = logging.getLogger(__name__) - - -@enum.unique -class App(Enum): - """Vendor-specific CTAPHID commands for Trussed apps.""" - - SECRETS = 0x70 - PROVISIONER = 0x71 - ADMIN = 0x72 - - -class NitrokeyTrussedDevice(NitrokeyTrussedBase): - def __init__( - self, device: CtapHidDevice, fido2_certs: Sequence[Fido2Certs] - ) -> None: - self.validate_vid_pid(device.descriptor.vid, device.descriptor.pid) - - self.device = device - self.fido2_certs = fido2_certs - self._path = device_path_to_str(device.descriptor.path) - self.logger = logger.getChild(self._path) - - from .admin_app import AdminApp - - self.admin = AdminApp(self) - self.admin.status() - - @property - def path(self) -> str: - return self._path - - def close(self) -> None: - self.device.close() - - def reboot(self) -> bool: - from .admin_app import BootMode - - return self.admin.reboot(BootMode.FIRMWARE) - - def uuid(self) -> Optional[Uuid]: - return self.admin.uuid() - - def wink(self) -> None: - self.device.wink() - - def _call( - self, - command: int, - command_name: str, - response_len: Optional[int] = None, - data: bytes = b"", - ) -> bytes: - response = self.device.call(command, data=data) - if response_len is not None and response_len != len(response): - raise ValueError( - f"The response for the CTAPHID {command_name} command has an unexpected length " - f"(expected: {response_len}, actual: {len(response)})" - ) - return response - - def _call_app( - self, - app: App, - response_len: Optional[int] = None, - data: bytes = b"", - ) -> bytes: - return self._call(app.value, app.name, response_len, data) - - @classmethod - @abstractmethod - def from_device(cls: type[T], device: CtapHidDevice) -> T: - ... - - @classmethod - def open(cls: type[T], path: str) -> Optional[T]: - try: - if platform.system() == "Windows": - device = open_device(bytes(path, "utf-8")) - else: - device = open_device(path) - except Exception: - logger.warn(f"No CTAPHID device at path {path}", exc_info=sys.exc_info()) - return None - try: - return cls.from_device(device) - except ValueError: - logger.warn(f"No Nitrokey device at path {path}", exc_info=sys.exc_info()) - return None - - @classmethod - def list(cls: type[T]) -> list[T]: - devices = [] - for device in CtapHidDevice.list_devices(): - try: - devices.append(cls.from_device(device)) - except ValueError: - # not the correct device type, skip - pass - return devices diff --git a/pynitrokey/trussed/exceptions.py b/pynitrokey/trussed/exceptions.py deleted file mode 100644 index b5e05925..00000000 --- a/pynitrokey/trussed/exceptions.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2022-2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - - -class NitrokeyTrussedException(Exception): - pass - - -class TimeoutException(NitrokeyTrussedException): - def __init__(self) -> None: - super().__init__("The user confirmation request timed out") diff --git a/pynitrokey/trussed/provisioner_app.py b/pynitrokey/trussed/provisioner_app.py deleted file mode 100644 index 62feaca8..00000000 --- a/pynitrokey/trussed/provisioner_app.py +++ /dev/null @@ -1,57 +0,0 @@ -import enum -from enum import Enum -from typing import Optional - -from pynitrokey.trussed.device import App, NitrokeyTrussedDevice - - -@enum.unique -class Buffer(Enum): - FILENAME = bytes([0xE1, 0x01]) - FILE = bytes([0xE1, 0x02]) - - -@enum.unique -class ProvisionerCommand(Enum): - SELECT = 0xA4 - WRITE_BINARY = 0xD0 - WRITE_FILE = 0xBF - GET_UUID = 0x62 - - -class ProvisionerApp: - def __init__(self, device: NitrokeyTrussedDevice) -> None: - self.device = device - - try: - self._call(ProvisionerCommand.GET_UUID) - except Exception: - raise RuntimeError("Provisioner application not available") - - def _call( - self, - command: ProvisionerCommand, - response_len: Optional[int] = None, - data: bytes = b"", - ) -> bytes: - return self.device._call_app( - App.PROVISIONER, - response_len=response_len, - data=command.value.to_bytes(1, "big") + data, - ) - - def _select(self, buffer: Buffer) -> None: - self._call(ProvisionerCommand.SELECT, data=buffer.value, response_len=0) - - def _write_binary(self, data: bytes) -> None: - self._call(ProvisionerCommand.WRITE_BINARY, data=data, response_len=0) - - def _write_file(self) -> None: - self._call(ProvisionerCommand.WRITE_FILE, response_len=0) - - def write_file(self, filename: bytes, data: bytes) -> None: - self._select(Buffer.FILENAME) - self._write_binary(filename) - self._select(Buffer.FILE) - self._write_binary(data) - self._write_file() diff --git a/pynitrokey/trussed/utils.py b/pynitrokey/trussed/utils.py deleted file mode 100644 index a4b0cc69..00000000 --- a/pynitrokey/trussed/utils.py +++ /dev/null @@ -1,247 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2024 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import dataclasses -from dataclasses import dataclass, field -from functools import total_ordering -from typing import Optional, Sequence - -from spsdk.sbfile.misc import BcdVersion3 - - -@dataclass(order=True, frozen=True) -class Uuid: - """UUID of a Nitrokey Trussed device.""" - - value: int - - def __str__(self) -> str: - return f"{self.value:032X}" - - def __int__(self) -> int: - return self.value - - -@dataclass(eq=False, frozen=True) -@total_ordering -class Version: - """ - The version of a Nitrokey Trussed device, following Semantic Versioning - 2.0.0. - - Some sources for version information, namely the version returned by older - devices and the firmware binaries, do not contain the pre-release - component. These instances are marked with *complete=False*. This flag - affects comparison: The pre-release version is only taken into account if - both version instances are complete. - - >>> Version(1, 0, 0) - Version(major=1, minor=0, patch=0, pre=None, build=None) - >>> Version.from_str("1.0.0") - Version(major=1, minor=0, patch=0, pre=None, build=None) - >>> Version.from_v_str("v1.0.0") - Version(major=1, minor=0, patch=0, pre=None, build=None) - >>> Version(1, 0, 0, "rc.1") - Version(major=1, minor=0, patch=0, pre='rc.1', build=None) - >>> Version.from_str("1.0.0-rc.1") - Version(major=1, minor=0, patch=0, pre='rc.1', build=None) - >>> Version.from_v_str("v1.0.0-rc.1") - Version(major=1, minor=0, patch=0, pre='rc.1', build=None) - >>> Version.from_v_str("v1.0.0-rc.1+git") - Version(major=1, minor=0, patch=0, pre='rc.1', build='git') - """ - - major: int - minor: int - patch: int - pre: Optional[str] = None - build: Optional[str] = None - complete: bool = field(default=False, repr=False) - - def __str__(self) -> str: - """ - >>> str(Version(major=1, minor=0, patch=0)) - 'v1.0.0' - >>> str(Version(major=1, minor=0, patch=0, pre="rc.1")) - 'v1.0.0-rc.1' - >>> str(Version(major=1, minor=0, patch=0, pre="rc.1", build="git")) - 'v1.0.0-rc.1+git' - """ - - version = f"v{self.major}.{self.minor}.{self.patch}" - if self.pre: - version += f"-{self.pre}" - if self.build: - version += f"+{self.build}" - return version - - def __eq__(self, other: object) -> bool: - """ - >>> Version(1, 0, 0) == Version(1, 0, 0) - True - >>> Version(1, 0, 0) == Version(1, 0, 1) - False - >>> Version.from_str("1.0.0-rc.1") == Version.from_str("1.0.0-rc.1") - True - >>> Version.from_str("1.0.0") == Version.from_str("1.0.0-rc.1") - False - >>> Version.from_str("1.0.0") == Version.from_str("1.0.0+git") - True - >>> Version(1, 0, 0, complete=False) == Version.from_str("1.0.0-rc.1") - True - >>> Version(1, 0, 0, complete=False) == Version.from_str("1.0.1") - False - """ - if not isinstance(other, Version): - return NotImplemented - lhs = (self.major, self.minor, self.patch) - rhs = (other.major, other.minor, other.patch) - - if lhs != rhs: - return False - if self.complete and other.complete: - return self.pre == other.pre - return True - - def __lt__(self, other: object) -> bool: - """ - >>> def cmp(a, b): - ... return Version.from_str(a) < Version.from_str(b) - >>> cmp("1.0.0", "1.0.0") - False - >>> cmp("1.0.0", "1.0.1") - True - >>> cmp("1.1.0", "2.0.0") - True - >>> cmp("1.1.0", "1.0.3") - False - >>> cmp("1.0.0-rc.1", "1.0.0-rc.1") - False - >>> cmp("1.0.0-rc.1", "1.0.0") - True - >>> cmp("1.0.0", "1.0.0-rc.1") - False - >>> cmp("1.0.0-rc.1", "1.0.0-rc.2") - True - >>> cmp("1.0.0-rc.2", "1.0.0-rc.1") - False - >>> cmp("1.0.0-alpha.1", "1.0.0-rc.1") - True - >>> cmp("1.0.0-alpha.1", "1.0.0-rc.1.0") - True - >>> cmp("1.0.0-alpha.1", "1.0.0-alpha.1.0") - True - >>> cmp("1.0.0-rc.2", "1.0.0-rc.10") - True - >>> Version(1, 0, 0, "rc.1") < Version(1, 0, 0) - False - """ - - if not isinstance(other, Version): - return NotImplemented - - lhs = (self.major, self.minor, self.patch) - rhs = (other.major, other.minor, other.patch) - - if lhs == rhs and self.complete and other.complete: - # relevant rules: - # 1. pre-releases sort before regular releases - # 2. two pre-releases for the same core version are sorted by the pre-release component - # (split into subcomponents) - if self.pre == other.pre: - return False - elif self.pre is None: - # self is regular release, other is pre-release - return False - elif other.pre is None: - # self is pre-release, other is regular release - return True - else: - # both are pre-releases - def int_or_str(s: str) -> object: - if s.isdigit(): - return int(s) - else: - return s - - lhs_pre = [int_or_str(s) for s in self.pre.split(".")] - rhs_pre = [int_or_str(s) for s in other.pre.split(".")] - return lhs_pre < rhs_pre - else: - return lhs < rhs - - def core(self) -> "Version": - """ - Returns the core part of this version, i. e. the version without the - pre-release and build components. - - >>> Version(1, 0, 0).core() - Version(major=1, minor=0, patch=0, pre=None, build=None) - >>> Version(1, 0, 0, "rc.1").core() - Version(major=1, minor=0, patch=0, pre=None, build=None) - >>> Version(1, 0, 0, "rc.1", "git").core() - Version(major=1, minor=0, patch=0, pre=None, build=None) - """ - return dataclasses.replace(self, pre=None, build=None) - - @classmethod - def from_int(cls, version: int) -> "Version": - # This is the reverse of the calculation in runners/lpc55/build.rs (CARGO_PKG_VERSION): - # https://github.com/Nitrokey/nitrokey-3-firmware/blob/main/runners/lpc55/build.rs#L131 - major = version >> 22 - minor = (version >> 6) & ((1 << 16) - 1) - patch = version & ((1 << 6) - 1) - return cls(major=major, minor=minor, patch=patch) - - @classmethod - def from_str(cls, s: str) -> "Version": - version_parts = s.split("+", maxsplit=1) - s = version_parts[0] - build = version_parts[1] if len(version_parts) == 2 else None - - version_parts = s.split("-", maxsplit=1) - pre = version_parts[1] if len(version_parts) == 2 else None - - str_parts = version_parts[0].split(".") - if len(str_parts) != 3: - raise ValueError(f"Invalid firmware version: {s}") - - try: - int_parts = [int(part) for part in str_parts] - except ValueError: - raise ValueError(f"Invalid component in firmware version: {s}") - - [major, minor, patch] = int_parts - return cls( - major=major, minor=minor, patch=patch, pre=pre, build=build, complete=True - ) - - @classmethod - def from_v_str(cls, s: str) -> "Version": - if not s.startswith("v"): - raise ValueError(f"Missing v prefix for firmware version: {s}") - return Version.from_str(s[1:]) - - @classmethod - def from_bcd_version(cls, version: BcdVersion3) -> "Version": - return cls(major=version.major, minor=version.minor, patch=version.service) - - -@dataclass -class Fido2Certs: - start: Version - hashes: list[str] - - @staticmethod - def get(certs: Sequence["Fido2Certs"], version: Version) -> Optional["Fido2Certs"]: - matching_certs = [c for c in certs if version >= c.start] - if matching_certs: - return max(matching_certs, key=lambda c: c.start) - else: - return None diff --git a/pynitrokey/updates.py b/pynitrokey/updates.py deleted file mode 100644 index 02dce450..00000000 --- a/pynitrokey/updates.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2022 Nitrokey Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. - -import os.path -import urllib.parse -from dataclasses import dataclass -from typing import Any, BinaryIO, Callable, Dict, Generator, Optional, Pattern - -import requests - -API_BASE_URL = "https://api.github.com" - - -ProgressCallback = Callable[[int, int], None] - - -class DownloadError(Exception): - def __init__(self, msg: str) -> None: - super().__init__("Cannot download firmware: " + msg) - - -class OverwriteError(Exception): - def __init__(self, path: str) -> None: - super().__init__(f"File {path} already exists and may not be overwritten") - self.path = path - - -@dataclass -class Asset: - tag: str - url: str - - def download( - self, f: BinaryIO, callback: Optional[ProgressCallback] = None - ) -> None: - for chunk in self._get_chunks(callback=callback): - f.write(chunk) - - def download_to_dir( - self, - d: str, - overwrite: bool = False, - callback: Optional[ProgressCallback] = None, - ) -> str: - if not os.path.exists(d): - raise DownloadError(f"Directory {d} does not exist") - if not os.path.isdir(d): - raise DownloadError(f"{d} is not a directory") - url = urllib.parse.urlparse(self.url) - filename = os.path.basename(url.path) - path = os.path.join(d, filename) - if os.path.exists(path) and not overwrite: - raise OverwriteError(path) - with open(path, "wb") as f: - self.download(f, callback=callback) - return path - - def read(self, callback: Optional[ProgressCallback] = None) -> bytes: - result = bytes() - for chunk in self._get_chunks(callback=callback): - result += chunk - return result - - def _get_chunks( - self, chunk_size: int = 1024, callback: Optional[ProgressCallback] = None - ) -> Generator[bytes, None, None]: - response = self._get(stream=True) - total = int(response.headers.get("content-length", 0)) - if callback: - callback(0, total) - - for chunk in response.iter_content(chunk_size=chunk_size): - if callback: - callback(len(chunk), total) - yield chunk - - def _get(self, stream: bool = False) -> requests.Response: - response = requests.get(self.url, stream=stream) - response.raise_for_status() - return response - - def __str__(self) -> str: - return self.url - - -@dataclass -class Release: - tag: str - assets: list[str] - - def __str__(self) -> str: - return self.tag - - def find_asset(self, url_pattern: Pattern[str]) -> Optional[Asset]: - urls = [] - for asset in self.assets: - if url_pattern.search(asset): - urls.append(asset) - - if len(urls) == 1: - return Asset(tag=self.tag, url=urls[0]) - elif len(urls) > 1: - raise ValueError( - f"Found multiple assets for release {self.tag} matching {url_pattern}" - ) - else: - return None - - def require_asset(self, url_pattern: Pattern[str]) -> Asset: - update = self.find_asset(url_pattern) - if not update: - raise ValueError( - f"Failed to find asset for release {self.tag} matching {url_pattern}" - ) - return update - - @classmethod - def _from_api_response(cls, release: dict[Any, Any]) -> "Release": - tag = release["tag_name"] - assets = [asset["browser_download_url"] for asset in release["assets"]] - if not assets: - raise ValueError(f"No update files for firmware release {tag}") - return cls(tag=tag, assets=assets) - - -@dataclass -class Repository: - owner: str - name: str - - def get_latest_release(self) -> Release: - release = self._call(f"/repos/{self.owner}/{self.name}/releases/latest") - return Release._from_api_response(release) - - def get_release(self, tag: str) -> Release: - release = self._call( - f"/repos/{self.owner}/{self.name}/releases/tags/{tag}", - {404: f"Failed to find firmware release {tag}"}, - ) - return Release._from_api_response(release) - - def get_release_or_latest(self, tag: Optional[str] = None) -> Release: - if tag: - return self.get_release(tag) - return self.get_latest_release() - - def _call(self, path: str, errors: Dict[int, str] = dict()) -> dict[Any, Any]: - url = self._get_url(path) - response = requests.get(url) - for code in errors: - if response.status_code == code: - raise ValueError(errors[code]) - response.raise_for_status() - data = response.json() - assert isinstance(data, dict) - return data - - def _get_url(self, path: str) -> str: - return API_BASE_URL + path diff --git a/pyproject.toml b/pyproject.toml index 5cbc358f..3919ff3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,15 +29,14 @@ dependencies = [ "fido2 >=1.1.2,<2", "intelhex", "nkdfu", + "nitrokey @ git+https://github.com/Nitrokey/nitrokey-sdk-py.git@trussed", "python-dateutil ~= 2.7.0", "pyusb", "requests", - "spsdk >=2.0,<2.2", "tqdm", "tlv8", "typing_extensions ~= 4.3.0", "pyserial", - "protobuf >=3.17.3, < 4.0.0", "click-aliases", "semver", "nethsm >=1.2.0, <2", @@ -110,17 +109,6 @@ strict_equality = false warn_unused_ignores = false warn_return_any = false - -# pynitrokey.nk3.bootloader.nrf52_upload is only temporary in this package -[[tool.mypy.overrides]] -module = "pynitrokey.trussed.bootloader.nrf52_upload.*" -ignore_errors = true - -# nrf52 has to use the untyped nrf52_upload module -[[tool.mypy.overrides]] -module = "pynitrokey.trussed.bootloader.nrf52" -disallow_untyped_calls = false - # libraries without annotations [[tool.mypy.overrides]] module = [