Skip to content

Commit

Permalink
Add separate firmware signature keys for nkpk
Browse files Browse the repository at this point in the history
  • Loading branch information
robin-nitrokey committed Feb 2, 2024
1 parent c953b42 commit 9c49614
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 34 deletions.
2 changes: 1 addition & 1 deletion pynitrokey/cli/trussed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
22 changes: 19 additions & 3 deletions pynitrokey/nk3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,17 +21,35 @@
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())
return devices


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:
Expand Down
13 changes: 11 additions & 2 deletions pynitrokey/nk3/bootloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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] = []
Expand Down
1 change: 1 addition & 0 deletions pynitrokey/nk3/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 21 additions & 2 deletions pynitrokey/nkpk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
),
],
)


Expand Down Expand Up @@ -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] = []
Expand Down
5 changes: 5 additions & 0 deletions pynitrokey/trussed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions pynitrokey/trussed/bootloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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}")
37 changes: 14 additions & 23 deletions pynitrokey/trussed/bootloader/nrf52.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 9c49614

Please sign in to comment.