Skip to content

Commit

Permalink
Vendor SPSDK dependency
Browse files Browse the repository at this point in the history
This commit is extracted from:  Nitrokey/pynitrokey#519

I’ve further reduced the included code so that we can get rid of even
more dependencies:  We only need crcmod, cryptography and libusbsio.  We
now also pass strict mypy checks on all imported modules (except for the
libusbsio imports).

Co-authored-by: Sosthène Guédon <[email protected]>
  • Loading branch information
robin-nitrokey and sosthene-nitrokey committed Aug 5, 2024
1 parent e1bb1c8 commit da1137e
Show file tree
Hide file tree
Showing 53 changed files with 8,524 additions and 1,013 deletions.
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[flake8]
# E203,E701 suggested by black, see:
# https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
# E221 for alignment in mboot code
# E501 (line length) disabled as this is handled by black which takes better care of edge cases
extend-ignore = E203,E501,E701
extend-ignore = E203,E221,E501,E701
max-complexity = 18
extend-exclude = src/nitrokey/trussed/_bootloader/nrf52_upload
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `trussed.admin_app`: Add error codes `CONFIG_ERROR` and `RNG_ERROR` to `InitStatus` enum

### Other Changes

- Vendor `spsdk` dependency to reduce the total number of dependencies

## [v0.1.0](https://github.com/Nitrokey/nitrokey-sdk-py/releases/tag/v0.1.0) (2024-07-29)

Initial release with support for Nitrokey 3 and Nitrokey Passkey devices and the admin, provisioner and secrets app.
996 changes: 1 addition & 995 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ semver = "^3"
tlv8 = "^0.10"

# lpc55
spsdk = ">=2,<2.3"
crcmod = "^1.7"
cryptography = ">=42"
libusbsio = "^2.1"

# nrf52
ecdsa = "^0.19"
intelhex = "^2.3"
protobuf = "^3.17.3"
pyserial = "^3.5"

Expand All @@ -41,6 +44,7 @@ flake8 = "^7.1"
isort = "^5.13.2"
mypy = "^1.4"
types-requests = "^2.32"
typing-extensions = "^4"

[tool.black]
target-version = ["py39"]
Expand All @@ -64,3 +68,8 @@ ignore_errors = true
[[tool.mypy.overrides]]
module = "nitrokey.trussed._bootloader.nrf52"
disallow_untyped_calls = false

# libusbsio is used by lpc55_upload, will be replaced eventually
[[tool.mypy.overrides]]
module = ["libusbsio.*"]
ignore_missing_imports = true
5 changes: 3 additions & 2 deletions src/nitrokey/nk3/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
from io import BytesIO
from typing import Any, Callable, Iterator, List, Optional

from spsdk.mboot.exceptions import McuBootConnectionError

from nitrokey._helpers import Retries
from nitrokey.nk3 import NK3, NK3Bootloader
from nitrokey.trussed import TimeoutException, TrussedBase, Version
Expand All @@ -25,6 +23,9 @@
Variant,
validate_firmware_image,
)
from nitrokey.trussed._bootloader.lpc55_upload.mboot.exceptions import (
McuBootConnectionError,
)
from nitrokey.trussed.admin_app import BootMode
from nitrokey.updates import Asset, Release

Expand Down
18 changes: 10 additions & 8 deletions src/nitrokey/trussed/_bootloader/lpc55.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@
import sys
from typing import Optional, TypeVar

from spsdk.mboot.interfaces.usb import MbootUSBInterface
from spsdk.mboot.mcuboot import McuBoot
from spsdk.mboot.properties import PropertyTag
from spsdk.sbfile.sb2.images import BootImageV21
from spsdk.utils.interfaces.device.usb_device import UsbDevice
from spsdk.utils.usbfilter import USBDeviceFilter

from nitrokey.trussed import Uuid, Version

