Skip to content

Commit

Permalink
Merge pull request Nitrokey#132 from robin-nitrokey/nk3-bootloader
Browse files Browse the repository at this point in the history
nk3: Add support for bootloader using spsdk
  • Loading branch information
robin-nitrokey authored Dec 15, 2021
2 parents 68b680f + bd5b91a commit 3f57f82
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 3 deletions.
1 change: 1 addition & 0 deletions ci-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pyusb
requests
pygments
python-dateutil
spsdk>=1.5.0
urllib3
cffi

2 changes: 1 addition & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 14 additions & 2 deletions pynitrokey/nk3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import List, Optional

from .base import Nitrokey3Base
from .bootloader import Nitrokey3Bootloader
from .device import Nitrokey3Device

VID_NITROKEY = 0x20A0
Expand All @@ -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
109 changes: 109 additions & 0 deletions pynitrokey/nk3/bootloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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
8 changes: 8 additions & 0 deletions pynitrokey/stubs/spsdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
20 changes: 20 additions & 0 deletions pynitrokey/stubs/spsdk/mboot/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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]]: ...
21 changes: 21 additions & 0 deletions pynitrokey/stubs/spsdk/mboot/interfaces.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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): ...
14 changes: 14 additions & 0 deletions pynitrokey/stubs/spsdk/mboot/properties.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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]
8 changes: 8 additions & 0 deletions pynitrokey/stubs/spsdk/utils/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
11 changes: 11 additions & 0 deletions pynitrokey/stubs/spsdk/utils/usbfilter.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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: ...
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ requires = [
"requests",
"pygments",
"python-dateutil",
"spsdk >= 1.5.0",
"urllib3",
"cffi",
"cbor",
Expand Down

0 comments on commit 3f57f82

Please sign in to comment.