diff --git a/pynitrokey/cli/trussed/__init__.py b/pynitrokey/cli/trussed/__init__.py index accd51f9..601d4cac 100644 --- a/pynitrokey/cli/trussed/__init__.py +++ b/pynitrokey/cli/trussed/__init__.py @@ -555,7 +555,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) + metadata = parse_firmware_image(variant, data, ctx.data) except Exception as e: raise CliException("Failed to parse and validate firmware image", e) diff --git a/pynitrokey/nk3/__init__.py b/pynitrokey/nk3/__init__.py index f089fff5..309d22cf 100644 --- a/pynitrokey/nk3/__init__.py +++ b/pynitrokey/nk3/__init__.py @@ -11,9 +11,7 @@ from pynitrokey.trussed import DeviceData from pynitrokey.trussed.base import NitrokeyTrussedBase - -from . import bootloader -from .device import Nitrokey3Device +from pynitrokey.trussed.bootloader.nrf52 import SignatureKey PID_NITROKEY3_DEVICE = 0x42B2 PID_NITROKEY3_LPC55_BOOTLOADER = 0x42DD @@ -23,10 +21,25 @@ 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()) @@ -34,6 +47,9 @@ def list() -> List[NitrokeyTrussedBase]: 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: diff --git a/pynitrokey/nk3/bootloader.py b/pynitrokey/nk3/bootloader.py index 7c685c4b..8944394e 100644 --- a/pynitrokey/nk3/bootloader.py +++ b/pynitrokey/nk3/bootloader.py @@ -7,12 +7,17 @@ # 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 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 +from pynitrokey.trussed.bootloader.nrf52 import ( + NitrokeyTrussedBootloaderNrf52, + SignatureKey, +) + +from . import NK3_DATA class Nitrokey3Bootloader(NitrokeyTrussedBootloader): @@ -60,6 +65,10 @@ def open(cls, path: str) -> Optional["Nitrokey3BootloaderNrf52"]: 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] = [] diff --git a/pynitrokey/nk3/updates.py b/pynitrokey/nk3/updates.py index 9a6cb6ad..4899079e 100644 --- a/pynitrokey/nk3/updates.py +++ b/pynitrokey/nk3/updates.py @@ -200,6 +200,7 @@ def update( bootloader.variant, container.images[bootloader.variant], container.version, + NK3_DATA, ) except Exception as e: raise self.ui.error("Failed to validate firmware image", e) diff --git a/pynitrokey/nkpk.py b/pynitrokey/nkpk.py index 2a97066f..1e5cd1c4 100644 --- a/pynitrokey/nkpk.py +++ b/pynitrokey/nkpk.py @@ -7,13 +7,16 @@ # 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 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 +from pynitrokey.trussed.bootloader.nrf52 import ( + NitrokeyTrussedBootloaderNrf52, + SignatureKey, +) from pynitrokey.trussed.device import NitrokeyTrussedDevice PID_NITROKEY_PASSKEY_DEVICE = 0x42F3 @@ -23,6 +26,18 @@ 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", + ), + ], ) @@ -56,6 +71,10 @@ def list(cls) -> List["NitrokeyPasskeyBootloader"]: 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] = [] diff --git a/pynitrokey/trussed/__init__.py b/pynitrokey/trussed/__init__.py index 5001105c..3fc395be 100644 --- a/pynitrokey/trussed/__init__.py +++ b/pynitrokey/trussed/__init__.py @@ -10,9 +10,13 @@ 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 @@ -21,6 +25,7 @@ class DeviceData: name: str firmware_repository_name: str firmware_pattern_string: str + nrf52_signature_keys: list["SignatureKey"] @property def firmware_repository(self) -> Repository: diff --git a/pynitrokey/trussed/bootloader/__init__.py b/pynitrokey/trussed/bootloader/__init__.py index f5d841db..194e029d 100644 --- a/pynitrokey/trussed/bootloader/__init__.py +++ b/pynitrokey/trussed/bootloader/__init__.py @@ -19,6 +19,7 @@ from typing import Callable, Dict, List, Optional, Tuple, Union from zipfile import ZipFile +from .. import DeviceData from ..base import NitrokeyTrussedBase from ..utils import Version @@ -149,9 +150,10 @@ def validate_firmware_image( variant: Variant, data: bytes, version: Optional[Version], + device: DeviceData, ) -> FirmwareMetadata: try: - metadata = parse_firmware_image(variant, data) + 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") @@ -174,13 +176,15 @@ def validate_firmware_image( return metadata -def parse_firmware_image(variant: Variant, data: bytes) -> FirmwareMetadata: +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) + return parse_firmware_image_nrf52(data, device.nrf52_signature_keys) else: raise ValueError(f"Unexpected variant {variant}") diff --git a/pynitrokey/trussed/bootloader/nrf52.py b/pynitrokey/trussed/bootloader/nrf52.py index 1a36ccf7..5b76c0e7 100644 --- a/pynitrokey/trussed/bootloader/nrf52.py +++ b/pynitrokey/trussed/bootloader/nrf52.py @@ -11,9 +11,10 @@ import logging import re import time +from abc import abstractmethod from dataclasses import dataclass from io import BytesIO -from typing import Optional, TypeVar +from typing import Optional, Sequence, TypeVar from zipfile import ZipFile import ecdsa @@ -43,6 +44,8 @@ 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: @@ -60,23 +63,6 @@ def verify(self, signature: str, message: str) -> bool: return False -# openssl ec -in dfu_public.pem -inform pem -pubin -outform der | xxd -p -SIGNATURE_KEYS = [ - # Nitrokey production key - SignatureKey( - name="Nitrokey", - is_official=True, - der="3059301306072a8648ce3d020106082a8648ce3d03010703420004a0849b19007ccd4661c01c533804b7fd0c4d8c0e7583653f1f36a8331afff298b542bd00a3dc47c16bf428ac4d2864137d63f702d89e5b42674e0549b4232618", - ), - # Nitrokey test key - SignatureKey( - name="Nitrokey Test", - is_official=False, - der="3059301306072a8648ce3d020106082a8648ce3d0301070342000493e461ab0582bda1f45b0ce47d66bc4e8623e289c31af2098cde6ebd8631da85acf17e412d406c1e38c2de654a8fd0196506a85b169a756aeac2505a541cdd5d", - ), -] - - @dataclass class Image: init_packet: InitPacketPB @@ -86,7 +72,7 @@ class Image: signature_key: Optional[SignatureKey] = None @classmethod - def parse(cls, data: bytes) -> "Image": + 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: @@ -128,7 +114,7 @@ def parse(cls, data: bytes) -> "Image": # see nordicsemi.dfu.signing.Signing.sign signature = signature[31::-1] + signature[63:31:-1] message = init_packet.get_init_command_bytes() - for key in SIGNATURE_KEYS: + for key in keys: if key.verify(signature, message): image.signature_key = key @@ -148,6 +134,11 @@ def variant(self) -> Variant: def path(self) -> str: return self._path + @property + @abstractmethod + def signature_keys(self) -> Sequence[SignatureKey]: + ... + def close(self) -> None: pass @@ -162,7 +153,7 @@ def update(self, data: bytes, callback: Optional[ProgressCallback] = None) -> No # we have to implement this ourselves because we want to read the files # from memory, not from the filesystem - image = Image.parse(data) + image = Image.parse(data, self.signature_keys) time.sleep(3) @@ -226,8 +217,8 @@ def _list_ports(vid: int, pid: int) -> list[tuple[str, int]]: return ports -def parse_firmware_image(data: bytes) -> FirmwareMetadata: - image = Image.parse(data) +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)