from . import FirmwareMetadata, ProgressCallback, TrussedBootloader, Variant
from .lpc55_upload.mboot.interfaces.usb import MbootUSBInterface
from .lpc55_upload.mboot.mcuboot import McuBoot
from .lpc55_upload.mboot.properties import PropertyTag
from .lpc55_upload.sbfile.sb2.images import BootImageV21
from .lpc55_upload.utils.interfaces.device.usb_device import UsbDevice
from .lpc55_upload.utils.usbfilter import USBDeviceFilter

RKTH = bytes.fromhex("050aad3e77791a81e59c5b2ba5a158937e9460ee325d8ccba09734b8fdebb171")
KEK = bytes([0xAA] * 32)
Expand Down Expand Up @@ -135,7 +134,10 @@ def _open(cls: type[T], path: str) -> Optional[T]:

def parse_firmware_image(data: bytes) -> FirmwareMetadata:
image = BootImageV21.parse(data, kek=KEK)
version = Version.from_bcd_version(image.header.product_version)
bcd_version = image.header.product_version
version = Version(
major=bcd_version.major, minor=bcd_version.minor, patch=bcd_version.service
)
metadata = FirmwareMetadata(version=version)
if image.cert_block:
if image.cert_block.rkth == RKTH:
Expand Down
6 changes: 6 additions & 0 deletions src/nitrokey/trussed/_bootloader/lpc55_upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# LPC55 Bootloader Firmware Upload Module

Anything inside this directory is originally extracted from: https://github.com/nxp-mcuxpresso/spsdk/tree/master.
In detail anything that is needed to upload a signed firmware image to a Nitrokey 3 xN with an LPC55 MCU.


13 changes: 13 additions & 0 deletions src/nitrokey/trussed/_bootloader/lpc55_upload/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2019-2024 NXP
#
# SPDX-License-Identifier: BSD-3-Clause

version = "2.1.0"

__author__ = "NXP"
__license__ = "BSD-3-Clause"
__version__ = version
__release__ = "beta"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2020-2024 NXP
#
# SPDX-License-Identifier: BSD-3-Clause
"""Module for crypto operations (certificate and key management)."""
208 changes: 208 additions & 0 deletions src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2020-2024 NXP
#
# SPDX-License-Identifier: BSD-3-Clause
"""Module for certificate management (generating certificate, validating certificate, chains)."""

from typing import Optional

from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa

from ..crypto.hash import EnumHashAlgorithm
from ..crypto.keys import PublicKey, PublicKeyRsa
from ..crypto.types import (
SPSDKEncoding,
SPSDKExtensionOID,
SPSDKExtensions,
SPSDKName,
SPSDKVersion,
)
from ..exceptions import SPSDKError, SPSDKValueError
from ..utils.abstract import BaseClass
from ..utils.misc import align_block


class Certificate(BaseClass):
"""SPSDK Certificate representation."""

def __init__(self, certificate: x509.Certificate) -> None:
"""Constructor of SPSDK Certificate.
:param certificate: Cryptography Certificate representation.
"""
assert isinstance(certificate, x509.Certificate)
self.cert = certificate

def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes:
"""Convert certificates into bytes.
:param encoding: encoding type
:return: certificate in bytes form
"""
if encoding == SPSDKEncoding.NXP:
return align_block(self.export(SPSDKEncoding.DER), 4, "zeros")

return self.cert.public_bytes(
SPSDKEncoding.get_cryptography_encodings(encoding)
)

def get_public_key(self) -> PublicKey:
"""Get public keys from certificate.
:return: RSA public key
"""
pub_key = self.cert.public_key()
if isinstance(pub_key, rsa.RSAPublicKey):
return PublicKeyRsa(pub_key)

raise SPSDKError(f"Unsupported Certificate public key: {type(pub_key)}")

@property
def version(self) -> SPSDKVersion:
"""Returns the certificate version."""
return self.cert.version

@property
def signature(self) -> bytes:
"""Returns the signature bytes."""
return self.cert.signature

@property
def tbs_certificate_bytes(self) -> bytes:
"""Returns the tbsCertificate payload bytes as defined in RFC 5280."""
return self.cert.tbs_certificate_bytes

