Skip to content

Commit

Permalink
Use Nitrokey Python SDK
Browse files Browse the repository at this point in the history
This patch removes the trussed, nk3 and nkpk modules that have been
extracted into the the Nitrokey Python SDK available at
https://github.com/Nitrokey/nitrokey-sdk-py.

The SDK provides a stable API without unnecessary dependencies that can
be used by pynitrokey, nitrokey-app2 and third-party applications.
  • Loading branch information
robin-nitrokey committed Aug 1, 2024
1 parent b9e1f93 commit 2256fc5
Show file tree
Hide file tree
Showing 56 changed files with 122 additions and 7,821 deletions.
1 change: 0 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions docs/developer-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ Usage:

Checks configured in ``.pre-commit-config.yaml`` will be executed before each commit, and on-demand when calling ``pre-commit`` from the command line.

Patching the Nitrokey SDK
-------------------------

To use an unreleased version of the Nitrokey Python SDK, replace the ``nitrokey`` dependency in ``pyproject.toml`` with::

"nitrokey @ git+https://github.com/Nitrokey/nitrokey-sdk-py.git@rev",

``rev`` can be a branch name, a tag or a commit hash.

It is also possible to use a local path::

"nitrokey @ file:../nitrokey-sdk-py",


Design Patterns
---------------
Expand Down
37 changes: 22 additions & 15 deletions pynitrokey/cli/nk3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,21 @@
# copied, modified, or distributed except according to those terms.

import sys
from typing import List, Optional
from typing import Optional, Sequence

import click
from nitrokey.nk3 import NK3, NK3Bootloader
from nitrokey.trussed import Model, TrussedBase

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]):
class Context(trussed.Context[NK3Bootloader, NK3]):
def __init__(self, path: Optional[str]) -> None:
super().__init__(path, Nitrokey3Bootloader, Nitrokey3Device, Device.NITROKEY3, NK3_DATA) # type: ignore[type-abstract]
super().__init__(path, NK3Bootloader, NK3, Model.NK3) # type: ignore[type-abstract]

@property
def test_cases(self) -> list[TestCase]:
Expand All @@ -41,13 +38,13 @@ def test_cases(self) -> list[TestCase]:
tests.test_fido2,
]

def open(self, path: str) -> Optional[NitrokeyTrussedBase]:
from pynitrokey.nk3 import open
def open(self, path: str) -> Optional[TrussedBase]:
from nitrokey.nk3 import open

return open(path)

def list_all(self) -> List[NitrokeyTrussedBase]:
from pynitrokey.nk3 import list
def list_all(self) -> Sequence[TrussedBase]:
from nitrokey.nk3 import list

return list()

Expand Down Expand Up @@ -252,7 +249,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
Expand All @@ -278,7 +280,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()
Expand Down
8 changes: 4 additions & 4 deletions pynitrokey/cli/nk3/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pynitrokey/cli/nk3/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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__)

Expand Down
24 changes: 11 additions & 13 deletions pynitrokey/cli/nkpk.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,24 @@
# 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 Optional
from typing import Optional, Sequence

import click
from nitrokey.nkpk import NKPK, NKPKBootloader
from nitrokey.trussed import Model, TrussedBase

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


class Context(trussed.Context[NitrokeyPasskeyBootloader, NitrokeyPasskeyDevice]):
class Context(trussed.Context[NKPKBootloader, NKPK]):
def __init__(self, path: Optional[str]) -> None:
super().__init__(
path,
NitrokeyPasskeyBootloader,
NitrokeyPasskeyDevice,
Device.NITROKEY_PASSKEY,
NKPK_DATA,
NKPKBootloader,
NKPK,
Model.NKPK,
)

@property
Expand All @@ -46,13 +44,13 @@ def test_cases(self) -> list[TestCase]:
def device_name(self) -> str:
return "Nitrokey Passkey"

def open(self, path: str) -> Optional[NitrokeyTrussedBase]:
from pynitrokey.nkpk import open
def open(self, path: str) -> Optional[TrussedBase]:
from nitrokey.nkpk import open

return open(path)

def list_all(self) -> list[NitrokeyTrussedBase]:
from pynitrokey.nkpk import list
def list_all(self) -> Sequence[TrussedBase]:
from nitrokey.nkpk import list

return list()

Expand Down
72 changes: 34 additions & 38 deletions pynitrokey/cli/trussed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from ecdsa import NIST256p, SigningKey
from nitrokey.trussed import (
FirmwareContainer,
Model,
TimeoutException,
TrussedBase,
TrussedBootloader,
TrussedDevice,
parse_firmware_image,
)
from nitrokey.trussed.admin_app import BootMode
from nitrokey.trussed.provisioner_app import ProvisionerApp
from nitrokey.updates import OverwriteError

