diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 569e3cd0..0452fe87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -77,20 +77,3 @@ jobs: run: | . venv/bin/activate make check-typing - test-documentation: - name: Run documentation tests - runs-on: ubuntu-latest - container: python:3.9-slim - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Install required packages - run: | - apt update - apt install -y gcc libpcsclite-dev make swig git - - name: Create virtual environment - run: make init - - name: Check code static typing - run: | - . venv/bin/activate - make check-doctest diff --git a/Makefile b/Makefile index d0b5f5f0..755e56c6 100644 --- a/Makefile +++ b/Makefile @@ -46,10 +46,7 @@ check-typing: @echo "Note: run semi-clean target in case this fails without any proper reason" $(PYTHON3_VENV) -m mypy $(PACKAGE_NAME)/ -check-doctest: - $(PYTHON3_VENV) -m doctest $(PACKAGE_NAME)/nk3/utils.py - -check: check-format check-import-sorting check-style check-typing check-doctest +check: check-format check-import-sorting check-style check-typing # automatic code fixes fix: diff --git a/pynitrokey/cli/trussed/tests.py b/pynitrokey/cli/trussed/tests.py index 2c8d2af4..ca12f3a6 100644 --- a/pynitrokey/cli/trussed/tests.py +++ b/pynitrokey/cli/trussed/tests.py @@ -19,10 +19,9 @@ 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.nk3.utils import Fido2Certs from pynitrokey.trussed.base import NitrokeyTrussedBase from pynitrokey.trussed.device import NitrokeyTrussedDevice -from pynitrokey.trussed.utils import Uuid, Version +from pynitrokey.trussed.utils import Fido2Certs, Uuid, Version logger = logging.getLogger(__name__) @@ -400,7 +399,7 @@ def request_uv(self, permissions: Any, rd_id: Any) -> bool: firmware_version = ctx.firmware_version or device.admin.version() if firmware_version: - expected_certs = Fido2Certs.get(firmware_version) + expected_certs = Fido2Certs.get(device.fido2_certs, firmware_version) if expected_certs and cert_hash not in expected_certs.hashes: return TestResult( TestStatus.FAILURE, diff --git a/pynitrokey/nk3/device.py b/pynitrokey/nk3/device.py index df255188..436b5c48 100644 --- a/pynitrokey/nk3/device.py +++ b/pynitrokey/nk3/device.py @@ -10,13 +10,31 @@ 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) + super().__init__(device, FIDO2_CERTS) @property def pid(self) -> int: @@ -27,3 +45,7 @@ def pid(self) -> int: @property def name(self) -> str: return "Nitrokey 3" + + @classmethod + def from_device(cls, device: CtapHidDevice) -> "Nitrokey3Device": + return cls(device) diff --git a/pynitrokey/nk3/utils.py b/pynitrokey/nk3/utils.py deleted file mode 100644 index 3d9913ff..00000000 --- a/pynitrokey/nk3/utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2021-2023 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 dataclasses import dataclass -from typing import Optional - -from pynitrokey.trussed.utils import Version - - -@dataclass -class Fido2Certs: - start: Version - hashes: list[str] - - @classmethod - def get(cls, version: Version) -> Optional["Fido2Certs"]: - """ - >>> Fido2Certs.get(Version.from_str("0.0.0")) - >>> Fido2Certs.get(Version.from_str("0.1.0")).start - Version(major=0, minor=1, patch=0, pre=None, build=None) - >>> Fido2Certs.get(Version.from_str("0.1.0-rc.1")).start - Version(major=0, minor=1, patch=0, pre=None, build=None) - >>> Fido2Certs.get(Version.from_str("0.2.0")).start - Version(major=0, minor=1, patch=0, pre=None, build=None) - >>> Fido2Certs.get(Version.from_str("1.0.3")).start - Version(major=1, minor=0, patch=3, pre=None, build=None) - >>> Fido2Certs.get(Version.from_str("1.0.3-alpha.1")).start - Version(major=1, minor=0, patch=3, pre=None, build=None) - >>> Fido2Certs.get(Version.from_str("2.5.0")).start - Version(major=1, minor=0, patch=3, pre=None, build=None) - """ - certs = [certs for certs in FIDO2_CERTS if version >= certs.start] - if certs: - return max(certs, key=lambda c: c.start) - else: - return None - - -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 - ], - ), -] diff --git a/pynitrokey/nkpk.py b/pynitrokey/nkpk.py index 3a145807..8538e876 100644 --- a/pynitrokey/nkpk.py +++ b/pynitrokey/nkpk.py @@ -15,14 +15,24 @@ from pynitrokey.trussed.base import NitrokeyTrussedBase from pynitrokey.trussed.bootloader.nrf52 import NitrokeyTrussedBootloaderNrf52 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", + ], + ), +] + class NitrokeyPasskeyDevice(NitrokeyTrussedDevice): def __init__(self, device: CtapHidDevice) -> None: - super().__init__(device) + super().__init__(device, FIDO2_CERTS) @property def pid(self) -> int: @@ -32,6 +42,10 @@ def pid(self) -> int: def name(self) -> str: return "Nitrokey Passkey" + @classmethod + def from_device(cls, device: CtapHidDevice) -> "NitrokeyPasskeyDevice": + return cls(device) + class NitrokeyPasskeyBootloader(NitrokeyTrussedBootloaderNrf52): @property diff --git a/pynitrokey/trussed/device.py b/pynitrokey/trussed/device.py index 377713ac..5678ff3e 100644 --- a/pynitrokey/trussed/device.py +++ b/pynitrokey/trussed/device.py @@ -13,14 +13,14 @@ import sys from abc import abstractmethod from enum import Enum -from typing import Optional, TypeVar +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 Uuid, Version +from .utils import Fido2Certs, Uuid, Version T = TypeVar("T", bound="NitrokeyTrussedDevice") @@ -37,10 +37,13 @@ class App(Enum): class NitrokeyTrussedDevice(NitrokeyTrussedBase): - def __init__(self, device: CtapHidDevice) -> None: + 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) @@ -90,6 +93,11 @@ def _call_app( ) -> 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: @@ -101,7 +109,7 @@ def open(cls: type[T], path: str) -> Optional[T]: logger.warn(f"No CTAPHID device at path {path}", exc_info=sys.exc_info()) return None try: - return cls(device) + return cls.from_device(device) except ValueError: logger.warn(f"No Nitrokey device at path {path}", exc_info=sys.exc_info()) return None @@ -111,7 +119,7 @@ def list(cls: type[T]) -> list[T]: devices = [] for device in CtapHidDevice.list_devices(): try: - devices.append(cls(device)) + devices.append(cls.from_device(device)) except ValueError: # not the correct device type, skip pass diff --git a/pynitrokey/trussed/utils.py b/pynitrokey/trussed/utils.py index 1ff4e5fd..a4b0cc69 100644 --- a/pynitrokey/trussed/utils.py +++ b/pynitrokey/trussed/utils.py @@ -10,7 +10,7 @@ import dataclasses from dataclasses import dataclass, field from functools import total_ordering -from typing import Optional +from typing import Optional, Sequence from spsdk.sbfile.misc import BcdVersion3 @@ -231,3 +231,17 @@ def from_v_str(cls, s: str) -> "Version": @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