From bd5b91af1388a0ce97b9e716ccec34ba603f1b98 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 14 Dec 2021 19:58:41 +0100 Subject: [PATCH] nk3: Add support for bootloader using spsdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds support for listing Nitrokey 3 bootloaders, querying their UUID and rebooting them. It uses NXP’s spsdk library to interact with the bootloader. --- ci-requirements.txt | 1 + mypy.ini | 2 +- pynitrokey/nk3/__init__.py | 16 ++- pynitrokey/nk3/bootloader.py | 109 ++++++++++++++++++++ pynitrokey/stubs/spsdk/__init__.py | 8 ++ pynitrokey/stubs/spsdk/mboot/__init__.pyi | 20 ++++ pynitrokey/stubs/spsdk/mboot/interfaces.pyi | 21 ++++ pynitrokey/stubs/spsdk/mboot/properties.pyi | 14 +++ pynitrokey/stubs/spsdk/utils/__init__.pyi | 8 ++ pynitrokey/stubs/spsdk/utils/usbfilter.pyi | 11 ++ pyproject.toml | 1 + 11 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 pynitrokey/nk3/bootloader.py create mode 100644 pynitrokey/stubs/spsdk/__init__.py create mode 100644 pynitrokey/stubs/spsdk/mboot/__init__.pyi create mode 100644 pynitrokey/stubs/spsdk/mboot/interfaces.pyi create mode 100644 pynitrokey/stubs/spsdk/mboot/properties.pyi create mode 100644 pynitrokey/stubs/spsdk/utils/__init__.pyi create mode 100644 pynitrokey/stubs/spsdk/utils/usbfilter.pyi diff --git a/ci-requirements.txt b/ci-requirements.txt index a7d4e219..4e440819 100644 --- a/ci-requirements.txt +++ b/ci-requirements.txt @@ -9,6 +9,7 @@ pyusb requests pygments python-dateutil +spsdk>=1.5.0 urllib3 cffi diff --git a/mypy.ini b/mypy.ini index b2daf62a..cee839f3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -9,5 +9,5 @@ disallow_untyped_defs = True ignore_errors = True # libraries without annotations -[mypy-cbor.*,cffi.*,click.*,ecdsa.*,intelhex.*,nacl.*,nkdfu.*,serial.*,urllib3.*,usb.*,usb1.*] +[mypy-cbor.*,cffi.*,click.*,cryptography.*,ecdsa.*,intelhex.*,nacl.*,nkdfu.*,serial.*,urllib3.*,usb.*,usb1.*] ignore_missing_imports = True diff --git a/pynitrokey/nk3/__init__.py b/pynitrokey/nk3/__init__.py index f27a2272..52ba23df 100644 --- a/pynitrokey/nk3/__init__.py +++ b/pynitrokey/nk3/__init__.py @@ -10,6 +10,7 @@ from typing import List, Optional from .base import Nitrokey3Base +from .bootloader import Nitrokey3Bootloader from .device import Nitrokey3Device VID_NITROKEY = 0x20A0 @@ -18,8 +19,19 @@ def list() -> List[Nitrokey3Base]: - return [device for device in Nitrokey3Device.list()] + devices: List[Nitrokey3Base] = [] + devices.extend(Nitrokey3Bootloader.list()) + devices.extend(Nitrokey3Device.list()) + return devices def open(path: str) -> Optional[Nitrokey3Base]: - return Nitrokey3Device.open(path) + device = Nitrokey3Device.open(path) + bootloader = Nitrokey3Bootloader.open(path) + if device and bootloader: + raise Exception(f"Found multiple devices at path {path}") + if device: + return device + if bootloader: + return bootloader + return None diff --git a/pynitrokey/nk3/bootloader.py b/pynitrokey/nk3/bootloader.py new file mode 100644 index 00000000..db9bb461 --- /dev/null +++ b/pynitrokey/nk3/bootloader.py @@ -0,0 +1,109 @@ +# -*- 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. + +import logging +import sys +from typing import List, Optional + +from spsdk.mboot import McuBoot +from spsdk.mboot.interfaces import RawHid +from spsdk.mboot.properties import PropertyTag +from spsdk.utils.usbfilter import USBDeviceFilter + +from .base import Nitrokey3Base + +UUID_LEN = 4 + +logger = logging.getLogger(__name__) + + +class Nitrokey3Bootloader(Nitrokey3Base): + """A Nitrokey 3 device running the bootloader.""" + + def __init__(self, device: RawHid): + from . import PID_NITROKEY3_BOOTLOADER, VID_NITROKEY + + if (device.vid, device.pid) != (VID_NITROKEY, PID_NITROKEY3_BOOTLOADER): + raise ValueError( + "Not a Nitrokey 3 device: expected VID:PID " + f"{VID_NITROKEY:x}:{PID_NITROKEY3_BOOTLOADER:x}, " + f"got {device.vid:x}:{device.pid:x}" + ) + self._path = device.path + self.device = McuBoot(device) + + def __enter__(self) -> "Nitrokey3Bootloader": + self.device.open() + return self + + @property + def path(self) -> str: + if isinstance(self._path, bytes): + return self._path.decode("UTF-8") + return self._path + + @property + def name(self) -> str: + return "Nitrokey 3 Bootloader" + + def close(self) -> None: + self.device.close() + + def reboot(self) -> None: + if not self.device.reset(reopen=False): + raise Exception("Failed to reboot Nitrokey 3 bootloader") + + def uuid(self) -> Optional[int]: + 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 int.from_bytes(right_endian, byteorder="big") + + @staticmethod + def list() -> List["Nitrokey3Bootloader"]: + from . import PID_NITROKEY3_BOOTLOADER, VID_NITROKEY + + device_filter = USBDeviceFilter( + f"0x{VID_NITROKEY:x}:0x{PID_NITROKEY3_BOOTLOADER:x}" + ) + devices = [] + for device in RawHid.enumerate(device_filter): + try: + devices.append(Nitrokey3Bootloader(device)) + except ValueError: + logger.warn( + f"Invalid Nitrokey 3 bootloader returned by enumeration: {device}" + ) + return devices + + @staticmethod + def open(path: str) -> Optional["Nitrokey3Bootloader"]: + device_filter = USBDeviceFilter(path) + devices = RawHid.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 Nitrokey3Bootloader(devices[0]) + except ValueError: + logger.warn( + f"No Nitrokey 3 bootloader at path {path}", exc_info=sys.exc_info() + ) + return None diff --git a/pynitrokey/stubs/spsdk/__init__.py b/pynitrokey/stubs/spsdk/__init__.py new file mode 100644 index 00000000..92a8ebd6 --- /dev/null +++ b/pynitrokey/stubs/spsdk/__init__.py @@ -0,0 +1,8 @@ +# -*- 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. diff --git a/pynitrokey/stubs/spsdk/mboot/__init__.pyi b/pynitrokey/stubs/spsdk/mboot/__init__.pyi new file mode 100644 index 00000000..4f8d44a9 --- /dev/null +++ b/pynitrokey/stubs/spsdk/mboot/__init__.pyi @@ -0,0 +1,20 @@ +# -*- 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 typing import List, Optional + +from .interfaces import Interface +from .properties import PropertyTag + +class McuBoot: + def __init__(self, device: Interface) -> None: ... + def open(self) -> None: ... + def close(self) -> None: ... + def reset(self, reopen: bool) -> bool: ... + def get_property(self, prop_tag: PropertyTag) -> Optional[List[int]]: ... diff --git a/pynitrokey/stubs/spsdk/mboot/interfaces.pyi b/pynitrokey/stubs/spsdk/mboot/interfaces.pyi new file mode 100644 index 00000000..7bc932d3 --- /dev/null +++ b/pynitrokey/stubs/spsdk/mboot/interfaces.pyi @@ -0,0 +1,21 @@ +# -*- 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 typing import Union + +from ..utils.usbfilter import USBDeviceFilter + +class Interface: ... + +class RawHid(Interface): + vid: int + pid: int + path: Union[bytes, str] + @staticmethod + def enumerate(usb_device_filter: USBDeviceFilter): ... diff --git a/pynitrokey/stubs/spsdk/mboot/properties.pyi b/pynitrokey/stubs/spsdk/mboot/properties.pyi new file mode 100644 index 00000000..9cc2275a --- /dev/null +++ b/pynitrokey/stubs/spsdk/mboot/properties.pyi @@ -0,0 +1,14 @@ +# -*- 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 enum import Enum +from typing import Tuple + +class PropertyTag(Enum): + UNIQUE_DEVICE_IDENT: Tuple[int, str, str] diff --git a/pynitrokey/stubs/spsdk/utils/__init__.pyi b/pynitrokey/stubs/spsdk/utils/__init__.pyi new file mode 100644 index 00000000..92a8ebd6 --- /dev/null +++ b/pynitrokey/stubs/spsdk/utils/__init__.pyi @@ -0,0 +1,8 @@ +# -*- 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. diff --git a/pynitrokey/stubs/spsdk/utils/usbfilter.pyi b/pynitrokey/stubs/spsdk/utils/usbfilter.pyi new file mode 100644 index 00000000..854abac8 --- /dev/null +++ b/pynitrokey/stubs/spsdk/utils/usbfilter.pyi @@ -0,0 +1,11 @@ +# -*- 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. + +class USBDeviceFilter: + def __init__(self, usb_id: str) -> None: ... diff --git a/pyproject.toml b/pyproject.toml index d9703711..f25dac5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ requires = [ "requests", "pygments", "python-dateutil", + "spsdk >= 1.5.0", "urllib3", "cffi", "cbor",