from pynitrokey.cli.exceptions import CliException
from pynitrokey.helpers import (
Expand All @@ -26,25 +38,12 @@
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

T = TypeVar("T", bound=NitrokeyTrussedBase)
Bootloader = TypeVar("Bootloader", bound=NitrokeyTrussedBootloader)
Device = TypeVar("Device", bound=NitrokeyTrussedDevice)
T = TypeVar("T", bound=TrussedBase)
Bootloader = TypeVar("Bootloader", bound=TrussedBootloader)
Device = TypeVar("Device", bound=TrussedDevice)

logger = logging.getLogger(__name__)

Expand All @@ -55,29 +54,27 @@ def __init__(
path: Optional[str],
bootloader_type: type[Bootloader],
device_type: type[Device],
bootloader_device: BootloaderDevice,
data: DeviceData,
model: Model,
) -> None:
self.path = path
self.bootloader_type = bootloader_type
self.device_type = device_type
self.bootloader_device = bootloader_device
self.data = data
self.model = model

@property
@abstractmethod
def test_cases(self) -> Sequence[TestCase]:
...

@abstractmethod
def open(self, path: str) -> Optional[NitrokeyTrussedBase]:
def open(self, path: str) -> Optional[TrussedBase]:
...

@abstractmethod
def list_all(self) -> Sequence[NitrokeyTrussedBase]:
def list_all(self) -> Sequence[TrussedBase]:
...

def list(self) -> Sequence[NitrokeyTrussedBase]:
def list(self) -> Sequence[TrussedBase]:
if self.path:
device = self.open(self.path)
if device:
Expand All @@ -87,30 +84,29 @@ def list(self) -> Sequence[NitrokeyTrussedBase]:
else:
return self.list_all()

def connect(self) -> NitrokeyTrussedBase:
return self._select_unique(self.data.name, self.list())
def connect(self) -> TrussedBase:
return self._select_unique(self.model.name, self.list())

def connect_device(self) -> Device:
devices = [
device for device in self.list() if isinstance(device, self.device_type)
]
return self._select_unique(self.data.name, devices)
return self._select_unique(self.model.name, devices)

def await_device(
self,
retries: Optional[int] = None,
callback: Optional[Callable[[int, int], None]] = None,
) -> Device:
return self._await(self.data.name, self.device_type, retries, callback)
return self._await(self.model.name, self.device_type, retries, callback)

def await_bootloader(
self,
retries: Optional[int] = None,
callback: Optional[Callable[[int, int], None]] = None,
) -> Bootloader:
# mypy does not allow abstract types here, but this is still valid
return self._await(
f"{self.data.name} bootloader", self.bootloader_type, retries, callback
f"{self.model.name} bootloader", self.bootloader_type, retries, callback
)

def _select_unique(self, name: str, devices: Sequence[T]) -> T:
Expand Down Expand Up @@ -195,8 +191,8 @@ def fetch_update(
download a specific version, use the --version option.
"""
try:
release = ctx.data.firmware_repository.get_release_or_latest(version)
update = release.require_asset(ctx.data.firmware_pattern)
release = ctx.model.firmware_repository.get_release_or_latest(version)
update = release.require_asset(ctx.model.firmware_pattern)
except Exception as e:
if version:
raise CliException(f"Failed to find firmware update {version}", e)
Expand Down Expand Up @@ -234,7 +230,7 @@ def list(ctx: Context[Bootloader, Device]) -> None:


def _list(ctx: Context[Bootloader, Device]) -> None:
local_print(f":: '{ctx.data.name}' keys")
local_print(f":: '{ctx.model.name}' keys")
for device in ctx.list_all():
with device as device:
uuid = device.uuid()
Expand Down Expand Up @@ -354,7 +350,7 @@ def reboot(ctx: Context[Bootloader, Device], bootloader: bool) -> None:
"""
with ctx.connect() as device:
if bootloader:
if isinstance(device, NitrokeyTrussedDevice):
if isinstance(device, TrussedDevice):
success = reboot_to_bootloader(device)
else:
raise CliException(
Expand All @@ -372,7 +368,7 @@ def reboot(ctx: Context[Bootloader, Device], bootloader: bool) -> None:
)


def reboot_to_bootloader(device: NitrokeyTrussedDevice) -> bool:
def reboot_to_bootloader(device: TrussedDevice) -> bool:
local_print(
"Please press the touch button to reboot the device into bootloader mode ..."
)
Expand Down Expand Up @@ -503,9 +499,9 @@ def test(

if len(devices) == 0:
log_devices()
raise CliException(f"No connected {ctx.data.name} devices found")
raise CliException(f"No connected {ctx.model.name} devices found")

local_print(f"Found {len(devices)} {ctx.data.name} device(s):")
local_print(f"Found {len(devices)} {ctx.model.name} device(s):")
for device in devices:
local_print(f"- {device.name} at {device.path}")

Expand Down Expand Up @@ -543,7 +539,7 @@ def validate_update(ctx: Context[Bootloader, Device], image: str) -> None:
available variants.
"""
try:
container = FirmwareContainer.parse(image, ctx.bootloader_device)
container = FirmwareContainer.parse(image, ctx.model)
except ValueError as e:
raise CliException("Failed to validate firmware image", e, support_hint=False)

Expand All @@ -554,7 +550,7 @@ def validate_update(ctx: Context[Bootloader, Device], image: str) -> None:
for variant in container.images:
data = container.images[variant]
try:
metadata = parse_firmware_image(variant, data, ctx.data)
metadata = parse_firmware_image(variant, data, ctx.model)
except Exception as e:
raise CliException("Failed to parse and validate firmware image", e)

Expand Down
Loading

0 comments on commit 2256fc5

Please sign in to comment.