@property
def signature_hash_algorithm(
self,
) -> Optional[hashes.HashAlgorithm]:
"""Returns a HashAlgorithm corresponding to the type of the digest signed in the certificate."""
return self.cert.signature_hash_algorithm

@property
def extensions(self) -> SPSDKExtensions:
"""Returns an Extensions object."""
return self.cert.extensions

@property
def issuer(self) -> SPSDKName:
"""Returns the issuer name object."""
return self.cert.issuer

@property
def serial_number(self) -> int:
"""Returns certificate serial number."""
return self.cert.serial_number

@property
def subject(self) -> SPSDKName:
"""Returns the subject name object."""
return self.cert.subject

def validate(self, issuer_certificate: "Certificate") -> bool:
"""Validate certificate.
:param issuer_certificate: Issuer's certificate
:raises SPSDKError: Unsupported key type in Certificate
:return: true/false whether certificate is valid or not
"""
assert self.signature_hash_algorithm
return issuer_certificate.get_public_key().verify_signature(
self.signature,
self.tbs_certificate_bytes,
EnumHashAlgorithm.from_label(self.signature_hash_algorithm.name),
)

@property
def ca(self) -> bool:
"""Check if CA flag is set in certificate.
:return: true/false depending whether ca flag is set or not
"""
extension = self.extensions.get_extension_for_oid(
SPSDKExtensionOID.BASIC_CONSTRAINTS
)
return extension.value.ca # type: ignore # mypy can not handle property definition in cryptography

@property
def self_signed(self) -> bool:
"""Indication whether the Certificate is self-signed."""
return self.validate(self)

@property
def raw_size(self) -> int:
"""Raw size of the certificate."""
return len(self.export())

def public_key_hash(
self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256
) -> bytes:
"""Get key hash.
:param algorithm: Used hash algorithm, defaults to sha256
:return: Key Hash
"""
return self.get_public_key().key_hash(algorithm)

def __repr__(self) -> str:
"""Text short representation about the Certificate."""
return f"Certificate, SN:{hex(self.cert.serial_number)}"

def __str__(self) -> str:
"""Text information about the Certificate."""
not_valid_before = self.cert.not_valid_before.strftime("%d.%m.%Y (%H:%M:%S)")
not_valid_after = self.cert.not_valid_after.strftime("%d.%m.%Y (%H:%M:%S)")
nfo = ""
nfo += f" Certification Authority: {'YES' if self.ca else 'NO'}\n"
nfo += f" Serial Number: {hex(self.cert.serial_number)}\n"
nfo += f" Validity Range: {not_valid_before} - {not_valid_after}\n"
if self.signature_hash_algorithm:
nfo += (
f" Signature Algorithm: {self.signature_hash_algorithm.name}\n"
)
nfo += f" Self Issued: {'YES' if self.self_signed else 'NO'}\n"

return nfo

@classmethod
def parse(cls, data: bytes) -> "Certificate":
"""Deserialize object from bytes array.
:param data: Data to be parsed
:returns: Recreated certificate
"""

def load_der_certificate(data: bytes) -> x509.Certificate:
"""Load the DER certificate from bytes.
This function is designed to eliminate cryptography exception
when the padded data is provided.
:param data: Data with DER certificate
:return: Certificate (from cryptography library)
:raises SPSDKError: Unsupported certificate to load
"""
while True:
try:
return x509.load_der_x509_certificate(data)
except ValueError as exc:
if (
len(exc.args)
and "kind: ExtraData" in exc.args[0]
and data[-1:] == b"\00"
):
data = data[:-1]
else:
raise SPSDKValueError(str(exc)) from exc

try:
encoding = SPSDKEncoding.get_file_encodings(data)
if encoding != SPSDKEncoding.DER:
raise ValueError("Unsupported encoding: {encoding}")
return Certificate(load_der_certificate(data))
except ValueError as exc:
raise SPSDKError(f"Cannot load certificate: ({str(exc)})") from exc
Loading

0 comments on commit da1137e

Please sign in to comment.