From 507e4cfc0557ad96509a7452b5cdeee8d005bb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 20 Mar 2024 17:42:04 +0100 Subject: [PATCH 01/11] Vendor spsdk --- .../lpc55_upload/apps/utils/utils.py | 36 + .../lpc55_upload/crypto/__init__.py | 7 + .../lpc55_upload/crypto/certificate.py | 392 ++ .../bootloader/lpc55_upload/crypto/cmac.py | 42 + .../bootloader/lpc55_upload/crypto/cms.py | 161 + .../lpc55_upload/crypto/exceptions.py | 18 + .../bootloader/lpc55_upload/crypto/hash.py | 99 + .../bootloader/lpc55_upload/crypto/hmac.py | 51 + .../bootloader/lpc55_upload/crypto/keys.py | 1421 +++++++ .../bootloader/lpc55_upload/crypto/oscca.py | 144 + .../bootloader/lpc55_upload/crypto/rng.py | 40 + .../lpc55_upload/crypto/signature_provider.py | 426 ++ .../lpc55_upload/crypto/symmetric.py | 276 ++ .../bootloader/lpc55_upload/crypto/types.py | 67 + .../bootloader/lpc55_upload/crypto/utils.py | 91 + .../bootloader/lpc55_upload/ele/__init__.py | 8 + .../bootloader/lpc55_upload/ele/ele_comm.py | 267 ++ .../lpc55_upload/ele/ele_constants.py | 336 ++ .../lpc55_upload/ele/ele_message.py | 1525 +++++++ .../bootloader/lpc55_upload/exceptions.py | 94 + .../lpc55_upload/image/ahab/__init__.py | 8 + .../image/ahab/ahab_abstract_interfaces.py | 221 + .../lpc55_upload/image/ahab/ahab_container.py | 3699 +++++++++++++++++ .../lpc55_upload/image/ahab/signed_msg.py | 1556 +++++++ .../lpc55_upload/image/ahab/utils.py | 79 + .../bootloader/lpc55_upload/image/header.py | 193 + .../bootloader/lpc55_upload/image/misc.py | 107 + .../bootloader/lpc55_upload/image/secret.py | 932 +++++ .../bootloader/lpc55_upload/mboot/__init__.py | 28 + .../bootloader/lpc55_upload/mboot/commands.py | 521 +++ .../lpc55_upload/mboot/error_codes.py | 352 ++ .../lpc55_upload/mboot/exceptions.py | 59 + .../lpc55_upload/mboot/interfaces/__init__.py | 8 + .../lpc55_upload/mboot/interfaces/buspal.py | 528 +++ .../lpc55_upload/mboot/interfaces/sdio.py | 170 + .../lpc55_upload/mboot/interfaces/uart.py | 109 + .../lpc55_upload/mboot/interfaces/usb.py | 117 + .../lpc55_upload/mboot/interfaces/usbsio.py | 106 + .../bootloader/lpc55_upload/mboot/mcuboot.py | 1683 ++++++++ .../bootloader/lpc55_upload/mboot/memories.py | 226 + .../lpc55_upload/mboot/properties.py | 850 ++++ .../lpc55_upload/mboot/protocol/__init__.py | 8 + .../lpc55_upload/mboot/protocol/base.py | 16 + .../mboot/protocol/bulk_protocol.py | 118 + .../mboot/protocol/serial_protocol.py | 328 ++ .../bootloader/lpc55_upload/mboot/scanner.py | 97 + .../bootloader/lpc55_upload/sbfile/misc.py | 203 + .../lpc55_upload/sbfile/sb2/__init__.py | 8 + .../sbfile/sb2/bd_ebnf_grammar.txt | 174 + .../lpc55_upload/sbfile/sb2/bd_grammer.txt | 210 + .../lpc55_upload/sbfile/sb2/commands.py | 1030 +++++ .../lpc55_upload/sbfile/sb2/headers.py | 241 ++ .../lpc55_upload/sbfile/sb2/images.py | 1041 +++++ .../lpc55_upload/sbfile/sb2/sb_21_helper.py | 476 +++ .../lpc55_upload/sbfile/sb2/sections.py | 381 ++ .../lpc55_upload/sbfile/sb2/sly_bd_lexer.py | 366 ++ .../lpc55_upload/sbfile/sb2/sly_bd_parser.py | 1550 +++++++ .../bootloader/lpc55_upload/uboot/__init__.py | 7 + .../bootloader/lpc55_upload/uboot/uboot.py | 135 + .../bootloader/lpc55_upload/utils/__init__.py | 16 + .../bootloader/lpc55_upload/utils/abstract.py | 44 + .../lpc55_upload/utils/crypto/__init__.py | 8 + .../lpc55_upload/utils/crypto/cert_blocks.py | 1745 ++++++++ .../lpc55_upload/utils/crypto/iee.py | 803 ++++ .../lpc55_upload/utils/crypto/otfad.py | 940 +++++ .../lpc55_upload/utils/crypto/rkht.py | 287 ++ .../lpc55_upload/utils/crypto/rot.py | 218 + .../bootloader/lpc55_upload/utils/database.py | 833 ++++ .../lpc55_upload/utils/exceptions.py | 33 + .../bootloader/lpc55_upload/utils/images.py | 606 +++ .../lpc55_upload/utils/interfaces/__init__.py | 8 + .../lpc55_upload/utils/interfaces/commands.py | 34 + .../utils/interfaces/device/__init__.py | 8 + .../utils/interfaces/device/base.py | 75 + .../utils/interfaces/device/sdio_device.py | 269 ++ .../utils/interfaces/device/serial_device.py | 206 + .../utils/interfaces/device/usb_device.py | 208 + .../utils/interfaces/device/usbsio_device.py | 448 ++ .../utils/interfaces/protocol/__init__.py | 8 + .../interfaces/protocol/protocol_base.py | 130 + .../utils/interfaces/scanner_helper.py | 37 + .../bootloader/lpc55_upload/utils/misc.py | 915 ++++ .../bootloader/lpc55_upload/utils/plugins.py | 180 + .../lpc55_upload/utils/registers.py | 1323 ++++++ .../lpc55_upload/utils/schema_validator.py | 757 ++++ .../lpc55_upload/utils/spsdk_enum.py | 170 + .../lpc55_upload/utils/usbfilter.py | 0 87 files changed, 33751 insertions(+) create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/rng.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/exceptions.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/header.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_ebnf_grammar.txt create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_grammer.txt create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/abstract.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/exceptions.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/commands.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/base.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py new file mode 100644 index 00000000..4cc43b8f --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +def filepath_from_config( + config: Dict, + key: str, + default_value: str, + base_dir: str, + output_folder: str = "", + file_extension: str = ".bin", +) -> str: + """Get file path from configuration dictionary and append .bin if the value is not blank. + + Function returns the output_folder + filename if the filename does not contain path. + In case filename contains path, return filename and append ".bin". + The empty string "" indicates that the user doesn't want the output. + :param config: Configuration dictionary + :param key: Name of the key + :param default_value: default value in case key value is not present + :param base_dir: base directory for path expansion + :param output_folder: Output folder, if blank file path from config will be used + :param file_extension: File extension that will be appended + :return: filename with appended ".bin" or blank filename "" + """ + filename = config.get(key, default_value) + if filename == "": + return filename + if not os.path.dirname(filename): + filename = os.path.join(output_folder, filename) + if not filename.endswith(file_extension): + filename += file_extension + return get_abs_path(filename, base_dir) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/__init__.py new file mode 100644 index 00000000..cd69e9a0 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/__init__.py @@ -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).""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py new file mode 100644 index 00000000..5492ab2a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py @@ -0,0 +1,392 @@ +#!/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 datetime import datetime, timedelta +from typing import Dict, List, Optional, Union + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.x509.extensions import ExtensionNotFound +from typing_extensions import Self + +from spsdk.crypto.hash import EnumHashAlgorithm +from spsdk.crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from spsdk.crypto.types import ( + SPSDKEncoding, + SPSDKExtensionOID, + SPSDKExtensions, + SPSDKName, + SPSDKNameOID, + SPSDKObjectIdentifier, + SPSDKVersion, +) +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.abstract import BaseClass +from spsdk.utils.misc import align_block, load_binary, write_file + + +class SPSDKExtensionNotFoundError(SPSDKError, ExtensionNotFound): + """Extension not found error.""" + + +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 + + @staticmethod + def generate_certificate( + subject: x509.Name, + issuer: x509.Name, + subject_public_key: PublicKey, + issuer_private_key: PrivateKey, + serial_number: Optional[int] = None, + duration: Optional[int] = None, + extensions: Optional[List[x509.ExtensionType]] = None, + ) -> "Certificate": + """Generate certificate. + + :param subject: subject name that the CA issues the certificate to + :param issuer: issuer name that issued the certificate + :param subject_public_key: Public key of subject + :param issuer_private_key: Private key of issuer + :param serial_number: certificate serial number, if not specified, random serial number will be set + :param duration: how long the certificate will be valid (in days) + :param extensions: List of extensions to include in the certificate + :return: certificate + """ + before = datetime.utcnow() if duration else datetime(2000, 1, 1) + after = datetime.utcnow() + timedelta(days=duration) if duration else datetime(9999, 12, 31) + crt = x509.CertificateBuilder( + subject_name=subject, + issuer_name=issuer, + not_valid_before=before, + not_valid_after=after, + public_key=subject_public_key.key, + # we don't pass extensions directly, need to handle the "critical" flag + extensions=[], + serial_number=serial_number or x509.random_serial_number(), + ) + + if extensions: + for ext in extensions: + crt = crt.add_extension(ext, critical=True) + + return Certificate(crt.sign(issuer_private_key.key, hashes.SHA256())) + + def save( + self, + file_path: str, + encoding_type: SPSDKEncoding = SPSDKEncoding.PEM, + ) -> None: + """Save the certificate/CSR into file. + + :param file_path: path to the file where item will be stored + :param encoding_type: encoding type (PEM or DER) + """ + write_file(self.export(encoding_type), file_path, mode="wb") + + @classmethod + def load(cls, file_path: str) -> Self: + """Load the Certificate from the given file. + + :param file_path: path to the file, where the key is stored + """ + data = load_binary(file_path) + return cls.parse(data=data) + + 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) + if isinstance(pub_key, ec.EllipticCurvePublicKey): + return PublicKeyEcc(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 + + @property + def signature_algorithm_oid(self) -> SPSDKObjectIdentifier: + """Returns the ObjectIdentifier of the signature algorithm.""" + return self.cert.signature_algorithm_oid + + def validate_subject(self, subject_certificate: "Certificate") -> bool: + """Validate certificate. + + :param subject_certificate: Subject's certificate + :raises SPSDKError: Unsupported key type in Certificate + :return: true/false whether certificate is valid or not + """ + assert subject_certificate.signature_hash_algorithm + return self.get_public_key().verify_signature( + subject_certificate.signature, + subject_certificate.tbs_certificate_bytes, + EnumHashAlgorithm.from_label(subject_certificate.signature_hash_algorithm.name), + ) + + 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) -> Self: + """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: + cert = { + SPSDKEncoding.PEM: x509.load_pem_x509_certificate, + SPSDKEncoding.DER: load_der_certificate, + }[SPSDKEncoding.get_file_encodings(data)]( + data + ) # type: ignore + return Certificate(cert) # type: ignore + except ValueError as exc: + raise SPSDKError(f"Cannot load certificate: ({str(exc)})") from exc + + +def validate_certificate_chain(chain_list: List[Certificate]) -> List[bool]: + """Validate chain of certificates. + + :param chain_list: list of certificates in chain + :return: list of boolean values, which corresponds to the certificate validation in chain + :raises SPSDKError: When chain has less than two certificates + """ + if len(chain_list) <= 1: + raise SPSDKError("The chain must have at least two certificates") + result = [] + for i in range(len(chain_list) - 1): + result.append(chain_list[i].validate(chain_list[i + 1])) + return result + + +def validate_ca_flag_in_cert_chain(chain_list: List[Certificate]) -> bool: + """Validate CA flag in certification chain. + + :param chain_list: list of certificates in the chain + :return: true/false depending whether ca flag is set or not + """ + return chain_list[0].ca + + +X509NameConfig = Union[List[Dict[str, str]], Dict[str, Union[str, List[str]]]] + + +def generate_name(config: X509NameConfig) -> x509.Name: + """Generate x509 Name. + + :param config: subject/issuer description + :return: x509.Name + """ + attributes: List[x509.NameAttribute] = [] + + def _get_name_oid(name: str) -> x509.ObjectIdentifier: + try: + return getattr(SPSDKNameOID, name) + except Exception as exc: + raise SPSDKError(f"Invalid value of certificate attribute: {name}") from exc + + if isinstance(config, list): + for item in config: + for key, value in item.items(): + name_oid = _get_name_oid(key) + attributes.append(x509.NameAttribute(name_oid, str(value))) + + if isinstance(config, dict): + for key_second, value_second in config.items(): + name_oid = _get_name_oid(key_second) + if isinstance(value_second, list): + for value in value_second: + attributes.append(x509.NameAttribute(name_oid, str(value))) + else: + attributes.append(x509.NameAttribute(name_oid, str(value_second))) + + return x509.Name(attributes) + + +def generate_extensions(config: dict) -> List[x509.ExtensionType]: + """Get x509 extensions out of config data.""" + extensions: List[x509.ExtensionType] = [] + + for key, val in config.items(): + if key == "BASIC_CONSTRAINTS": + ca = bool(val["ca"]) + extensions.append( + x509.BasicConstraints(ca=ca, path_length=val.get("path_length") if ca else None) + ) + if key == "WPC_QIAUTH_POLICY": + extensions.append(WPCQiAuthPolicy(value=val["value"])) + if key == "WPC_QIAUTH_RSID": + extensions.append(WPCQiAuthRSID(value=val["value"])) + return extensions + + +class WPCQiAuthPolicy(x509.UnrecognizedExtension): + """WPC Qi Auth Policy x509 extension.""" + + oid = x509.ObjectIdentifier("2.23.148.1.1") + + def __init__(self, value: int) -> None: + """Initialize the extension with given policy number.""" + super().__init__( + oid=self.oid, + value=b"\x04\x04" + value.to_bytes(length=4, byteorder="big"), + ) + + +class WPCQiAuthRSID(x509.UnrecognizedExtension): + """WPC Qi Auth RSID x509 extension.""" + + oid = x509.ObjectIdentifier("2.23.148.1.2") + + def __init__(self, value: str) -> None: + """Initialize the extension with given RSID in form of a hex-string.""" + super().__init__( + oid=self.oid, + value=b"\x04\x09" + bytes.fromhex(value).zfill(9), + ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py new file mode 100644 index 00000000..08157087 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for CMAC packet authentication.""" + +# Used security modules +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import cmac as cmac_cls +from cryptography.hazmat.primitives.ciphers import algorithms + + +def cmac(key: bytes, data: bytes) -> bytes: + """Return a CMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :return: CMAC bytes + """ + cmac_obj = cmac_cls.CMAC(algorithm=algorithms.AES(key)) + cmac_obj.update(data) + return cmac_obj.finalize() + + +def cmac_validate(key: bytes, data: bytes, signature: bytes) -> bool: + """Return a CMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :param signature: CMAC signature to validate + :return: CMAC bytes + """ + cmac_obj = cmac_cls.CMAC(algorithm=algorithms.AES(key)) + cmac_obj.update(data) + try: + cmac_obj.verify(signature=signature) + return True + except InvalidSignature: + return False diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py new file mode 100644 index 00000000..2e50ad7f --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""ASN1Crypto implementation for CMS signature container.""" + + +# Used security modules +from datetime import datetime +from typing import Optional + +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash +from spsdk.crypto.keys import ECDSASignature, PrivateKey, PrivateKeyEcc, PrivateKeyRsa +from spsdk.crypto.signature_provider import SignatureProvider +from spsdk.crypto.types import SPSDKEncoding +from spsdk.exceptions import SPSDKError, SPSDKTypeError, SPSDKValueError + + +def cms_sign( + zulu: datetime, + data: bytes, + certificate: Certificate, + signing_key: Optional[PrivateKey], + signature_provider: Optional[SignatureProvider], +) -> bytes: + """Sign provided data and return CMS signature. + + :param zulu: current UTC time+date + :param data: to be signed + :param certificate: Certificate with issuer information + :param signing_key: Signing key, is mutually exclusive with signature_provider parameter + :param signature_provider: Signature provider, is mutually exclusive with signing_key parameter + :return: CMS signature (binary) + :raises SPSDKError: If certificate is not present + :raises SPSDKError: If private key is not present + :raises SPSDKError: If incorrect time-zone" + """ + # Lazy imports are used here to save some time during SPSDK startup + from asn1crypto import cms, util, x509 + + if certificate is None: + raise SPSDKValueError("Certificate is not present") + if not (signing_key or signature_provider): + raise SPSDKValueError("Private key or signature provider is not present") + if signing_key and signature_provider: + raise SPSDKValueError("Only one of private key and signature provider must be specified") + if signing_key and not isinstance(signing_key, (PrivateKeyEcc, PrivateKeyRsa)): + raise SPSDKTypeError(f"Unsupported private key type {type(signing_key)}.") + + # signed data (main section) + signed_data = cms.SignedData() + signed_data["version"] = "v1" + signed_data["encap_content_info"] = util.OrderedDict([("content_type", "data")]) + signed_data["digest_algorithms"] = [ + util.OrderedDict([("algorithm", "sha256"), ("parameters", None)]) + ] + + # signer info sub-section + signer_info = cms.SignerInfo() + signer_info["version"] = "v1" + signer_info["digest_algorithm"] = util.OrderedDict( + [("algorithm", "sha256"), ("parameters", None)] + ) + signer_info["signature_algorithm"] = ( + util.OrderedDict([("algorithm", "rsassa_pkcs1v15"), ("parameters", b"")]) + if (signing_key and isinstance(signing_key, PrivateKeyRsa)) + or (signature_provider and signature_provider.signature_length >= 256) + else util.OrderedDict([("algorithm", "sha256_ecdsa")]) + ) + # signed identifier: issuer amd serial number + + asn1_cert = x509.Certificate.load(certificate.export(SPSDKEncoding.DER)) + signer_info["sid"] = cms.SignerIdentifier( + { + "issuer_and_serial_number": cms.IssuerAndSerialNumber( + { + "issuer": asn1_cert.issuer, + "serial_number": asn1_cert.serial_number, + } + ) + } + ) + # signed attributes + signed_attrs = cms.CMSAttributes() + signed_attrs.append( + cms.CMSAttribute( + { + "type": "content_type", + "values": [cms.ContentType("data")], + } + ) + ) + + # check time-zone is assigned (expected UTC+0) + if not zulu.tzinfo: + raise SPSDKError("Incorrect time-zone") + signed_attrs.append( + cms.CMSAttribute( + { + "type": "signing_time", + "values": [cms.Time(name="utc_time", value=zulu.strftime("%y%m%d%H%M%SZ"))], + } + ) + ) + signed_attrs.append( + cms.CMSAttribute( + { + "type": "message_digest", + "values": [cms.OctetString(get_hash(data))], # digest + } + ) + ) + signer_info["signed_attrs"] = signed_attrs + + # create signature + data_to_sign = signed_attrs.dump() + signature = sign_data(data_to_sign, signing_key, signature_provider) + + signer_info["signature"] = signature + # Adding SignerInfo object to SignedData object + signed_data["signer_infos"] = [signer_info] + + # content info + content_info = cms.ContentInfo() + content_info["content_type"] = "signed_data" + content_info["content"] = signed_data + + return content_info.dump() + + +def sign_data( + data_to_sign: bytes, + signing_key: Optional[PrivateKey], + signature_provider: Optional[SignatureProvider], +) -> bytes: + """Sign the data. + + :param data_to_sign: Data to be signed + :param signing_key: Signing key, is mutually exclusive with signature_provider parameter + :param signature_provider: Signature provider, is mutually exclusive with signing_key parameter + """ + assert signing_key or signature_provider + if signing_key and signature_provider: + raise SPSDKValueError("Only one of private key and signature provider must be specified") + if signing_key: + return ( + signing_key.sign(data_to_sign, algorithm=EnumHashAlgorithm.SHA256, der_format=True) + if isinstance(signing_key, PrivateKeyEcc) + else signing_key.sign(data_to_sign) + ) + assert signature_provider + signature = signature_provider.get_signature(data_to_sign) + # convert to DER format + if signature_provider.signature_length < 256: + ecdsa_signature = ECDSASignature.parse(signature) + signature = ecdsa_signature.export(encoding=SPSDKEncoding.DER) + return signature diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py new file mode 100644 index 00000000..75f8f238 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Exceptions used in the Crypto module.""" + +from spsdk.exceptions import SPSDKError + + +class SPSDKPCryptoError(SPSDKError): + """General SPSDK Crypto Error.""" + + +class SPSDKKeysNotMatchingError(SPSDKPCryptoError): + """Key pair not matching error.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py new file mode 100644 index 00000000..7ead6079 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation Hash algorithms.""" + +# Used security modules + +from math import ceil + +from cryptography.hazmat.primitives import hashes + +from spsdk.exceptions import SPSDKError +from spsdk.utils.misc import Endianness +from spsdk.utils.spsdk_enum import SpsdkEnum + + +class EnumHashAlgorithm(SpsdkEnum): + """Hash algorithm enum.""" + + SHA1 = (0, "sha1", "SHA1") + SHA256 = (1, "sha256", "SHA256") + SHA384 = (2, "sha384", "SHA384") + SHA512 = (3, "sha512", "SHA512") + MD5 = (4, "md5", "MD5") + SM3 = (5, "sm3", "SM3") + + +def get_hash_algorithm(algorithm: EnumHashAlgorithm) -> hashes.HashAlgorithm: + """For specified name return hashes algorithm instance. + + :param algorithm: Algorithm type enum + :return: instance of algorithm class + :raises SPSDKError: If algorithm not found + """ + algo_cls = getattr(hashes, algorithm.label.upper(), None) # hack: get class object by name + if algo_cls is None: + raise SPSDKError(f"Unsupported algorithm: hashes.{algorithm.label.upper()}") + + return algo_cls() # pylint: disable=not-callable + + +def get_hash_length(algorithm: EnumHashAlgorithm) -> int: + """For specified name return hash binary length. + + :param algorithm: Algorithm type enum + :return: Hash length + :raises SPSDKError: If algorithm not found + """ + return get_hash_algorithm(algorithm).digest_size + + +class Hash: + """SPSDK Hash Class.""" + + def __init__(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> None: + """Initialize hash object. + + :param algorithm: Algorithm type enum, defaults to EnumHashAlgorithm.SHA256 + """ + self.hash_obj = hashes.Hash(get_hash_algorithm(algorithm)) + + def update(self, data: bytes) -> None: + """Update the hash by new data. + + :param data: Data to be hashed + """ + self.hash_obj.update(data) + + def update_int(self, value: int) -> None: + """Update the hash by new integer value as is. + + :param value: Integer value to be hashed + """ + data = value.to_bytes(length=ceil(value.bit_length() / 8), byteorder=Endianness.BIG.value) + self.update(data) + + def finalize(self) -> bytes: + """Finalize the hash and return the hash value. + + :returns: Computed hash + """ + return self.hash_obj.finalize() + + +def get_hash(data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + """Return a HASH from input data with specified algorithm. + + :param data: Input data in bytes + :param algorithm: Algorithm type enum + :return: Hash-ed bytes + :raises SPSDKError: If algorithm not found + """ + hash_obj = hashes.Hash(get_hash_algorithm(algorithm)) + hash_obj.update(data) + return hash_obj.finalize() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py new file mode 100644 index 00000000..e498916a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for HMAC packet authentication.""" + +from cryptography.exceptions import InvalidSignature + +# Used security modules +from cryptography.hazmat.primitives import hmac as hmac_cls + +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash_algorithm + + +def hmac(key: bytes, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + """Return a HMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :param algorithm: Algorithm type for HASH function (sha256, sha384, sha512, ...) + :return: HMAC bytes + """ + hmac_obj = hmac_cls.HMAC(key, get_hash_algorithm(algorithm)) + hmac_obj.update(data) + return hmac_obj.finalize() + + +def hmac_validate( + key: bytes, + data: bytes, + signature: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, +) -> bool: + """Return a HMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :param signature: HMAC signature to validate + :param algorithm: Algorithm type for HASH function (sha256, sha384, sha512, ...) + :return: HMAC bytes + """ + hmac_obj = hmac_cls.HMAC(key=key, algorithm=get_hash_algorithm(algorithm)) + hmac_obj.update(data) + try: + hmac_obj.verify(signature=signature) + return True + except InvalidSignature: + return False diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py new file mode 100644 index 00000000..63993fac --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py @@ -0,0 +1,1421 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module for key generation and saving keys to file.""" + +import abc +import getpass +import math +from enum import Enum +from typing import Any, Callable, Dict, Optional, Tuple, Union + +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa, utils +from cryptography.hazmat.primitives.serialization import ( + BestAvailableEncryption, + NoEncryption, + PrivateFormat, + PublicFormat, +) +from cryptography.hazmat.primitives.serialization import ( + load_der_private_key as crypto_load_der_private_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_der_public_key as crypto_load_der_public_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key as crypto_load_pem_private_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_pem_public_key as crypto_load_pem_public_key, +) +from typing_extensions import Self + +from spsdk.exceptions import SPSDKError, SPSDKNotImplementedError, SPSDKValueError +from spsdk.utils.abstract import BaseClass +from spsdk.utils.misc import Endianness, load_binary, write_file + +from .hash import EnumHashAlgorithm, get_hash, get_hash_algorithm +from .oscca import IS_OSCCA_SUPPORTED +from .rng import rand_below, random_hex +from .types import SPSDKEncoding + +if IS_OSCCA_SUPPORTED: + from asn1tools import DecodeError # pylint: disable=import-error + from gmssl import sm2 # pylint: disable=import-error + + from .oscca import SM2Encoder, sanitize_pem + + +def _load_pem_private_key(data: bytes, password: Optional[bytes]) -> Any: + """Load PEM Private key. + + :param data: key data + :param password: optional password + :raises SPSDKError: if the key cannot be decoded + :return: Key + """ + last_error: Exception + try: + return _crypto_load_private_key(SPSDKEncoding.PEM, data, password) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + if IS_OSCCA_SUPPORTED: + try: + key_data = sanitize_pem(data) + key_set = SM2Encoder().decode_private_key(data=key_data) + return sm2.CryptSM2(private_key=key_set.private, public_key=key_set.public) + except (SPSDKError, DecodeError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load PEM private key: {last_error}") + + +def _load_der_private_key(data: bytes, password: Optional[bytes]) -> Any: + """Load DER Private key. + + :param data: key data + :param password: optional password + :raises SPSDKError: if the key cannot be decoded + :return: Key + """ + last_error: Exception + try: + return _crypto_load_private_key(SPSDKEncoding.DER, data, password) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + if IS_OSCCA_SUPPORTED: + try: + key_set = SM2Encoder().decode_private_key(data=data) + return sm2.CryptSM2(private_key=key_set.private, public_key=key_set.public) + except (SPSDKError, DecodeError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load DER private key: {last_error}") + + +def _crypto_load_private_key( + encoding: SPSDKEncoding, data: bytes, password: Optional[bytes] +) -> Union[ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey]: + """Load Private key. + + :param encoding: Encoding of input data + :param data: Key data + :param password: Optional password + :raises SPSDKValueError: Unsupported encoding + :raises SPSDKWrongKeyPassphrase: Private key is encrypted and passphrase is incorrect + :raises SPSDKKeyPassphraseMissing: Private key is encrypted and passphrase is missing + :return: Key + """ + if encoding not in [SPSDKEncoding.DER, SPSDKEncoding.PEM]: + raise SPSDKValueError(f"Unsupported encoding: {encoding}") + crypto_load_function = { + SPSDKEncoding.DER: crypto_load_der_private_key, + SPSDKEncoding.PEM: crypto_load_pem_private_key, + }[encoding] + try: + private_key = crypto_load_function(data, password) + assert isinstance(private_key, (ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey)) + return private_key + except ValueError as exc: + if "Incorrect password" in exc.args[0]: + raise SPSDKWrongKeyPassphrase("Provided password was incorrect.") from exc + raise exc + except TypeError as exc: + if "Password was not given but private key is encrypted" in str(exc): + raise SPSDKKeyPassphraseMissing(str(exc)) from exc + raise exc + + +def _load_pem_public_key(data: bytes) -> Any: + """Load PEM Public key. + + :param data: key data + :raises SPSDKError: if the key cannot be decoded + :return: PublicKey + """ + last_error: Exception + try: + return crypto_load_pem_public_key(data) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + if IS_OSCCA_SUPPORTED: + try: + key_data = sanitize_pem(data) + public_key = SM2Encoder().decode_public_key(data=key_data) + return sm2.CryptSM2(private_key=None, public_key=public_key.public) + except (SPSDKError, DecodeError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load PEM public key: {last_error}") + + +def _load_der_public_key(data: bytes) -> Any: + """Load DER Public key. + + :param data: key data + :raises SPSDKError: if the key cannot be decoded + :return: PublicKey + """ + last_error: Exception + try: + return crypto_load_der_public_key(data) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + if IS_OSCCA_SUPPORTED: + try: + public_key = SM2Encoder().decode_public_key(data=data) + return sm2.CryptSM2(private_key=None, public_key=public_key.public) + except (SPSDKError, DecodeError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load DER private key: {last_error}") + + +class SPSDKInvalidKeyType(SPSDKError): + """Invalid Key Type.""" + + +class SPSDKKeyPassphraseMissing(SPSDKError): + """Passphrase for decryption of private key is missing.""" + + +class SPSDKWrongKeyPassphrase(SPSDKError): + """Passphrase for decryption of private key is wrong.""" + + +class PrivateKey(BaseClass, abc.ABC): + """SPSDK Private Key.""" + + key: Any + + @classmethod + @abc.abstractmethod + def generate_key(cls) -> Self: + """Generate SPSDK Key (private key). + + :return: SPSDK private key + """ + + @property + @abc.abstractmethod + def signature_size(self) -> int: + """Size of signature data.""" + + @property + @abc.abstractmethod + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + + @abc.abstractmethod + def get_public_key(self) -> "PublicKey": + """Generate public key. + + :return: Public key + """ + + @abc.abstractmethod + def verify_public_key(self, public_key: "PublicKey") -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return isinstance(obj, self.__class__) and self.get_public_key() == obj.get_public_key() + + def save( + self, + file_path: str, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.PEM, + ) -> None: + """Save the Private key to the given file. + + :param file_path: path to the file, where the key will be stored + :param password: password to private key; None to store without password + :param encoding: encoding type, default is PEM + """ + write_file(self.export(password=password, encoding=encoding), file_path, mode="wb") + + @classmethod + def load(cls, file_path: str, password: Optional[str] = None) -> Self: + """Load the Private key from the given file. + + :param file_path: path to the file, where the key is stored + :param password: password to private key; None to load without password + """ + data = load_binary(file_path) + return cls.parse(data=data, password=password) + + @abc.abstractmethod + def sign(self, data: bytes) -> bytes: + """Sign input data. + + :param data: Input data + :return: Signed data + """ + + @abc.abstractmethod + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export key into bytes in requested format. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :return: Byte representation of key + """ + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + try: + private_key = { + SPSDKEncoding.PEM: _load_pem_private_key, + SPSDKEncoding.DER: _load_der_private_key, + }[SPSDKEncoding.get_file_encodings(data)]( + data, password.encode("utf-8") if password else None + ) + if isinstance(private_key, (ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey)): + return cls.create(private_key) + if IS_OSCCA_SUPPORTED and isinstance(private_key, sm2.CryptSM2): + return cls.create(private_key) + except (ValueError, SPSDKInvalidKeyType) as exc: + raise SPSDKError(f"Cannot load private key: ({str(exc)})") from exc + raise SPSDKError(f"Unsupported private key: ({str(private_key)})") + + @classmethod + def create(cls, key: Any) -> Self: + """Create Private Key object. + + :param key: Supported private key. + :raises SPSDKInvalidKeyType: Unsupported private key given + :return: SPSDK Private Kye object + """ + SUPPORTED_KEYS = { + PrivateKeyEcc: ec.EllipticCurvePrivateKey, + PrivateKeyRsa: rsa.RSAPrivateKey, + } + if IS_OSCCA_SUPPORTED: + SUPPORTED_KEYS[PrivateKeySM2] = sm2.CryptSM2 + + for k, v in SUPPORTED_KEYS.items(): + if isinstance(key, v): + return k(key) + + raise SPSDKInvalidKeyType(f"Unsupported key type: {str(key)}") + + +class PublicKey(BaseClass, abc.ABC): + """SPSDK Public Key.""" + + key: Any + + @property + @abc.abstractmethod + def signature_size(self) -> int: + """Size of signature data.""" + + @property + @abc.abstractmethod + def public_numbers(self) -> Any: + """Public numbers.""" + + def save(self, file_path: str, encoding: SPSDKEncoding = SPSDKEncoding.PEM) -> None: + """Save the public key to the file. + + :param file_path: path to the file, where the key will be stored + :param encoding: encoding type, default is PEM + """ + write_file(data=self.export(encoding=encoding), path=file_path, mode="wb") + + @classmethod + def load(cls, file_path: str) -> Self: + """Load the Public key from the given file. + + :param file_path: path to the file, where the key is stored + """ + data = load_binary(file_path) + return cls.parse(data=data) + + @abc.abstractmethod + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :return: True if signature is valid, False otherwise + """ + + @abc.abstractmethod + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export key into bytes to requested format. + + :param encoding: encoding type, default is NXP + :return: Byte representation of key + """ + + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + public_key = { + SPSDKEncoding.PEM: _load_pem_public_key, + SPSDKEncoding.DER: _load_der_public_key, + }[SPSDKEncoding.get_file_encodings(data)](data) + if isinstance(public_key, (ec.EllipticCurvePublicKey, rsa.RSAPublicKey)): + return cls.create(public_key) + if IS_OSCCA_SUPPORTED and isinstance(public_key, sm2.CryptSM2): + return cls.create(public_key) + except (ValueError, SPSDKInvalidKeyType) as exc: + raise SPSDKError(f"Cannot load public key: ({str(exc)})") from exc + raise SPSDKError(f"Unsupported public key: ({str(public_key)})") + + def key_hash(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + """Get key hash. + + :param algorithm: Used hash algorithm, defaults to sha256 + :return: Key Hash + """ + return get_hash(self.export(), algorithm) + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return isinstance(obj, self.__class__) and self.public_numbers == obj.public_numbers + + @classmethod + def create(cls, key: Any) -> Self: + """Create Public Key object. + + :param key: Supported public key. + :raises SPSDKInvalidKeyType: Unsupported public key given + :return: SPSDK Public Kye object + """ + SUPPORTED_KEYS = { + PublicKeyEcc: ec.EllipticCurvePublicKey, + PublicKeyRsa: rsa.RSAPublicKey, + } + if IS_OSCCA_SUPPORTED: + SUPPORTED_KEYS[PublicKeySM2] = sm2.CryptSM2 + + for k, v in SUPPORTED_KEYS.items(): + if isinstance(key, v): + return k(key) + + raise SPSDKInvalidKeyType(f"Unsupported key type: {str(key)}") + + +# =================================================================================================== +# =================================================================================================== +# +# RSA Keys +# +# =================================================================================================== +# =================================================================================================== + + +class PrivateKeyRsa(PrivateKey): + """SPSDK Private Key.""" + + SUPPORTED_KEY_SIZES = [2048, 3072, 4096] + + key: rsa.RSAPrivateKey + + def __init__(self, key: rsa.RSAPrivateKey) -> None: + """Create SPSDK Key. + + :param key: Only RSA key is accepted + """ + self.key = key + + @classmethod + def generate_key(cls, key_size: int = 2048, exponent: int = 65537) -> Self: + """Generate SPSDK Key (private key). + + :param key_size: key size in bits; must be >= 512 + :param exponent: public exponent; must be >= 3 and odd + :return: SPSDK private key + """ + return cls( + rsa.generate_private_key( + public_exponent=exponent, + key_size=key_size, + ) + ) + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.key.key_size // 8 + + @property + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + return self.key.key_size + + def get_public_key(self) -> "PublicKeyRsa": + """Generate public key. + + :return: Public key + """ + return PublicKeyRsa(self.key.public_key()) + + def verify_public_key(self, public_key: PublicKey) -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + return self.get_public_key() == public_key + + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export the Private key to the bytes in requested encoding. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :returns: Private key in bytes + """ + enc = ( + BestAvailableEncryption(password=password.encode("utf-8")) + if password + else NoEncryption() + ) + return self.key.private_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), PrivateFormat.PKCS8, enc + ) + + def sign(self, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + """Sign input data. + + :param data: Input data + :param algorithm: Used algorithm + :return: Signed data + """ + signature = self.key.sign( + data=data, + padding=padding.PKCS1v15(), + algorithm=get_hash_algorithm(algorithm), + ) + return signature + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + key = super().parse(data=data, password=password) + if isinstance(key, PrivateKeyRsa): + return key + + raise SPSDKInvalidKeyType("Can't parse Rsa private key from given data") + + def __repr__(self) -> str: + return f"RSA{self.key_size} Private Key" + + def __str__(self) -> str: + """Object description in string format.""" + ret = f"RSA{self.key_size} Private key: \nd({hex(self.key.private_numbers().d)})" + return ret + + +class PublicKeyRsa(PublicKey): + """SPSDK Public Key.""" + + key: rsa.RSAPublicKey + + def __init__(self, key: rsa.RSAPublicKey) -> None: + """Create SPSDK Public Key. + + :param key: SPSDK Public Key data or file path + """ + self.key = key + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.key.key_size // 8 + + @property + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + return self.key.key_size + + @property + def public_numbers(self) -> rsa.RSAPublicNumbers: + """Public numbers of key. + + :return: Public numbers + """ + return self.key.public_numbers() + + @property + def e(self) -> int: + """Public number E. + + :return: E + """ + return self.public_numbers.e + + @property + def n(self) -> int: + """Public number N. + + :return: N + """ + return self.public_numbers.n + + def export( + self, + encoding: SPSDKEncoding = SPSDKEncoding.NXP, + exp_length: Optional[int] = None, + modulus_length: Optional[int] = None, + ) -> bytes: + """Save the public key to the bytes in NXP or DER format. + + :param encoding: encoding type, default is NXP + :param exp_length: Optional specific exponent length in bytes + :param modulus_length: Optional specific modulus length in bytes + :returns: Public key in bytes + """ + if encoding == SPSDKEncoding.NXP: + exp_rotk = self.e + mod_rotk = self.n + exp_length = exp_length or math.ceil(exp_rotk.bit_length() / 8) + modulus_length = modulus_length or math.ceil(mod_rotk.bit_length() / 8) + exp_rotk_bytes = exp_rotk.to_bytes(exp_length, Endianness.BIG.value) + mod_rotk_bytes = mod_rotk.to_bytes(modulus_length, Endianness.BIG.value) + return mod_rotk_bytes + exp_rotk_bytes + + return self.key.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), PublicFormat.PKCS1 + ) + + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :return: True if signature is valid, False otherwise + """ + try: + self.key.verify( + signature=signature, + data=data, + padding=padding.PKCS1v15(), + algorithm=get_hash_algorithm(algorithm), + ) + except InvalidSignature: + return False + + return True + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return isinstance(obj, self.__class__) and self.public_numbers == obj.public_numbers + + def __repr__(self) -> str: + return f"RSA{self.key_size} Public Key" + + def __str__(self) -> str: + """Object description in string format.""" + ret = f"RSA{self.key_size} Public key: \ne({hex(self.e)}) \nn({hex(self.n)})" + return ret + + @classmethod + def recreate(cls, exponent: int, modulus: int) -> Self: + """Recreate RSA public key from Exponent and modulus. + + :param exponent: Exponent of RSA key. + :param modulus: Modulus of RSA key. + :return: RSA public key. + """ + public_numbers = rsa.RSAPublicNumbers(e=exponent, n=modulus) + return cls(public_numbers.public_key()) + + @staticmethod + def recreate_public_numbers(data: bytes) -> rsa.RSAPublicNumbers: + """Recreate public numbers from data. + + :param data: Dat with raw key. + :raises SPSDKError: Un recognized data. + :return: RAS public numbers. + """ + data_len = len(data) + for key_size in PrivateKeyRsa.SUPPORTED_KEY_SIZES: + key_size_bytes = key_size // 8 + if key_size_bytes + 3 <= data_len <= key_size_bytes + 4: + n = int.from_bytes(data[:key_size_bytes], Endianness.BIG.value) + e = int.from_bytes(data[key_size_bytes:], Endianness.BIG.value) + return rsa.RSAPublicNumbers(e=e, n=n) + + raise SPSDKError(f"Unsupported RSA key to recreate with data size {data_len}") + + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + key = super().parse(data=data) + if isinstance(key, PublicKeyRsa): + return key + except SPSDKError: + public_numbers = PublicKeyRsa.recreate_public_numbers(data) + return PublicKeyRsa(public_numbers.public_key()) # type:ignore + + raise SPSDKInvalidKeyType("Can't parse RSA public key from given data") + + +# =================================================================================================== +# =================================================================================================== +# +# Elliptic Curves Keys +# +# =================================================================================================== +# =================================================================================================== + + +class EccCurve(str, Enum): + """Supported ecc key types.""" + + SECP256R1 = "secp256r1" + SECP384R1 = "secp384r1" + SECP521R1 = "secp521r1" + + +class SPSDKUnsupportedEccCurve(SPSDKValueError): + """Unsupported Ecc curve error.""" + + +class KeyEccCommon: + """SPSDK Common Key.""" + + key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] + + @property + def coordinate_size(self) -> int: + """Size of signature data.""" + return math.ceil(self.key.key_size / 8) + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.coordinate_size * 2 + + @property + def curve(self) -> EccCurve: + """Curve type.""" + return EccCurve(self.key.curve.name) + + @property + def key_size(self) -> int: + """Key size in bits.""" + return self.key.key_size + + @staticmethod + def _get_ec_curve_object(name: EccCurve) -> ec.EllipticCurve: + """Get the EC curve object by its name. + + :param name: Name of EC curve. + :return: EC curve object. + :raises SPSDKValueError: Invalid EC curve name. + """ + # pylint: disable=protected-access + for key_object in ec._CURVE_TYPES: + if key_object.lower() == name.lower(): + # pylint: disable=protected-access + return ec._CURVE_TYPES[key_object]() + + raise SPSDKValueError(f"The EC curve with name '{name}' is not supported.") + + @staticmethod + def serialize_signature(signature: bytes, coordinate_length: int) -> bytes: + """Re-format ECC ANS.1 DER signature into the format used by ROM code.""" + r, s = utils.decode_dss_signature(signature) + + r_bytes = r.to_bytes(coordinate_length, Endianness.BIG.value) + s_bytes = s.to_bytes(coordinate_length, Endianness.BIG.value) + return r_bytes + s_bytes + + +class PrivateKeyEcc(KeyEccCommon, PrivateKey): + """SPSDK Private Key.""" + + key: ec.EllipticCurvePrivateKey + + def __init__(self, key: ec.EllipticCurvePrivateKey) -> None: + """Create SPSDK Ecc Private Key. + + :param key: Only Ecc key is accepted + """ + self.key = key + + @classmethod + def generate_key(cls, curve_name: EccCurve = EccCurve.SECP256R1) -> Self: + """Generate SPSDK Key (private key). + + :param curve_name: Name of curve + :return: SPSDK private key + """ + curve_obj = cls._get_ec_curve_object(curve_name) + prv = ec.generate_private_key(curve_obj) + return cls(prv) + + def exchange(self, peer_public_key: "PublicKeyEcc") -> bytes: + """Exchange key using ECDH algorithm with provided peer public key. + + :param peer_public_key: Peer public key + :return: Shared key + """ + return self.key.exchange(algorithm=ec.ECDH(), peer_public_key=peer_public_key.key) + + def get_public_key(self) -> "PublicKeyEcc": + """Generate public key. + + :return: Public key + """ + return PublicKeyEcc(self.key.public_key()) + + def verify_public_key(self, public_key: PublicKey) -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + return self.get_public_key() == public_key + + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export the Private key to the bytes in requested format. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :returns: Private key in bytes + """ + return self.key.private_bytes( + encoding=SPSDKEncoding.get_cryptography_encodings(encoding), + format=PrivateFormat.PKCS8, + encryption_algorithm=BestAvailableEncryption(password.encode("utf-8")) + if password + else NoEncryption(), + ) + + def sign( + self, + data: bytes, + algorithm: Optional[EnumHashAlgorithm] = None, + der_format: bool = False, + prehashed: bool = False, + ) -> bytes: + """Sign input data. + + :param data: Input data + :param algorithm: Used algorithm + :param der_format: Use DER format as a output + :param prehashed: Use pre hashed value as input + :return: Signed data + """ + hash_name = ( + algorithm + or { + 256: EnumHashAlgorithm.SHA256, + 384: EnumHashAlgorithm.SHA384, + 521: EnumHashAlgorithm.SHA512, + }[self.key.key_size] + ) + if prehashed: + signature_algorithm = ec.ECDSA(utils.Prehashed(get_hash_algorithm(hash_name))) + else: + signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) + signature = self.key.sign(data, signature_algorithm) + + if der_format: + return signature + + return self.serialize_signature(signature, self.coordinate_size) + + @property + def d(self) -> int: + """Private number D.""" + return self.key.private_numbers().private_value + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + key = super().parse(data=data, password=password) + if isinstance(key, PrivateKeyEcc): + return key + + raise SPSDKInvalidKeyType("Can't parse Ecc private key from given data") + + @classmethod + def recreate(cls, d: int, curve: EccCurve) -> Self: + """Recreate ECC private key from private key number. + + :param d: Private number D. + :param curve: ECC curve. + + :return: ECC private key. + """ + key = ec.derive_private_key(d, cls._get_ec_curve_object(curve)) + return cls(key) + + def __repr__(self) -> str: + return f"ECC {self.curve} Private Key" + + def __str__(self) -> str: + """Object description in string format.""" + return f"ECC ({self.curve}) Private key: \nd({hex(self.d)})" + + +class PublicKeyEcc(KeyEccCommon, PublicKey): + """SPSDK Public Key.""" + + key: ec.EllipticCurvePublicKey + + def __init__(self, key: ec.EllipticCurvePublicKey) -> None: + """Create SPSDK Public Key. + + :param key: SPSDK Public Key data or file path + """ + self.key = key + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export the public key to the bytes in requested format. + + :param encoding: encoding type, default is NXP + :returns: Public key in bytes + """ + if encoding == SPSDKEncoding.NXP: + x_bytes = self.x.to_bytes(self.coordinate_size, Endianness.BIG.value) + y_bytes = self.y.to_bytes(self.coordinate_size, Endianness.BIG.value) + return x_bytes + y_bytes + + return self.key.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), + PublicFormat.SubjectPublicKeyInfo, + ) + + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: Optional[EnumHashAlgorithm] = None, + prehashed: bool = False, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :param prehashed: Use pre hashed value as input + :return: True if signature is valid, False otherwise + """ + coordinate_size = math.ceil(self.key.key_size / 8) + hash_name = ( + algorithm + or { + 256: EnumHashAlgorithm.SHA256, + 384: EnumHashAlgorithm.SHA384, + 521: EnumHashAlgorithm.SHA512, + }[self.key.key_size] + ) + + if prehashed: + signature_algorithm = ec.ECDSA(utils.Prehashed(get_hash_algorithm(hash_name))) + else: + signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) + + if len(signature) == self.signature_size: + der_signature = utils.encode_dss_signature( + int.from_bytes(signature[:coordinate_size], byteorder=Endianness.BIG.value), + int.from_bytes(signature[coordinate_size:], byteorder=Endianness.BIG.value), + ) + else: + der_signature = signature + try: + # pylint: disable=no-value-for-parameter # pylint is mixing RSA and ECC verify methods + self.key.verify(der_signature, data, signature_algorithm) + return True + except InvalidSignature: + return False + + @property + def public_numbers(self) -> ec.EllipticCurvePublicNumbers: + """Public numbers of key. + + :return: Public numbers + """ + return self.key.public_numbers() + + @property + def x(self) -> int: + """Public number X. + + :return: X + """ + return self.public_numbers.x + + @property + def y(self) -> int: + """Public number Y. + + :return: Y + """ + return self.public_numbers.y + + @classmethod + def recreate(cls, coor_x: int, coor_y: int, curve: EccCurve) -> Self: + """Recreate ECC public key from coordinates. + + :param coor_x: X coordinate of point on curve. + :param coor_y: Y coordinate of point on curve. + :param curve: ECC curve. + :return: ECC public key. + """ + pub_numbers = ec.EllipticCurvePublicNumbers( + x=coor_x, y=coor_y, curve=PrivateKeyEcc._get_ec_curve_object(curve) + ) + key = pub_numbers.public_key() + return cls(key) + + @classmethod + def recreate_from_data(cls, data: bytes, curve: Optional[EccCurve] = None) -> Self: + """Recreate ECC public key from coordinates in data blob. + + :param data: Data blob of coordinates in bytes (X,Y in Big Endian) + :param curve: ECC curve. + :return: ECC public key. + """ + + def get_curve(data_length: int, curve: Optional[EccCurve] = None) -> Tuple[EccCurve, bool]: + curve_list = [curve] if curve else list(EccCurve) + for cur in curve_list: + curve_obj = KeyEccCommon._get_ec_curve_object(EccCurve(cur)) + curve_sign_size = math.ceil(curve_obj.key_size / 8) * 2 + # Check raw binary format + if curve_sign_size == data_length: + return (cur, False) + # Check DER binary format + curve_sign_size += 7 + if curve_sign_size <= data_length <= curve_sign_size + 2: + return (cur, True) + raise SPSDKUnsupportedEccCurve(f"Cannot recreate ECC curve with {data_length} length") + + data_length = len(data) + (curve, der_format) = get_curve(data_length, curve) + + if der_format: + der = _load_der_public_key(data) + assert isinstance(der, ec.EllipticCurvePublicKey) + return cls(der) + + coordinate_length = data_length // 2 + coor_x = int.from_bytes(data[:coordinate_length], byteorder=Endianness.BIG.value) + coor_y = int.from_bytes(data[coordinate_length:], byteorder=Endianness.BIG.value) + return cls.recreate(coor_x=coor_x, coor_y=coor_y, curve=curve) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + key = super().parse(data=data) + if isinstance(key, PublicKeyEcc): + return key + except SPSDKError: + return cls.recreate_from_data(data=data) + + raise SPSDKInvalidKeyType("Can't parse ECC public key from given data") + + def __repr__(self) -> str: + return f"ECC {self.curve} Public Key" + + def __str__(self) -> str: + """Object description in string format.""" + return f"ECC ({self.curve}) Public key: \nx({hex(self.x)}) \ny({hex(self.y)})" + + +# =================================================================================================== +# =================================================================================================== +# +# SM2 Key +# +# =================================================================================================== +# =================================================================================================== +if IS_OSCCA_SUPPORTED: + from .oscca import SM2Encoder, SM2KeySet, SM2PublicKey, sanitize_pem + + class PrivateKeySM2(PrivateKey): + """SPSDK SM2 Private Key.""" + + key: sm2.CryptSM2 + + def __init__(self, key: sm2.CryptSM2) -> None: + """Create SPSDK Key. + + :param key: Only SM2 key is accepted + """ + if not isinstance(key, sm2.CryptSM2): + raise SPSDKInvalidKeyType("The input key is not SM2 type") + self.key = key + + @classmethod + def generate_key(cls) -> Self: + """Generate SM2 Key (private key). + + :return: SM2 private key + """ + key = sm2.CryptSM2(None, "None") + n = int(key.ecc_table["n"], base=16) + prk = rand_below(n) + while True: + puk = key._kg(prk, key.ecc_table["g"]) + if puk[:2] != "04": # PUK cannot start with 04 + break + key.private_key = f"{prk:064x}" + key.public_key = puk + + return cls(key) + + def get_public_key(self) -> "PublicKeySM2": + """Generate public key. + + :return: Public key + """ + return PublicKeySM2(sm2.CryptSM2(private_key=None, public_key=self.key.public_key)) + + def verify_public_key(self, public_key: PublicKey) -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + return self.get_public_key() == public_key + + def sign(self, data: bytes, salt: Optional[str] = None, use_ber: bool = False) -> bytes: + """Sign data using SM2 algorithm with SM3 hash. + + :param data: Data to sign. + :param salt: Salt for signature generation, defaults to None. If not specified a random string will be used. + :param use_ber: Encode signature into BER format, defaults to True + :raises SPSDKError: Signature can't be created. + :return: SM2 signature. + """ + data_hash = bytes.fromhex(self.key._sm3_z(data)) + if salt is None: + salt = random_hex(self.key.para_len // 2) + signature_str = self.key.sign(data=data_hash, K=salt) + if not signature_str: + raise SPSDKError("Can't sign data") + signature = bytes.fromhex(signature_str) + if use_ber: + ber_signature = SM2Encoder().encode_signature(signature) + return ber_signature + return signature + + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Convert key into bytes supported by NXP.""" + if encoding != SPSDKEncoding.DER: + raise SPSDKNotImplementedError("Only DER enocding is supported for SM2 keys export") + keys = SM2KeySet(self.key.private_key, self.key.public_key) + return SM2Encoder().encode_private_key(keys) + + def __repr__(self) -> str: + return "SM2 Private Key" + + def __str__(self) -> str: + """Object description in string format.""" + return f"SM2Key(private_key={self.key.private_key}, public_key='{self.key.public_key}')" + + @property + def key_size(self) -> int: + """Size of the key in bits.""" + return self.key.para_len + + @property + def signature_size(self) -> int: + """Signature size.""" + return 64 + + class PublicKeySM2(PublicKey): + """SM2 Public Key.""" + + key: sm2.CryptSM2 + + def __init__(self, key: sm2.CryptSM2) -> None: + """Create SPSDK Public Key. + + :param key: SPSDK Public Key data or file path + """ + if not isinstance(key, sm2.CryptSM2): + raise SPSDKInvalidKeyType("The input key is not SM2 type") + self.key = key + + def verify_signature( + self, signature: bytes, data: bytes, algorithm: Optional[EnumHashAlgorithm] = None + ) -> bool: + """Verify signature. + + :param signature: SM2 signature to verify + :param data: Signed data + :param algorithm: Just to keep compatibility with abstract class + :raises SPSDKError: Invalid signature + """ + # Check if the signature is BER formatted + if len(signature) > 64 and signature[0] == 0x30: + signature = SM2Encoder().decode_signature(signature) + # Otherwise the signature is in raw format r || s + data_hash = bytes.fromhex(self.key._sm3_z(data)) + return self.key.verify(Sign=signature.hex(), data=data_hash) + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.DER) -> bytes: + """Convert key into bytes supported by NXP. + + :return: Byte representation of key + """ + if encoding != SPSDKEncoding.DER: + raise SPSDKNotImplementedError("Only DER enocding is supported for SM2 keys export") + keys = SM2PublicKey(self.key.public_key) + return SM2Encoder().encode_public_key(keys) + + @property + def signature_size(self) -> int: + """Signature size.""" + return 64 + + @property + def public_numbers(self) -> str: + """Public numbers of key. + + :return: Public numbers + """ + return self.key.public_key + + @classmethod + def recreate(cls, data: bytes) -> Self: + """Recreate SM2 public key from data. + + :param data: public key data + :return: SPSDK public key. + """ + return cls(sm2.CryptSM2(private_key=None, public_key=data.hex())) + + @classmethod + def recreate_from_data(cls, data: bytes) -> Self: + """Recreate SM2 public key from data. + + :param data: PEM or DER encoded key. + :return: SM2 public key. + """ + key_data = sanitize_pem(data) + public_key = SM2Encoder().decode_public_key(data=key_data) + return cls(sm2.CryptSM2(private_key=None, public_key=public_key.public)) + + def __repr__(self) -> str: + return "SM2 Public Key" + + def __str__(self) -> str: + """Object description in string format.""" + ret = f"SM2 Public Key <{self.public_numbers}>" + return ret + +else: + # In case the OSCCA is not installed, do this to avoid import errors + PrivateKeySM2 = PrivateKey # type: ignore + PublicKeySM2 = PublicKey # type: ignore + + +class ECDSASignature: + """ECDSA Signature.""" + + COORDINATE_LENGTHS = {EccCurve.SECP256R1: 32, EccCurve.SECP384R1: 48, EccCurve.SECP521R1: 66} + + def __init__(self, r: int, s: int, ecc_curve: EccCurve) -> None: + """ECDSA Signature constructor. + + :param r: r value of signature + :param s: s value of signature + :param ecc_curve: ECC Curve enum + """ + self.r = r + self.s = s + self.ecc_curve = ecc_curve + + @classmethod + def parse(cls, signature: bytes) -> Self: + """Parse signature in DER or NXP format. + + :param signature: Signature binary + """ + encoding = cls.get_encoding(signature) + if encoding == SPSDKEncoding.DER: + r, s = utils.decode_dss_signature(signature) + ecc_curve = cls.get_ecc_curve(len(signature)) + return cls(r, s, ecc_curve) + if encoding == SPSDKEncoding.NXP: + r = int.from_bytes(signature[: len(signature) // 2], Endianness.BIG.value) + s = int.from_bytes(signature[len(signature) // 2 :], Endianness.BIG.value) + ecc_curve = cls.get_ecc_curve(len(signature)) + return cls(r, s, ecc_curve) + raise SPSDKValueError(f"Invalid signature encoding {encoding.value}") + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export signature in DER or NXP format. + + :param encoding: Signature encoding + :return: Signature as bytes + """ + if encoding == SPSDKEncoding.NXP: + r_bytes = self.r.to_bytes(self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value) + s_bytes = self.s.to_bytes(self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value) + return r_bytes + s_bytes + if encoding == SPSDKEncoding.DER: + return utils.encode_dss_signature(self.r, self.s) + raise SPSDKValueError(f"Invalid signature encoding {encoding.value}") + + @classmethod + def get_encoding(cls, signature: bytes) -> SPSDKEncoding: + """Get encoding of signature. + + :param signature: Signature + """ + signature_length = len(signature) + # Try detect the NXP format by data length + if signature_length // 2 in cls.COORDINATE_LENGTHS.values(): + return SPSDKEncoding.NXP + # Try detect the DER format by decode of header + try: + utils.decode_dss_signature(signature) + return SPSDKEncoding.DER + except ValueError: + pass + raise SPSDKValueError( + f"The given signature with length {signature_length} does not match any encoding" + ) + + @classmethod + def get_ecc_curve(cls, signature_length: int) -> EccCurve: + """Get the Elliptic Curve of signature. + + :param signature_length: Signature length + """ + for curve, coord_len in cls.COORDINATE_LENGTHS.items(): + if signature_length == coord_len * 2: + return curve + if signature_length in range(coord_len * 2 + 3, coord_len * 2 + 9): + return curve + raise SPSDKValueError( + f"The given signature with length {signature_length} does not match any ecc curve" + ) + + +# # =================================================================================================== +# # =================================================================================================== +# # +# # General section +# # +# # =================================================================================================== +# # =================================================================================================== + +GeneratorParams = Dict[str, Union[int, str, bool]] +KeyGeneratorInfo = Dict[str, Tuple[Callable[..., PrivateKey], GeneratorParams]] + + +def get_supported_keys_generators() -> KeyGeneratorInfo: + """Generate list with list of supported key types. + + :return: `KeyGeneratorInfo` dictionary of supported key types. + """ + ret: KeyGeneratorInfo = { + # RSA keys + "rsa2048": (PrivateKeyRsa.generate_key, {"key_size": 2048}), + "rsa3072": (PrivateKeyRsa.generate_key, {"key_size": 3072}), + "rsa4096": (PrivateKeyRsa.generate_key, {"key_size": 4096}), + # ECC keys + "secp256r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp256r1"}), + "secp384r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp384r1"}), + "secp521r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp521r1"}), + } + if IS_OSCCA_SUPPORTED: + ret["sm2"] = (PrivateKeySM2.generate_key, {}) + + return ret + + +def get_ecc_curve(key_length: int) -> EccCurve: + """Get curve name for Crypto library. + + :param key_length: Length of ecc key in bytes + """ + if key_length <= 32 or key_length == 64: + return EccCurve.SECP256R1 + if key_length <= 48 or key_length == 96: + return EccCurve.SECP384R1 + if key_length <= 66: + return EccCurve.SECP521R1 + raise SPSDKError(f"Not sure what curve corresponds to {key_length} data") + + +def prompt_for_passphrase() -> str: + """Prompt interactively for private key passphrase.""" + password = getpass.getpass(prompt="Private key is encrypted. Enter password: ", stream=None) + return password diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py new file mode 100644 index 00000000..9de1d8a2 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Support for OSCCA SM2/SM3.""" + +from spsdk import SPSDK_DATA_FOLDER_COMMON +from spsdk.utils.misc import Endianness + +try: + # this import is to find out whether OSCCA support is installed or not + # pylint: disable=unused-import + import gmssl + + IS_OSCCA_SUPPORTED = True +except ImportError: + IS_OSCCA_SUPPORTED = False + + +if IS_OSCCA_SUPPORTED: + import base64 + import os + from typing import Any, NamedTuple, Optional, Type, TypeVar + + from ..exceptions import SPSDKError + + OSCCA_ASN_DEFINITION_FILE = os.path.join(SPSDK_DATA_FOLDER_COMMON, "crypto", "oscca.asn") + SM2_OID = "1.2.156.10197.1.301" + + class SM2KeySet(NamedTuple): + """Bare-bone representation of a SM2 Key.""" + + private: str + public: Optional[str] + + class SM2PublicKey(NamedTuple): + """Bare-bone representation of a SM2 Public Key.""" + + public: str + + _T = TypeVar("_T") + + def singleton(class_: Type[_T]) -> Type[_T]: + """Decorator providing Singleton functionality for classes.""" + instances = {} + + def getinstance(*args: Any, **kwargs: Any) -> _T: + # args/kwargs should be part of cache key + if class_ not in instances: + instances[class_] = class_(*args, **kwargs) + return instances[class_] + + return getinstance # type: ignore # why are we even using Mypy?! + + @singleton + class SM2Encoder: + """ASN1 Encoder/Decoder for SM2 keys and signature.""" + + def __init__(self, asn_file: str = OSCCA_ASN_DEFINITION_FILE) -> None: + """Create ASN encoder/decoder based on provided ASN file.""" + try: + import asn1tools + except ImportError as import_error: + raise SPSDKError( + "asn1tools package is missing, " + "please install it with pip install 'spsdk[oscca]' in order to use OSCCA" + ) from import_error + + self.parser = asn1tools.compile_files(asn_file) + + def decode_private_key(self, data: bytes) -> SM2KeySet: + """Parse private SM2 key set from binary data.""" + result = self.parser.decode("Private", data) + key_set = self.parser.decode("KeySet", result["keyset"]) + return SM2KeySet(private=key_set["prk"].hex(), public=key_set["puk"][0][1:].hex()) + + def decode_public_key(self, data: bytes) -> SM2PublicKey: + """Parse public SM2 key set from binary data.""" + result = self.parser.decode("Public", data) + return SM2PublicKey(public=result["puk"][0][1:].hex()) + + def encode_private_key(self, keys: SM2KeySet) -> bytes: + """Encode private SM2 key set from keyset.""" + assert isinstance(keys.public, str) + puk_array = bytearray(bytes.fromhex(keys.public)) + puk_array[0:0] = b"\x04" # 0x4 must be prepended + puk = (puk_array, 520) # tuple contains 520 + keyset = self.parser.encode( + "KeySet", + data={ + "number": 1, + "prk": bytes.fromhex(keys.private), + "puk": puk, + }, + ) + private_key = {"number": 0, "ids": [SM2_OID, SM2_OID], "keyset": keyset} + return self.parser.encode("Private", data=private_key) + + def encode_public_key(self, key: SM2PublicKey) -> bytes: + """Encode public SM2 key from SM2PublicKey.""" + puk_array = bytearray(bytes.fromhex(key.public)) + puk_array[0:0] = b"\x04" # 0x4 must be prepended + puk = (puk_array, 520) # tuple contains 520 + data = {"ids": [SM2_OID, SM2_OID], "puk": puk} + return self.parser.encode("Public", data=data) + + def decode_signature(self, data: bytes) -> bytes: + """Decode BER signature into r||s coordinates.""" + result = self.parser.decode("Signature", data) + r = int.to_bytes(result["r"], length=32, byteorder=Endianness.BIG.value) + s = int.to_bytes(result["s"], length=32, byteorder=Endianness.BIG.value) + return r + s + + def encode_signature(self, data: bytes) -> bytes: + """Encode raw r||s signature into BER format.""" + if len(data) != 64: + raise SPSDKError("SM2 signature must be 64B long.") + r = int.from_bytes(data[:32], byteorder=Endianness.BIG.value) + s = int.from_bytes(data[32:], byteorder=Endianness.BIG.value) + ber_signature = self.parser.encode("Signature", data={"r": r, "s": s}) + return ber_signature + + def sanitize_pem(data: bytes) -> bytes: + """Covert PEM data into DER.""" + if b"---" not in data: + return data + + capture_data = False + base64_data = b"" + for line in data.splitlines(keepends=False): + if capture_data: + base64_data += line + # PEM data may contain EC PARAMS, thus capture trigger should be the word KEY + if b"KEY" in line: + capture_data = not capture_data + # in the end the `capture_data` flag should be false singaling propper END * KEY + # and we should have some data + if capture_data is False and len(base64_data) > 0: + der_data = base64.b64decode(base64_data) + return der_data + raise SPSDKError("PEM data are corrupted") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/rng.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/rng.py new file mode 100644 index 00000000..f03a1ed4 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/rng.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Implementation for getting random numbers.""" + +# Used security modules + + +from secrets import randbelow, token_bytes, token_hex + + +def random_bytes(length: int) -> bytes: + """Return a random byte string with specified length. + + :param length: The length in bytes + :return: Random bytes + """ + return token_bytes(length) + + +def random_hex(length: int) -> str: + """Return a random hex string with specified length. + + :param length: The length in bytes + :return: Random hex + """ + return token_hex(length) + + +def rand_below(upper_bound: int) -> int: + """Return a random number in range [0, upper_bound]. + + :param upper_bound: Upper bound + :return: Random number + """ + return randbelow(upper_bound) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py new file mode 100644 index 00000000..b10cc161 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""SignatureProvider is an Interface for all potential signature providers. + +Each concrete signature provider needs to implement: +- sign(data: bytes) -> bytes +- signature_length -> int +- into() -> str +""" + +import abc +import json +import logging +from types import ModuleType +from typing import Any, Dict, List, Optional, Tuple, Type, Union + +import requests +from cryptography.hazmat.primitives.hashes import HashAlgorithm + +from spsdk.crypto.exceptions import SPSDKKeysNotMatchingError +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash_algorithm +from spsdk.crypto.keys import ( + ECDSASignature, + PrivateKey, + PrivateKeyEcc, + PrivateKeyRsa, + PrivateKeySM2, + PublicKeyEcc, + PublicKeyRsa, + PublicKeySM2, + SPSDKKeyPassphraseMissing, + prompt_for_passphrase, +) +from spsdk.crypto.types import SPSDKEncoding +from spsdk.exceptions import SPSDKError, SPSDKKeyError, SPSDKUnsupportedOperation, SPSDKValueError +from spsdk.utils.misc import find_file +from spsdk.utils.plugins import PluginsManager, PluginType + +logger = logging.getLogger(__name__) + + +class SignatureProvider(abc.ABC): + """Abstract class (Interface) for all signature providers.""" + + # Subclasses override the following signature provider type + sp_type = "INVALID" + reserved_keys = ["type", "search_paths"] + + @abc.abstractmethod + def sign(self, data: bytes) -> bytes: + """Return signature for data.""" + + @property + @abc.abstractmethod + def signature_length(self) -> int: + """Return length of the signature.""" + + def verify_public_key(self, public_key: bytes) -> bool: + """Verify if given public key matches private key.""" + raise SPSDKUnsupportedOperation("Verify method is not supported.") + + def get_signature(self, data: bytes) -> bytes: + """Get signature. In case of ECC signature, the NXP format(r+s) is used. + + :param data: Data to be signed. + :return: Signature of the data + + """ + signature = self.sign(data) + try: + ecdsa_sig = ECDSASignature.parse(signature) + signature = ecdsa_sig.export(SPSDKEncoding.NXP) + except SPSDKValueError: + pass # Not an ECC signature + if len(signature) != self.signature_length: + logger.warning( + f"Signature has unexpected length: {len(signature)}. Expected length: {self.signature_length}" + ) + return signature + + def info(self) -> str: + """Provide information about the Signature provider.""" + return self.__class__.__name__ + + @staticmethod + def convert_params(params: str) -> Dict[str, str]: + """Coverts creation params from string into dictionary. + + e.g.: "type=file;file_path=some_path" -> {'type': 'file', 'file_path': 'some_path'} + :param params: Params in the mentioned format. + :raises: SPSDKKeyError: Duplicate key found. + :raises: SPSDKValueError: Parameter must meet the following pattern: type=file;file_path=some_path. + :return: Converted dictionary of parameters. + """ + result: Dict[str, str] = {} + try: + for p in params.split(";"): + key, value = p.split("=") + + # Check for duplicate keys + if key in result: + raise SPSDKKeyError(f"Duplicate key found: {key}") + + result[key] = value + + except ValueError as e: + raise SPSDKValueError( + "Parameter must meet the following pattern: type=file;file_path=some_path" + ) from e + + return result + + @classmethod + def get_types(cls) -> List[str]: + """Returns a list of all available signature provider types.""" + return [sub_class.sp_type for sub_class in cls.__subclasses__()] + + @classmethod + def filter_params(cls, klass: Any, params: Dict[str, str]) -> Dict[str, str]: + """Remove unused parameters from the given dictionary based on the class constructor. + + :param klass: Signature provider class. + :param params: Dictionary of parameters. + :return: Filtered dictionary of parameters. + """ + unused_params = set(params) - set(klass.__init__.__code__.co_varnames) + for key in cls.reserved_keys: + if key in unused_params: + del params[key] + return params + + @classmethod + def create(cls, params: Union[str, dict]) -> Optional["SignatureProvider"]: + """Creates an concrete instance of signature provider.""" + load_plugins() + if isinstance(params, str): + params = cls.convert_params(params) + sp_classes = cls.get_all_signature_providers() + for klass in sp_classes: # pragma: no branch # there always be at least one subclass + if klass.sp_type == params["type"]: + klass.filter_params(klass, params) + return klass(**params) + + logger.info(f"Signature provider of type {params['type']} was not found.") + return None + + @staticmethod + def get_all_signature_providers() -> List[Type["SignatureProvider"]]: + """Get list of all available signature providers.""" + + def get_subclasses( + base_class: Type, + ) -> List[Type["SignatureProvider"]]: + """Recursively find all subclasses.""" + subclasses = [] + for subclass in base_class.__subclasses__(): + subclasses.append(subclass) + subclasses.extend(get_subclasses(subclass)) + return subclasses + + return get_subclasses(SignatureProvider) + + +class PlainFileSP(SignatureProvider): + """PlainFileSP is a SignatureProvider implementation that uses plain local files.""" + + sp_type = "file" + + def __init__( + self, + file_path: str, + password: Optional[str] = None, + hash_alg: Optional[EnumHashAlgorithm] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Initialize the plain file signature provider. + + :param file_path: Path to private file + :param password: Password in case of encrypted private file, defaults to None + :param hash_alg: Hash for the signature, defaults to None + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid Private Key + """ + self.file_path = find_file(file_path=file_path, search_paths=search_paths) + self.private_key = PrivateKey.load(self.file_path, password=password) + self.hash_alg = self._get_hash_algorithm(hash_alg) + + def _get_hash_algorithm(self, hash_alg: Optional[EnumHashAlgorithm] = None) -> HashAlgorithm: + if hash_alg: + hash_alg_name = hash_alg + else: + if isinstance(self.private_key, PrivateKeyRsa): + hash_alg_name = EnumHashAlgorithm.SHA256 + + elif isinstance(self.private_key, PrivateKeyEcc): + # key_size <= 256 => SHA256 + # 256 < key_size <= 384 => SHA384 + # 384 < key_size => SHA512 + if self.private_key.key_size <= 256: + hash_size = 256 + elif 256 < self.private_key.key_size <= 384: + hash_size = 384 + else: + hash_size = 512 + hash_alg_name = EnumHashAlgorithm.from_label(f"sha{hash_size}") + + elif isinstance(self.private_key, PrivateKeySM2): + hash_alg_name = EnumHashAlgorithm.SM3 + else: + raise SPSDKError( + f"Unsupported private key by signature provider: {str(self.private_key)}" + ) + return get_hash_algorithm(hash_alg_name) + + @property + def signature_length(self) -> int: + """Return length of the signature.""" + return self.private_key.signature_size + + def verify_public_key(self, public_key: bytes) -> bool: + """Verify if given public key matches private key.""" + try: + return self.private_key.verify_public_key(PublicKeyEcc.parse(public_key)) + except SPSDKError: + pass + try: + return self.private_key.verify_public_key(PublicKeyRsa.parse(public_key)) + except SPSDKError: + pass + try: + return self.private_key.verify_public_key(PublicKeySM2.parse(public_key)) + except SPSDKError: + pass + raise SPSDKError("Unsupported public key") + + def info(self) -> str: + """Return basic into about the signature provider.""" + msg = super().info() + msg += f"\nKey path: {self.file_path}\n" + return msg + + def sign(self, data: bytes) -> bytes: + """Return the signature for data.""" + return self.private_key.sign(data) + + +class InteractivePlainFileSP(PlainFileSP): + """SignatureProvider implementation that uses plain local file in an "interactive" mode. + + If the private key is encrypted, the user will be prompted for password + """ + + sp_type = "interactive_file" + + def __init__( # pylint: disable=super-init-not-called + self, + file_path: str, + hash_alg: Optional[EnumHashAlgorithm] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Initialize the interactive plain file signature provider. + + :param file_path: Path to private file + :param hash_alg: Hash for the signature, defaults to sha256 + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid Private Key + """ + self.file_path = find_file(file_path=file_path, search_paths=search_paths) + try: + self.private_key = PrivateKey.load(self.file_path) + except SPSDKKeyPassphraseMissing: + password = prompt_for_passphrase() + self.private_key = PrivateKey.load(self.file_path, password=password) + self.hash_alg = self._get_hash_algorithm(hash_alg) + + +class HttpProxySP(SignatureProvider): + """Signature Provider implementation that delegates all operations to a proxy server.""" + + sp_type = "proxy" + reserved_keys = ["type", "search_paths", "data"] + + def __init__( + self, + host: str = "localhost", + port: str = "8000", + url_prefix: str = "api", + **kwargs: Dict[str, str], + ) -> None: + """Initialize Http Proxy Signature Provider. + + :param host: Hostname (IP address) of the proxy server, defaults to "localhost" + :param port: Port of the proxy server, defaults to "8000" + :param url_prefix: REST API prefix, defaults to "api" + """ + self.base_url = f"http://{host}:{port}/" + self.base_url += f"{url_prefix}/" if url_prefix else "" + self.kwargs = kwargs + + def _handle_request(self, url: str, data: Optional[Dict] = None) -> Dict: + """Handle REST API request. + + :param url: REST API endpoint URL + :param data: JSON payload data, defaults to None + :raises SPSDKError: HTTP Error during API request + :raises SPSDKError: Invalid response data (not a valid dictionary) + :return: REST API data response as dictionary + """ + json_payload = data or {} + json_payload.update(self.kwargs) + full_url = self.base_url + url + logger.info(f"Requesting: {full_url}") + response = requests.get(url=full_url, json=json_payload, timeout=60) + logger.info(f"Response: {response}") + if not response.ok: + try: + extra_message = response.json() + except json.JSONDecodeError: + extra_message = "N/A" + raise SPSDKError( + f"Error {response.status_code} ({response.reason}) occurred when calling {full_url}\n" + f"Extra response data: {extra_message}" + ) + try: + return response.json() + except json.JSONDecodeError as e: + raise SPSDKError("Response is not a valid JSON object") from e + + def _check_response(self, response: Dict, names_types: List[Tuple[str, Type]]) -> None: + """Check if the response contains required data. + + :param response: Response to check + :param names_types: Name and type of required response members + :raises SPSDKError: Response doesn't contain required member + :raises SPSDKError: Responses' member has incorrect type + """ + for name, typ in names_types: + if name not in response: + raise SPSDKError(f"Response object doesn't contain member '{name}'") + if not isinstance(response[name], typ): + raise SPSDKError( + f"Response member '{name}' is not a instance of '{typ}' but '{type(response[name])}'" + ) + + def sign(self, data: bytes) -> bytes: + """Return signature for data.""" + response = self._handle_request("sign", {"data": data.hex()}) + self._check_response(response=response, names_types=[("data", str)]) + return bytes.fromhex(response["data"]) + + @property + def signature_length(self) -> int: + """Return length of the signature.""" + response = self._handle_request("signature_length") + self._check_response(response=response, names_types=[("data", int)]) + return int(response["data"]) + + def verify_public_key(self, public_key: bytes) -> bool: + """Verify if given public key matches private key.""" + response = self._handle_request("verify_public_key", {"data": public_key.hex()}) + self._check_response(response=response, names_types=[("data", bool)]) + return response["data"] + + +def get_signature_provider( + sp_cfg: Optional[str] = None, local_file_key: Optional[str] = None, **kwargs: Any +) -> SignatureProvider: + """Get the signature provider from configuration. + + :param sp_cfg: Configuration of signature provider. + :param local_file_key: Optional backward compatibility + option to specify just path to local private key. + :param kwargs: Additional parameters, that could be accepted by Signature providers. + :return: Signature Provider instance. + :raises SPSDKError: Invalid input configuration. + """ + if sp_cfg: + params: Dict[str, Union[str, List[str]]] = {} + params.update(SignatureProvider.convert_params(sp_cfg)) + for k, v in kwargs.items(): + if not k in params: + params[k] = v + signature_provider = SignatureProvider.create(params=params) + elif local_file_key: + signature_provider = InteractivePlainFileSP( + file_path=local_file_key, + search_paths=kwargs.get("search_paths"), + ) + else: + raise SPSDKValueError("No signature provider configuration is provided") + + if not signature_provider: + raise SPSDKError(f"Cannot create signature provider from: {sp_cfg or local_file_key}") + + return signature_provider + + +def load_plugins() -> Dict[str, ModuleType]: + """Load all installed signature provider plugins.""" + plugins_manager = PluginsManager() + plugins_manager.load_from_entrypoints(PluginType.SIGNATURE_PROVIDER.label) + return plugins_manager.plugins + + +def try_to_verify_public_key(signature_provider: SignatureProvider, public_key_data: bytes) -> None: + """Verify public key by signature provider if verify method is implemented. + + :param signature_provider: Signature provider used for verification. + :param public_key_data: Public key data to be verified. + :raises SPSDKUnsupportedOperation: The verify_public_key method si nto implemented + :raises SPSDKError: The verification of key-pair integrity failed + """ + try: + result = signature_provider.verify_public_key(public_key_data) + if not result: + raise SPSDKKeysNotMatchingError( + "Signature verification failed, public key does not match to private key" + ) + logger.debug("The verification of private key pair integrity has been successful.") + except SPSDKUnsupportedOperation: + logger.warning("Signature provider could not verify the integrity of private key pair.") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py new file mode 100644 index 00000000..8d652d10 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for symmetric key encryption.""" + + +# Used security modules +from typing import Optional + +from cryptography.hazmat.primitives import keywrap +from cryptography.hazmat.primitives.ciphers import Cipher, aead, algorithms, modes + +from spsdk.exceptions import SPSDKError +from spsdk.utils.misc import Endianness, align_block + + +class Counter: + """AES counter with specified counter byte ordering and customizable increment.""" + + @property + def value(self) -> bytes: + """Initial vector for AES encryption.""" + return self._nonce + self._ctr.to_bytes(4, self._ctr_byteorder_encoding.value) + + def __init__( + self, + nonce: bytes, + ctr_value: Optional[int] = None, + ctr_byteorder_encoding: Endianness = Endianness.LITTLE, + ): + """Constructor. + + :param nonce: last four bytes are used as initial value for counter + :param ctr_value: counter initial value; it is added to counter value retrieved from nonce + :param ctr_byteorder_encoding: way how the counter is encoded into output value + :raises SPSDKError: When invalid byteorder is provided + """ + assert isinstance(nonce, bytes) and len(nonce) == 16 + self._nonce = nonce[:-4] + self._ctr_byteorder_encoding = ctr_byteorder_encoding + self._ctr = int.from_bytes(nonce[-4:], ctr_byteorder_encoding.value) + if ctr_value is not None: + self._ctr += ctr_value + + def increment(self, value: int = 1) -> None: + """Increment counter by specified value. + + :param value: to add to counter + """ + self._ctr += value + + +def aes_key_wrap(kek: bytes, key_to_wrap: bytes) -> bytes: + """Wraps a key using a key-encrypting key (KEK). + + :param kek: The key-encrypting key + :param key_to_wrap: Plain data + :return: Wrapped key + """ + return keywrap.aes_key_wrap(kek, key_to_wrap) + + +def aes_key_unwrap(kek: bytes, wrapped_key: bytes) -> bytes: + """Unwraps a key using a key-encrypting key (KEK). + + :param kek: The key-encrypting key + :param wrapped_key: Encrypted data + :return: Un-wrapped key + """ + return keywrap.aes_key_unwrap(kek, wrapped_key) + + +def aes_ecb_encrypt(key: bytes, plain_data: bytes) -> bytes: + """Encrypt plain data with AES in ECB mode. + + :param key: The key for data encryption + :param plain_data: Input data + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.ECB()) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_ecb_decrypt(key: bytes, encrypted_data: bytes) -> bytes: + """Decrypt encrypted data with AES in ECB mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.ECB()) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None) -> bytes: + """Encrypt plain data with AES in CBC mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Encrypted image + """ + if len(key) * 8 not in algorithms.AES.key_sizes: + raise SPSDKError( + "The key must be a valid AES key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.AES.block_size // 8) + if len(init_vector) * 8 != algorithms.AES.block_size: + raise SPSDKError(f"The initial vector length must be {algorithms.AES.block_size // 8}") + cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) + enc = cipher.encryptor() + return ( + enc.update(align_block(plain_data, alignment=algorithms.AES.block_size // 8)) + + enc.finalize() + ) + + +def aes_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None) -> bytes: + """Decrypt encrypted data with AES in CBC mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Decrypted image + """ + if len(key) * 8 not in algorithms.AES.key_sizes: + raise SPSDKError( + "The key must be a valid AES key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.AES.block_size) + if len(init_vector) * 8 != algorithms.AES.block_size: + raise SPSDKError(f"The initial vector length must be {algorithms.AES.block_size}") + cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) + dec = cipher.decryptor() + return dec.update(encrypted_data) + dec.finalize() + + +def aes_ctr_encrypt(key: bytes, plain_data: bytes, nonce: bytes) -> bytes: + """Encrypt plain data with AES in CTR mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param nonce: Nonce data with counter value + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.CTR(nonce)) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_ctr_decrypt(key: bytes, encrypted_data: bytes, nonce: bytes) -> bytes: + """Decrypt encrypted data with AES in CTR mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param nonce: Nonce data with counter value + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.CTR(nonce)) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_xts_encrypt(key: bytes, plain_data: bytes, tweak: bytes) -> bytes: + """Encrypt plain data with AES in XTS mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param tweak: The tweak is a 16 byte value + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.XTS(tweak)) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_xts_decrypt(key: bytes, encrypted_data: bytes, tweak: bytes) -> bytes: + """Decrypt encrypted data with AES in XTS mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param tweak: The tweak is a 16 byte value + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.XTS(tweak)) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_ccm_encrypt( + key: bytes, plain_data: bytes, nonce: bytes, associated_data: bytes = b"", tag_len: int = 16 +) -> bytes: + """Encrypt plain data with AES in CCM mode (Counter with CBC). + + :param key: The key for data encryption + :param plain_data: Input data + :param nonce: Nonce data with counter value + :param associated_data: Associated data - Unencrypted but authenticated + :param tag_len: Length of encryption tag + :return: Encrypted data + """ + aesccm = aead.AESCCM(key, tag_length=tag_len) + return aesccm.encrypt(nonce, plain_data, associated_data) + + +def aes_ccm_decrypt( + key: bytes, encrypted_data: bytes, nonce: bytes, associated_data: bytes, tag_len: int = 16 +) -> bytes: + """Decrypt encrypted data with AES in CCM mode (Counter with CBC). + + :param key: The key for data decryption + :param encrypted_data: Input data + :param nonce: Nonce data with counter value + :param associated_data: Associated data - Unencrypted but authenticated + :param tag_len: Length of encryption tag + :return: Decrypted data + """ + aesccm = aead.AESCCM(key, tag_length=tag_len) + return aesccm.decrypt(nonce, encrypted_data, associated_data) + + +def sm4_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None) -> bytes: + """Encrypt plain data with SM4 in CBC mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Encrypted image + """ + if len(key) * 8 not in algorithms.SM4.key_sizes: + raise SPSDKError( + "The key must be a valid SM4 key length: " + f"{', '.join([str(k) for k in algorithms.SM4.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.SM4.block_size // 8) + if len(init_vector) * 8 != algorithms.SM4.block_size: + raise SPSDKError(f"The initial vector length must be {algorithms.SM4.block_size // 8}") + cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) + enc = cipher.encryptor() + return ( + enc.update(align_block(plain_data, alignment=algorithms.SM4.block_size // 8)) + + enc.finalize() + ) + + +def sm4_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None) -> bytes: + """Decrypt encrypted data with SM4 in CBC mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Decrypted image + """ + if len(key) * 8 not in algorithms.SM4.key_sizes: + raise SPSDKError( + "The key must be a valid SM4 key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.SM4.block_size) + if len(init_vector) * 8 != algorithms.SM4.block_size: + raise SPSDKError(f"The initial vector length must be {algorithms.SM4.block_size}") + cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) + dec = cipher.decryptor() + return dec.update(encrypted_data) + dec.finalize() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py new file mode 100644 index 00000000..a48c3095 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Based crypto classes.""" +from typing import Dict + +from cryptography import utils +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509.base import Version +from cryptography.x509.extensions import ExtensionOID, Extensions, KeyUsage +from cryptography.x509.name import Name, NameOID, ObjectIdentifier + +from spsdk.exceptions import SPSDKError + + +class SPSDKEncoding(utils.Enum): + """Extension of cryptography Encoders class.""" + + NXP = "NXP" + PEM = "PEM" + DER = "DER" + + @staticmethod + def get_cryptography_encodings(encoding: "SPSDKEncoding") -> Encoding: + """Get Encoding in cryptography class.""" + cryptography_encoding = { + SPSDKEncoding.PEM: Encoding.PEM, + SPSDKEncoding.DER: Encoding.DER, + }.get(encoding) + if cryptography_encoding is None: + raise SPSDKError(f"{encoding} format is not supported by cryptography.") + return cryptography_encoding + + @staticmethod + def get_file_encodings(data: bytes) -> "SPSDKEncoding": + """Get the encoding type out of given item from the data. + + :param data: Already loaded data file to determine the encoding style + :return: encoding type (Encoding.PEM, Encoding.DER) + """ + encoding = SPSDKEncoding.PEM + try: + decoded = data.decode("utf-8") + except UnicodeDecodeError: + encoding = SPSDKEncoding.DER + else: + if decoded.find("----") == -1: + encoding = SPSDKEncoding.DER + return encoding + + @staticmethod + def all() -> Dict[str, "SPSDKEncoding"]: + """Get all supported encodings.""" + return {"NXP": SPSDKEncoding.NXP, "PEM": SPSDKEncoding.PEM, "DER": SPSDKEncoding.DER} + + +SPSDKExtensions = Extensions +SPSDKExtensionOID = ExtensionOID +SPSDKNameOID = NameOID +SPSDKKeyUsage = KeyUsage +SPSDKName = Name +SPSDKVersion = Version +SPSDKObjectIdentifier = ObjectIdentifier diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py new file mode 100644 index 00000000..dd9a7118 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for security backend.""" + +from typing import Iterable, List, Optional + +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.keys import PrivateKey, PublicKey +from spsdk.crypto.signature_provider import SignatureProvider +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.misc import load_binary + + +def get_matching_key_id(public_keys: List[PublicKey], signature_provider: SignatureProvider) -> int: + """Get index of public key that match to given private key. + + :param public_keys: List of public key used to find the match for the private key. + :param signature_provider: Signature provider used to try to match public key index. + :raises SPSDKValueError: No match found. + :return: Index of public key. + """ + for i, public_key in enumerate(public_keys): + if signature_provider.verify_public_key(public_key.export()): + return i + + raise SPSDKValueError("There is no match of private key in given list.") + + +def extract_public_key_from_data(object_data: bytes, password: Optional[str] = None) -> PublicKey: + """Extract any kind of public key from a data that contains Certificate, Private Key or Public Key. + + :raises SPSDKError: Raised when file can not be loaded + :return: private key of any type + """ + try: + return Certificate.parse(object_data).get_public_key() + except SPSDKError: + pass + + try: + return PrivateKey.parse( + object_data, password=password if password else None + ).get_public_key() + except SPSDKError: + pass + + try: + return PublicKey.parse(object_data) + except SPSDKError as exc: + raise SPSDKError("Unable to load secret data.") from exc + + +def extract_public_key( + file_path: str, password: Optional[str] = None, search_paths: Optional[List[str]] = None +) -> PublicKey: + """Extract any kind of public key from a file that contains Certificate, Private Key or Public Key. + + :param file_path: File path to public key file. + :param password: Optional password for encrypted Private file source. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Raised when file can not be loaded + :return: Public key of any type + """ + try: + object_data = load_binary(file_path, search_paths=search_paths) + return extract_public_key_from_data(object_data, password) + except SPSDKError as exc: + raise SPSDKError(f"Unable to load secret file '{file_path}'.") from exc + + +def extract_public_keys( + secret_files: Iterable[str], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, +) -> List[PublicKey]: + """Extract any kind of public key from files that contain Certificate, Private Key or Public Key. + + :param secret_files: List of file paths to public key files. + :param password: Optional password for encrypted Private file source. + :param search_paths: List of paths where to search for the file, defaults to None + :return: List of public keys of any type + """ + return [ + extract_public_key(file_path=source, password=password, search_paths=search_paths) + for source in secret_files + ] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py new file mode 100644 index 00000000..2dfaaee1 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This module contains support for EdgeLock Enclave Tool.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py new file mode 100644 index 00000000..d2b940f4 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""EdgeLock Enclave Message handler.""" + +import logging +import re +from abc import abstractmethod +from types import TracebackType +from typing import List, Optional, Tuple, Type, Union + +from spsdk.ele.ele_constants import ResponseStatus +from spsdk.ele.ele_message import EleMessage +from spsdk.exceptions import SPSDKError, SPSDKLengthError +from spsdk.mboot.mcuboot import McuBoot +from spsdk.uboot.uboot import Uboot +from spsdk.utils.database import DatabaseManager, get_db, get_families +from spsdk.utils.misc import value_to_bytes + +logger = logging.getLogger(__name__) + + +class EleMessageHandler: + """Base class for ELE message handling.""" + + def __init__( + self, device: Union[McuBoot, Uboot], family: str, revision: str = "latest" + ) -> None: + """Class object initialized. + + :param device: Communication interface. + :param family: Target family name. + :param revision: Target revision, default is use 'latest' revision. + """ + self.device = device + self.database = get_db(device=family, revision=revision) + self.family = family + self.revision = revision + self.comm_buff_addr = self.database.get_int(DatabaseManager.COMM_BUFFER, "address") + self.comm_buff_size = self.database.get_int(DatabaseManager.COMM_BUFFER, "size") + logger.info( + f"ELE communicator is using {self.comm_buff_size} B size buffer at " + f"{self.comm_buff_addr:08X} address in {family} target." + ) + + @staticmethod + def get_supported_families() -> List[str]: + """Get list of supported target families. + + :return: List of supported families. + """ + return get_families(DatabaseManager.ELE) + + @staticmethod + def get_ele_device(device: str) -> str: + """Get default ELE device from DB.""" + return get_db(device, "latest").get_str(DatabaseManager.ELE, "ele_device") + + @abstractmethod + def send_message(self, msg: EleMessage) -> None: + """Send message and receive response. + + :param msg: EdgeLock Enclave message + """ + + def __enter__(self) -> None: + """Enter function of ELE handler.""" + if not self.device.is_opened: + self.device.open() + + def __exit__( + self, + exception_type: Optional[Type[BaseException]] = None, + exception_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + """Close function of ELE handler.""" + if self.device.is_opened: + self.device.close() + + +class EleMessageHandlerMBoot(EleMessageHandler): + """EdgeLock Enclave Message Handler over MCUBoot. + + This class can send the ELE message into target over mBoot and decode the response. + """ + + def __init__(self, device: McuBoot, family: str, revision: str = "latest") -> None: + """Class object initialized. + + :param device: mBoot device. + :param family: Target family name. + :param revision: Target revision, default is use 'latest' revision. + """ + if not isinstance(device, McuBoot): + raise SPSDKError("Wrong instance of device, must be MCUBoot") + super().__init__(device, family, revision) + + def send_message(self, msg: EleMessage) -> None: + """Send message and receive response. + + :param msg: EdgeLock Enclave message + :raises SPSDKError: Invalid response status detected. + :raises SPSDKLengthError: Invalid read back length detected. + """ + if not isinstance(self.device, McuBoot): + raise SPSDKError("Wrong instance of device, must be MCUBoot") + msg.set_buffer_params(self.comm_buff_addr, self.comm_buff_size) + try: + # 1. Prepare command in target memory + self.device.write_memory(msg.command_address, msg.export()) + + # 1.1. Prepare command data in target memory if required + if msg.has_command_data: + self.device.write_memory(msg.command_data_address, msg.command_data) + + # 2. Execute ELE message on target + self.device.ele_message( + msg.command_address, + msg.command_words_count, + msg.response_address, + msg.response_words_count, + ) + if msg.response_words_count == 0: + return + # 3. Read back the response + response = self.device.read_memory(msg.response_address, 4 * msg.response_words_count) + except SPSDKError as exc: + raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + + if not response or len(response) != 4 * msg.response_words_count: + raise SPSDKLengthError("ELE Message - Invalid response read-back operation.") + # 4. Decode the response + msg.decode_response(response) + + # 4.1 Check the response status + if msg.status != ResponseStatus.ELE_SUCCESS_IND: + raise SPSDKError(f"ELE Message failed. \n{msg.info()}") + + # 4.2 Read back the response data from target memory if required + if msg.has_response_data: + try: + response_data = self.device.read_memory( + msg.response_data_address, msg.response_data_size + ) + except SPSDKError as exc: + raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + + if not response_data or len(response_data) != msg.response_data_size: + raise SPSDKLengthError("ELE Message - Invalid response data read-back operation.") + + msg.decode_response_data(response_data) + + logger.info(f"Sent message information:\n{msg.info()}") + + +class EleMessageHandlerUBoot(EleMessageHandler): + """EdgeLock Enclave Message Handler over UBoot. + + This class can send the ELE message into target over UBoot and decode the response. + """ + + def __init__(self, device: Uboot, family: str, revision: str = "latest") -> None: + """Class object initialized. + + :param device: UBoot device. + :param family: Target family name. + :param revision: Target revision, default is use 'latest' revision. + """ + if not isinstance(device, Uboot): + raise SPSDKError("Wrong instance of device, must be UBoot") + super().__init__(device, family, revision) + + def extract_error_values(self, error_message: str) -> Tuple[int, int, int]: + """Extract error values from error_mesage. + + :param error_message: Error message containing ret and response + :return: abort_code, status and indication + """ + # Define regular expressions to extract values + ret_pattern = re.compile(r"ret (0x[0-9a-fA-F]+),") + response_pattern = re.compile(r"response (0x[0-9a-fA-F]+)") + + # Find matches in the error message + ret_match = ret_pattern.search(error_message) + response_match = response_pattern.search(error_message) + + if not ret_match or not response_match: + logger.error(f"Cannot decode error message from ELE!\n{error_message}") + abort_code = 0 + status = 0 + indication = 0 + else: + abort_code = int(ret_match.group(1), 16) + status_all = int(response_match.group(1), 16) + indication = status_all >> 8 + status = status_all & 0xFF + return abort_code, status, indication + + def send_message(self, msg: EleMessage) -> None: + """Send message and receive response. + + :param msg: EdgeLock Enclave message + :raises SPSDKError: Invalid response status detected. + :raises SPSDKLengthError: Invalid read back length detected. + """ + if not isinstance(self.device, Uboot): + raise SPSDKError("Wrong instance of device, must be UBoot") + msg.set_buffer_params(self.comm_buff_addr, self.comm_buff_size) + + try: + logger.debug(f"ELE msg {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}") + + # 0. Prepare command data in target memory if required + if msg.has_command_data: + self.device.write_memory(msg.command_data_address, msg.command_data) + + # 1. Execute ELE message on target + self.device.write( + f"ele_message {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}" + ) + output = self.device.read_output() + logger.debug(f"Raw ELE message output:\n{output}") + + if msg.response_words_count == 0: + return + + if "Error" in output: + msg.abort_code, msg.status, msg.indication = self.extract_error_values(output) + else: + # 2. Read back the response + stripped_output = output.splitlines()[-1].replace("u-boot=> ", "") + logger.debug(f"Stripped output {stripped_output}") + response = value_to_bytes("0x" + stripped_output) + except (SPSDKError, IndexError) as exc: + raise SPSDKError(f"ELE Communication failed with UBoot: {str(exc)}") from exc + + if not "Error" in output: + if not response or len(response) != 4 * msg.response_words_count: + raise SPSDKLengthError("ELE Message - Invalid response read-back operation.") + # 3. Decode the response + msg.decode_response(response) + + # 3.1 Check the response status + if msg.status != ResponseStatus.ELE_SUCCESS_IND: + raise SPSDKError(f"ELE Message failed. \n{msg.info()}") + + # 3.2 Read back the response data from target memory if required + if msg.has_response_data: + try: + response_data = self.device.read_memory( + msg.response_data_address, msg.response_data_size + ) + self.device.read_output() + except SPSDKError as exc: + raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + + if not response_data or len(response_data) != msg.response_data_size: + raise SPSDKLengthError("ELE Message - Invalid response data read-back operation.") + + msg.decode_response_data(response_data) + + logger.info(f"Sent message information:\n{msg.info()}") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py new file mode 100644 index 00000000..325c12af --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""EdgeLock Enclave Message constants.""" + +from spsdk.utils.spsdk_enum import SpsdkEnum, SpsdkSoftEnum + + +class MessageIDs(SpsdkSoftEnum): + """ELE Messages ID.""" + + PING_REQ = (0x01, "PING_REQ", "Ping request.") + ELE_FW_AUTH_REQ = (0x02, "ELE_FW_AUTH_REQ", "ELE firmware authenticate request.") + ELE_DUMP_DEBUG_BUFFER_REQ = (0x21, "ELE_DUMP_DEBUG_BUFFER_REQ", "Dump the ELE logs") + ELE_OEM_CNTN_AUTH_REQ = (0x87, "ELE_OEM_CNTN_AUTH_REQ", "OEM Container authenticate") + ELE_VERIFY_IMAGE_REQ = (0x88, "ELE_VERIFY_IMAGE_REQ", "Verify Image") + ELE_RELEASE_CONTAINER_REQ = (0x89, "ELE_RELEASE_CONTAINER_REQ", "Release Container.") + WRITE_SEC_FUSE_REQ = (0x91, "WRITE_SEC_FUSE_REQ", "Write secure fuse request.") + ELE_FWD_LIFECYCLE_UP_REQ = (0x95, "ELE_FWD_LIFECYCLE_UP_REQ", "Forward Lifecycle update") + READ_COMMON_FUSE = (0x97, "READ_COMMON_FUSE", "Read common fuse request.") + GET_FW_VERSION_REQ = (0x9D, "GET_FW_VERSION_REQ", "Get firmware version request.") + RETURN_LIFECYCLE_UPDATE_REQ = ( + 0xA0, + "RETURN_LIFECYCLE_UPDATE_REQ", + "Return lifecycle update request.", + ) + ELE_GET_EVENTS_REQ = (0xA2, "ELE_GET_EVENTS_REQ", "Get Events") + LOAD_KEY_BLOB_REQ = (0xA7, "LOAD_KEY_BLOB_REQ", "Load KeyBlob request.") + ELE_COMMIT_REQ = (0xA8, "ELE_COMMIT_REQ", "EdgeLock Enclave commit request.") + ELE_DERIVE_KEY_REQ = (0xA9, "ELE_DERIVE_KEY_REQ", "Derive key") + GENERATE_KEY_BLOB_REQ = (0xAF, "GENERATE_KEY_BLOB_REQ", "Generate KeyBlob request.") + GET_FW_STATUS_REQ = (0xC5, "GET_FW_STATUS_REQ", "Get ELE FW status request.") + ELE_ENABLE_APC_REQ = (0xD2, "ELE_ENABLE_APC_REQ", "Enable APC (Application processor)") + ELE_ENABLE_RTC_REQ = (0xD3, "ELE_ENABLE_RTC_REQ", "Enable RTC (Runtime processor)") + GET_INFO_REQ = (0xDA, "GET_INFO_REQ", "Get ELE Information request.") + ELE_RESET_APC_CTX_REQ = (0xD8, "ELE_RESET_APC_CTX_REQ", "Reset APC Context") + START_RNG_REQ = (0xA3, "START_RNG_REQ", "Start True Random Generator request.") + GET_TRNG_STATE_REQ = (0xA3, "GET_TRNG_STATE_REQ", "Get True Random Generator state request.") + RESET_REQ = (0xC7, "RESET_REQ", "System reset request.") + WRITE_FUSE = (0xD6, "WRITE_FUSE", "Write fuse") + WRITE_SHADOW_FUSE = (0xF2, "WRITE_SHADOW_FUSE", "Write shadow fuse") + READ_SHADOW_FUSE = (0xF3, "READ_SHADOW_FUSE", "Read shadow fuse request.") + + +class LifeCycle(SpsdkSoftEnum): + """ELE life cycles.""" + + LC_BLANK = (0x002, "BLANK", "Blank device") + LC_FAB = (0x004, "FAB", "Fab mode") + LC_NXP_PROV = (0x008, "NXP_PROV", "NXP Provisioned") + LC_OEM_OPEN = (0x010, "OEM_OPEN", "OEM Open") + LC_OEM_SWC = (0x020, "OEM_SWC", "OEM Secure World Closed") + LC_OEM_CLSD = (0x040, "OEM_CLSD", "OEM Closed") + LC_OEM_FR = (0x080, "OEM_FR", "Field Return OEM") + LC_NXP_FR = (0x100, "NXP_FR", "Field Return NXP") + LC_OEM_LCKD = (0x200, "OEM_LCKD", "OEM Locked") + LC_BRICKED = (0x400, "BRICKED", "BRICKED") + + +class LifeCycleToSwitch(SpsdkSoftEnum): + """ELE life cycles to switch request.""" + + OEM_CLOSED = (0x08, "OEM_CLOSED", "OEM Closed") + OEM_LOCKED = (0x80, "OEM_LOCKED", "OEM Locked") + + +class MessageUnitId(SpsdkSoftEnum): + """Message Unit ID.""" + + RTD_MU = (0x01, "RTD_MU", "Real Time Device message unit") + APD_MU = (0x02, "APD_MU", "Application Processor message unit") + + +class ResponseStatus(SpsdkEnum): + """ELE Message Response status.""" + + ELE_SUCCESS_IND = (0xD6, "Success", "The request was successful") + ELE_FAILURE_IND = (0x29, "Failure", "The request failed") + + +class ResponseIndication(SpsdkSoftEnum): + """ELE Message Response indication.""" + + ELE_ROM_PING_FAILURE_IND = (0x0A, "ELE_ROM_PING_FAILURE_IND", "ROM ping failure") + ELE_FW_PING_FAILURE_IND = (0x1A, "ELE_FW_PING_FAILURE_IND", "Firmware ping failure") + ELE_UNALIGNED_PAYLOAD_FAILURE_IND = ( + 0xA6, + "ELE_UNALIGNED_PAYLOAD_FAILURE_IND", + "Un-aligned payload failure", + ) + ELE_WRONG_SIZE_FAILURE_IND = (0xA7, "ELE_WRONG_SIZE_FAILURE_IND", "Wrong size failure") + ELE_ENCRYPTION_FAILURE_IND = (0xA8, "ELE_ENCRYPTION_FAILURE_IND", "Encryption failure") + ELE_DECRYPTION_FAILURE_IND = (0xA9, "ELE_DECRYPTION_FAILURE_IND", "Decryption failure") + ELE_OTP_PROGFAIL_FAILURE_IND = ( + 0xAA, + "ELE_OTP_PROGFAIL_FAILURE_IND", + "OTP program fail failure", + ) + ELE_OTP_LOCKED_FAILURE_IND = (0xAB, "ELE_OTP_LOCKED_FAILURE_IND", "OTP locked failure") + ELE_OTP_INVALID_IDX_FAILURE_IND = ( + 0xAD, + "ELE_OTP_INVALID_IDX_FAILURE_IND", + "OTP Invalid IDX failure", + ) + ELE_TIME_OUT_FAILURE_IND = (0xB0, "ELE_TIME_OUT_FAILURE_IND", "Timeout failure") + ELE_BAD_PAYLOAD_FAILURE_IND = (0xB1, "ELE_BAD_PAYLOAD_FAILURE_IND", "Bad payload failure") + ELE_WRONG_ADDRESS_FAILURE_IND = ( + 0xB4, + "ELE_WRONG_ADDRESS_FAILURE_IND", + "Wrong address failure", + ) + ELE_DMA_FAILURE_IND = (0xB5, "ELE_DMA_FAILURE_IND", "DMA failure") + ELE_DISABLED_FEATURE_FAILURE_IND = ( + 0xB6, + "ELE_DISABLED_FEATURE_FAILURE_IND", + "Disabled feature failure", + ) + ELE_MUST_ATTEST_FAILURE_IND = (0xB7, "ELE_MUST_ATTEST_FAILURE_IND", "Must attest failure") + ELE_RNG_NOT_STARTED_FAILURE_IND = ( + 0xB8, + "ELE_RNG_NOT_STARTED_FAILURE_IND", + "Random number generator not started failure", + ) + ELE_CRC_ERROR_IND = (0xB9, "ELE_CRC_ERROR_IND", "CRC error") + ELE_AUTH_SKIPPED_OR_FAILED_FAILURE_IND = ( + 0xBB, + "ELE_AUTH_SKIPPED_OR_FAILED_FAILURE_IND", + "Authentication skipped or failed failure", + ) + ELE_INCONSISTENT_PAR_FAILURE_IND = ( + 0xBC, + "ELE_INCONSISTENT_PAR_FAILURE_IND", + "Inconsistent parameter failure", + ) + ELE_RNG_INST_FAILURE_IND = ( + 0xBD, + "ELE_RNG_INST_FAILURE_IND", + "Random number generator instantiation failure", + ) + ELE_LOCKED_REG_FAILURE_IND = (0xBE, "ELE_LOCKED_REG_FAILURE_IND", "Locked register failure") + ELE_BAD_ID_FAILURE_IND = (0xBF, "ELE_BAD_ID_FAILURE_IND", "Bad ID failure") + ELE_INVALID_OPERATION_FAILURE_IND = ( + 0xC0, + "ELE_INVALID_OPERATION_FAILURE_IND", + "Invalid operation failure", + ) + ELE_NON_SECURE_STATE_FAILURE_IND = ( + 0xC1, + "ELE_NON_SECURE_STATE_FAILURE_IND", + "Non secure state failure", + ) + ELE_MSG_TRUNCATED_IND = (0xC2, "ELE_MSG_TRUNCATED_IND", "Message truncated failure") + ELE_BAD_IMAGE_NUM_FAILURE_IND = ( + 0xC3, + "ELE_BAD_IMAGE_NUM_FAILURE_IND", + "Bad image number failure", + ) + ELE_BAD_IMAGE_ADDR_FAILURE_IND = ( + 0xC4, + "ELE_BAD_IMAGE_ADDR_FAILURE_IND", + "Bad image address failure", + ) + ELE_BAD_IMAGE_PARAM_FAILURE_IND = ( + 0xC5, + "ELE_BAD_IMAGE_PARAM_FAILURE_IND", + "Bad image parameters failure", + ) + ELE_BAD_IMAGE_TYPE_FAILURE_IND = ( + 0xC6, + "ELE_BAD_IMAGE_TYPE_FAILURE_IND", + "Bad image type failure", + ) + ELE_APC_ALREADY_ENABLED_FAILURE_IND = ( + 0xCB, + "ELE_APC_ALREADY_ENABLED_FAILURE_IND", + "APC already enabled failure", + ) + ELE_RTC_ALREADY_ENABLED_FAILURE_IND = ( + 0xCC, + "ELE_RTC_ALREADY_ENABLED_FAILURE_IND", + "RTC already enabled failure", + ) + ELE_WRONG_BOOT_MODE_FAILURE_IND = ( + 0xCD, + "ELE_WRONG_BOOT_MODE_FAILURE_IND", + "Wrong boot mode failure", + ) + ELE_OLD_VERSION_FAILURE_IND = (0xCE, "ELE_OLD_VERSION_FAILURE_IND", "Old version failure") + ELE_CSTM_FAILURE_IND = (0xCF, "ELE_CSTM_FAILURE_IND", "CSTM failure") + ELE_CORRUPTED_SRK_FAILURE_IND = ( + 0xD0, + "ELE_CORRUPTED_SRK_FAILURE_IND", + "Corrupted SRK failure", + ) + ELE_OUT_OF_MEMORY_IND = (0xD1, "ELE_OUT_OF_MEMORY_IND", "Out of memory failure") + + ELE_MUST_SIGNED_FAILURE_IND = ( + 0xE0, + "ELE_MUST_SIGNED_FAILURE_IND", + "Must be signed failure", + ) + ELE_NO_AUTHENTICATION_FAILURE_IND = ( + 0xEE, + "ELE_NO_AUTHENTICATION_FAILURE_IND", + "No authentication failure", + ) + ELE_BAD_SRK_SET_FAILURE_IND = (0xEF, "ELE_BAD_SRK_SET_FAILURE_IND", "Bad SRK set failure") + ELE_BAD_SIGNATURE_FAILURE_IND = ( + 0xF0, + "ELE_BAD_SIGNATURE_FAILURE_IND", + "Bad signature failure", + ) + ELE_BAD_HASH_FAILURE_IND = (0xF1, "ELE_BAD_HASH_FAILURE_IND", "Bad hash failure") + ELE_INVALID_LIFECYCLE_IND = (0xF2, "ELE_INVALID_LIFECYCLE_IND", "Invalid lifecycle") + ELE_PERMISSION_DENIED_FAILURE_IND = ( + 0xF3, + "ELE_PERMISSION_DENIED_FAILURE_IND", + "Permission denied failure", + ) + ELE_INVALID_MESSAGE_FAILURE_IND = ( + 0xF4, + "ELE_INVALID_MESSAGE_FAILURE_IND", + "Invalid message failure", + ) + ELE_BAD_VALUE_FAILURE_IND = (0xF5, "ELE_BAD_VALUE_FAILURE_IND", "Bad value failure") + ELE_BAD_FUSE_ID_FAILURE_IND = (0xF6, "ELE_BAD_FUSE_ID_FAILURE_IND", "Bad fuse ID failure") + ELE_BAD_CONTAINER_FAILURE_IND = ( + 0xF7, + "ELE_BAD_CONTAINER_FAILURE_IND", + "Bad container failure", + ) + ELE_BAD_VERSION_FAILURE_IND = (0xF8, "ELE_BAD_VERSION_FAILURE_IND", "Bad version failure") + ELE_INVALID_KEY_FAILURE_IND = ( + 0xF9, + "ELE_INVALID_KEY_FAILURE_IND", + "The key in the container is invalid", + ) + ELE_BAD_KEY_HASH_FAILURE_IND = ( + 0xFA, + "ELE_BAD_KEY_HASH_FAILURE_IND", + "The key hash verification does not match OTP", + ) + ELE_NO_VALID_CONTAINER_FAILURE_IND = ( + 0xFB, + "ELE_NO_VALID_CONTAINER_FAILURE_IND", + "No valid container failure", + ) + ELE_BAD_CERTIFICATE_FAILURE_IND = ( + 0xFC, + "ELE_BAD_CERTIFICATE_FAILURE_IND", + "Bad certificate failure", + ) + ELE_BAD_UID_FAILURE_IND = (0xFD, "ELE_BAD_UID_FAILURE_IND", "Bad UID failure") + ELE_BAD_MONOTONIC_COUNTER_FAILURE_IND = ( + 0xFE, + "ELE_BAD_MONOTONIC_COUNTER_FAILURE_IND", + "Bad monotonic counter failure", + ) + ELE_ABORT_IND = (0xFF, "ELE_ABORT_IND", "Abort") + + +class EleFwStatus(SpsdkSoftEnum): + """ELE Firmware status.""" + + ELE_FW_STATUS_NOT_IN_PLACE = (0, "ELE_FW_STATUS_NOT_IN_PLACE", "Not in place") + ELE_FW_STATUS_IN_PLACE = (1, "ELE_FW_STATUS_IN_PLACE", "Authenticated and operational") + + +class EleInfo2Commit(SpsdkSoftEnum): + """ELE Information type to be committed.""" + + NXP_SRK_REVOCATION = (0x1 << 0, "NXP_SRK_REVOCATION", "SRK revocation of the NXP container") + NXP_FW_FUSE = (0x1 << 1, "NXP_FW_FUSE", "FW fuse version of the NXP container") + OEM_SRK_REVOCATION = (0x1 << 4, "OEM_SRK_REVOCATION", "SRK revocation of the OEM container") + OEM_FW_FUSE = (0x1 << 5, "OEM_FW_FUSE", "FW fuse version of the OEM container") + + +class KeyBlobEncryptionAlgorithm(SpsdkSoftEnum): + """ELE KeyBlob encryption algorithms.""" + + AES_CBC = (0x03, "AES_CBC", "KeyBlob encryption algorithm AES CBC") + AES_CTR = (0x04, "AES_CTR", "KeyBlob encryption algorithm AES CTR") + AES_XTS = (0x37, "AES_XTS", "KeyBlob encryption algorithm AES XTS") + SM4_CBC = (0x2B, "SM4_CBC", "KeyBlob encryption algorithm SM4 CBC") + + +class KeyBlobEncryptionIeeCtrModes(SpsdkSoftEnum): + """IEE Keyblob mode attributes.""" + + AesCTRWAddress = (0x02, "CTR_WITH_ADDRESS", " AES CTR w address binding mode") + AesCTRWOAddress = (0x03, "CTR_WITHOUT_ADDRESS", " AES CTR w/o address binding mode") + AesCTRkeystream = (0x04, "CTR_KEY_STREAM", "AES CTR keystream only") + + +class EleTrngState(SpsdkSoftEnum): + """ELE TRNG state.""" + + ELE_TRNG_NOT_READY = ( + 0x0, + "ELE_TRNG_NOT_READY", + "True random generator not started yet. Use 'start-trng' command", + ) + ELE_TRNG_PROGRAM = (0x1, "ELE_TRNG_PROGRAM", "TRNG is in program mode") + ELE_TRNG_GENERATING_ENTROPY = ( + 0x2, + "ELE_TRNG_GENERATING_ENTROPY", + "TRNG is still generating entropy", + ) + ELE_TRNG_READY = (0x3, "ELE_TRNG_READY", "TRNG entropy is valid and ready to be read") + ELE_TRNG_ERROR = (0x4, "ELE_TRNG_ERROR", "TRNG encounter an error while generating entropy") + + +class EleCsalState(SpsdkSoftEnum): + """ELE CSAL state.""" + + ELE_CSAL_NOT_READY = ( + 0x0, + "ELE_CSAL_NOT_READY", + "Crypto Lib random context initialization is not done yet", + ) + ELE_CSAL_ON_GOING = ( + 0x1, + "ELE_CSAL_ON_GOING", + "Crypto Lib random context initialization is on-going", + ) + ELE_CSAL_READY = (0x2, "ELE_CSAL_READY", "Crypto Lib random context initialization succeed") + ELE_CSAL_ERROR = (0x3, "ELE_CSAL_ERROR", "Crypto Lib random context initialization failed") + ELE_CSAL_PAUSE = ( + 0x4, + "ELE_CSAL_PAUSE", + "Crypto Lib random context initialization is in 'pause' mode", + ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py new file mode 100644 index 00000000..a1cb4042 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py @@ -0,0 +1,1525 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""EdgeLock Enclave Message.""" + + +import logging +from struct import pack, unpack +from typing import Dict, List, Optional + +from crcmod.predefined import mkPredefinedCrcFun + +from spsdk.ele.ele_constants import ( + EleCsalState, + EleFwStatus, + EleInfo2Commit, + EleTrngState, + KeyBlobEncryptionAlgorithm, + KeyBlobEncryptionIeeCtrModes, + LifeCycle, + LifeCycleToSwitch, + MessageIDs, + MessageUnitId, + ResponseIndication, + ResponseStatus, +) +from spsdk.exceptions import SPSDKParsingError, SPSDKValueError +from spsdk.image.ahab.signed_msg import SignedMessage +from spsdk.utils.misc import Endianness, align, align_block +from spsdk.utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + +LITTLE_ENDIAN = "<" +UINT8 = "B" +UINT16 = "H" +UINT32 = "L" +UINT64 = "Q" +RESERVED = 0 + + +class EleMessage: + """Base class for any EdgeLock Enclave Message. + + Message contains a header - tag, command id, size and version. + """ + + CMD = 0x00 + TAG = 0x17 + RSP_TAG = 0xE1 + VERSION = 0x06 + HEADER_FORMAT = LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8 + COMMAND_HEADER_WORDS_COUNT = 1 + COMMAND_PAYLOAD_WORDS_COUNT = 0 + RESPONSE_HEADER_WORDS_COUNT = 2 + RESPONSE_PAYLOAD_WORDS_COUNT = 0 + ELE_MSG_ALIGN = 8 + MAX_RESPONSE_DATA_SIZE = 0 + MAX_COMMAND_DATA_SIZE = 0 + + def __init__(self) -> None: + """Class object initialized.""" + self.abort_code = 0 + self.indication = 0 + self.status = 0 + self.buff_addr = 0 + self.buff_size = 0 + self.command = self.CMD + self._response_data_size = self.MAX_RESPONSE_DATA_SIZE + + @property + def command_address(self) -> int: + """Command address in target memory space.""" + return align(self.buff_addr, self.ELE_MSG_ALIGN) + + @property + def command_words_count(self) -> int: + """Command Words count.""" + return self.COMMAND_HEADER_WORDS_COUNT + self.COMMAND_PAYLOAD_WORDS_COUNT + + @property + def has_command_data(self) -> bool: + """Check if command has additional data.""" + return bool(self.command_data_size > 0) + + @property + def command_data_address(self) -> int: + """Command data address in target memory space.""" + return align(self.command_address + self.command_words_count * 4, self.ELE_MSG_ALIGN) + + @property + def command_data_size(self) -> int: + """Command data address in target memory space.""" + return align(len(self.command_data) or self.MAX_COMMAND_DATA_SIZE, self.ELE_MSG_ALIGN) + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + return b"" + + @property + def response_address(self) -> int: + """Response address in target memory space.""" + if self.has_command_data: + address = self.command_data_address + self.command_data_size + else: + address = self.buff_addr + self.command_words_count * 4 + return align(address, self.ELE_MSG_ALIGN) + + @property + def response_words_count(self) -> int: + """Response Words count.""" + return self.RESPONSE_HEADER_WORDS_COUNT + self.RESPONSE_PAYLOAD_WORDS_COUNT + + @property + def has_response_data(self) -> bool: + """Check if response has additional data.""" + return bool(self.response_data_size > 0) + + @property + def response_data_address(self) -> int: + """Response data address in target memory space.""" + return align(self.response_address + self.response_words_count * 4, self.ELE_MSG_ALIGN) + + @property + def response_data_size(self) -> int: + """Response data address in target memory space.""" + return align(self._response_data_size, self.ELE_MSG_ALIGN) + + @property + def free_space_address(self) -> int: + """First free address after ele message in target memory space.""" + return align(self.response_data_address + self._response_data_size, self.ELE_MSG_ALIGN) + + @property + def free_space_size(self) -> int: + """Free space size after ele message in target memory space.""" + return align( + self.buff_size - (self.free_space_address - self.buff_addr), self.ELE_MSG_ALIGN + ) + + @property + def status_string(self) -> str: + """Get status in readable string format.""" + if self.status not in ResponseStatus: + return "Invalid status!" + if self.status == ResponseStatus.ELE_SUCCESS_IND: + return "Succeeded" + indication = ( + ResponseIndication.get_label(self.indication) + if ResponseIndication.contains(self.indication) + else f"Invalid indication code: {self.indication:02X}" + ) + return f"Failed: {indication}" + + def set_buffer_params(self, buff_addr: int, buff_size: int) -> None: + """Set the communication buffer parameters to allow command update addresses inside command payload. + + :param buff_addr: Real address of communication buffer in target memory space + :param buff_size: Size of communication buffer in target memory space + """ + self.buff_addr = buff_addr + self.buff_size = buff_size + + self.validate_buffer_params() + + def validate_buffer_params(self) -> None: + """Validate communication buffer parameters. + + raises SPSDKValueError: Invalid buffer parameters. + """ + if self.has_response_data: + needed_space = self.response_data_address + self.response_data_size + else: + needed_space = self.response_address + self.response_words_count * 4 + + if self.buff_size < needed_space - self.buff_addr: + raise SPSDKValueError( + "ELE Message: Communication buffer is to small to fit message. " + f"({needed_space-self.buff_addr} > {self.buff_size})" + ) + + def validate(self) -> None: + """Validate message.""" + + def header_export( + self, + ) -> bytes: + """Exports message header to bytes. + + :return: Bytes representation of message header. + """ + return pack( + self.HEADER_FORMAT, self.VERSION, self.command_words_count, self.command, self.TAG + ) + + def export( + self, + ) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + return self.header_export() + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + # Decode and validate header + (version, size, command, tag) = unpack(self.HEADER_FORMAT, response[:4]) + if tag != self.RSP_TAG: + raise SPSDKParsingError(f"Message TAG in response is invalid: {hex(tag)}") + if command != self.command: + raise SPSDKParsingError(f"Message COMMAND in response is invalid: {hex(command)}") + if size not in [self.response_words_count, self.RESPONSE_HEADER_WORDS_COUNT]: + raise SPSDKParsingError(f"Message SIZE in response is invalid: {hex(size)}") + if version != self.VERSION: + raise SPSDKParsingError(f"Message VERSION in response is invalid: {hex(version)}") + + # Decode status word + ( + self.status, + self.indication, + self.abort_code, + ) = unpack(LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, response[4:8]) + + def decode_response_data(self, response_data: bytes) -> None: + """Decode response data from target. + + :note: The response data are specific per command. + :param response_data: Data of response. + """ + + def __eq__(self, other: object) -> bool: + if isinstance(other, EleMessage): + if ( + self.TAG == other.TAG + and self.command == other.command + and self.VERSION == other.VERSION + and self.command_words_count == other.command_words_count + ): + return True + + return False + + @staticmethod + def get_msg_crc(payload: bytes) -> bytes: + """Compute message CRC. + + :param payload: The input data to compute CRC on them. Must be 4 bytes aligned. + :return: 4 bytes of CRC in little endian format. + """ + assert len(payload) % 4 == 0 + res = 0 + for i in range(0, len(payload), 4): + res ^= int.from_bytes(payload[i : i + 4], Endianness.LITTLE.value) + return res.to_bytes(4, Endianness.LITTLE.value) + + def response_status(self) -> str: + """Print the response status information. + + :return: String with response status. + """ + ret = f"Response status: {ResponseStatus.get_label(self.status)}\n" + if self.status == ResponseStatus.ELE_FAILURE_IND: + ret += ( + f" Response indication: {ResponseIndication.get_label(self.indication)}" + f" - ({hex(self.indication)})\n" + ) + ret += f" Response abort code: {hex(self.abort_code)}\n" + return ret + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + ret = f"Command: {MessageIDs.get_label(self.command)} - ({hex(self.command)})\n" + ret += f"Command words: {self.command_words_count}\n" + ret += f"Command data: {self.has_command_data}\n" + ret += f"Response words: {self.response_words_count}\n" + ret += f"Response data: {self.has_response_data}\n" + # if self.status in ResponseStatus: + ret += self.response_status() + + return ret + + +class EleMessagePing(EleMessage): + """ELE Message Ping.""" + + CMD = MessageIDs.PING_REQ.tag + + +class EleMessageDumpDebugBuffer(EleMessage): + """ELE Message Dump Debug buffer.""" + + CMD = MessageIDs.ELE_DUMP_DEBUG_BUFFER_REQ.tag + RESPONSE_PAYLOAD_WORDS_COUNT = 21 + + def __init__(self) -> None: + """Class object initialized.""" + super().__init__() + self.debug_words: List[int] = [0] * 20 + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + *self.debug_words, crc = unpack(LITTLE_ENDIAN + "20L4s", response[8:92]) + crc_computed = self.get_msg_crc(response[0:88]) + if crc != crc_computed: + raise SPSDKParsingError("Invalid message CRC for dump debug buffer") + + def response_info(self) -> str: + """Print Dumped data of debug buffer.""" + ret = "" + for i, dump_data in enumerate(self.debug_words): + ret += f"Dump debug word[{i}]: {dump_data:08X}\n" + + return ret + + +class EleMessageReset(EleMessage): + """ELE Message Reset.""" + + CMD = MessageIDs.RESET_REQ.tag + RESPONSE_HEADER_WORDS_COUNT = 0 + + +class EleMessageEleFwAuthenticate(EleMessage): + """Ele firmware authenticate request.""" + + CMD = MessageIDs.ELE_FW_AUTH_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 3 + + def __init__(self, ele_fw_address: int) -> None: + """Constructor. + + Be aware to have ELE FW in accessible memory for ROM, and + do not use the RAM memory used to communicate with ELE. + + :param ele_fw_address: Address in target memory with ele firmware. + """ + super().__init__() + self.ele_fw_address = ele_fw_address + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack( + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, self.ele_fw_address, 0, self.ele_fw_address + ) + return ret + + +class EleMessageOemContainerAuthenticate(EleMessage): + """OEM container authenticate request.""" + + CMD = MessageIDs.ELE_OEM_CNTN_AUTH_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self, oem_cntn_addr: int) -> None: + """Constructor. + + Be aware to have OEM Container in accessible memory for ROM. + + :param oem_cntn_addr: Address in target memory with oem container. + """ + super().__init__() + self.oem_cntn_addr = oem_cntn_addr + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT32 + UINT32, 0, self.oem_cntn_addr) + return ret + + +class EleMessageVerifyImage(EleMessage): + """Verify image request.""" + + CMD = MessageIDs.ELE_VERIFY_IMAGE_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 1 + RESPONSE_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self, image_mask: int = 0x0000_0001) -> None: + """Constructor. + + The Verify Image message is sent to the ELE after a container has been + loaded into memory and processed with an Authenticate Container message. + This commands the ELE to check the hash on one or more images. + + :param image_mask: Used to indicate which images are to be checked. There must be at least + one image. Each bit corresponds to a particular image index in the header, for example, + bit 0 is for image 0, and bit 1 is for image 1, and so on. + """ + super().__init__() + self.image_mask = image_mask + self.valid_image_mask = 0 + self.invalid_image_mask = 0xFFFF_FFFF + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT32, self.image_mask) + return ret + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.valid_image_mask, self.invalid_image_mask = unpack( + LITTLE_ENDIAN + "LL", response[8:16] + ) + checked_mask = self.valid_image_mask | self.invalid_image_mask + if self.image_mask != checked_mask: + logger.error( + "The invalid&valid mask doesn't cover requested mask to check! " + f"valid: 0x{self.valid_image_mask:08X} | invalid: 0x{self.invalid_image_mask:08X}" + f" != requested: 0x{self.image_mask:08X}" + ) + + def response_info(self) -> str: + """Print Dumped data of debug buffer.""" + ret = f"Valid image mask : 0x{self.valid_image_mask:08X}\n" + ret += f"Invalid image mask : 0x{self.invalid_image_mask:08X}" + return ret + + +class EleMessageReleaseContainer(EleMessage): + """ELE Message Release container.""" + + CMD = MessageIDs.ELE_RELEASE_CONTAINER_REQ.tag + + +class EleMessageForwardLifeCycleUpdate(EleMessage): + """Forward Life cycle update request.""" + + CMD = MessageIDs.ELE_FWD_LIFECYCLE_UP_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 1 + + def __init__(self, lifecycle_update: LifeCycleToSwitch) -> None: + """Constructor. + + Be aware that this is non-revertible operation. + + :param lifecycle_update: New life cycle value. + """ + super().__init__() + self.lifecycle_update = lifecycle_update + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, self.lifecycle_update.tag, 0, 0) + return ret + + +class EleMessageGetEvents(EleMessage): + """Get events request. + + \b + Event layout: + ------------------------- + - TAG - CMD - IND - STS - + ------------------------- + \b + """ + + CMD = MessageIDs.ELE_GET_EVENTS_REQ.tag + RESPONSE_PAYLOAD_WORDS_COUNT = 10 + + MAX_EVENT_CNT = 8 + + def __init__(self) -> None: + """Constructor. + + This message is used to retrieve any singular event that has occurred since the FW has + started. A singular event occurs when the second word of a response to any request is + different from ELE_SUCCESS_IND. That includes commands with failure response as well as + commands with successful response containing an indication (i.e. warning response). + The events are stored by the ELE in a fixed sized buffer. When the capacity of the buffer + is exceeded, new occurring events are lost. + The event buffer is systematically returned in full to the requester independently of + the actual numbers of events stored. + """ + super().__init__() + self.event_cnt = 0 + self.events: List[int] = [0] * self.MAX_EVENT_CNT + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.event_cnt, max_events, *self.events, crc = unpack( + LITTLE_ENDIAN + UINT16 + UINT16 + "8L4s", response[8:48] + ) + if max_events != self.MAX_EVENT_CNT: + logger.error(f"Invalid maximal events count: {max_events}!={self.MAX_EVENT_CNT}") + + crc_computed = self.get_msg_crc(response[0:44]) + if crc != crc_computed: + logger.error("Invalid message CRC for get events message") + + @staticmethod + def get_ipc_id(event: int) -> str: + """Get IPC ID in string from event.""" + ipc_id = (event >> 24) & 0xFF + return MessageUnitId.get_description(ipc_id, f"Unknown MU: ({ipc_id})") or "" + + @staticmethod + def get_cmd(event: int) -> str: + """Get Command in string from event.""" + cmd = (event >> 16) & 0xFF + return MessageIDs.get_description(cmd, f"Unknown Command: (0x{cmd:02})") or "" + + @staticmethod + def get_ind(event: int) -> str: + """Get Indication in string from event.""" + ind = (event >> 8) & 0xFF + return ResponseIndication.get_description(ind, f"Unknown Indication: (0x{ind:02})") or "" + + @staticmethod + def get_sts(event: int) -> str: + """Get Status in string from event.""" + sts = event & 0xFF + return ResponseStatus.get_description(sts, f"Unknown Status: (0x{sts:02})") or "" + + def response_info(self) -> str: + """Print events info.""" + ret = f"Event count: {self.event_cnt}" + for i, event in enumerate(self.events[: min(self.event_cnt, self.MAX_EVENT_CNT)]): + ret += f"\nEvent[{i}]: 0x{event:08X}" + ret += f"\n IPC ID: {self.get_ipc_id(event)}" + ret += f"\n Command: {self.get_cmd(event)}" + ret += f"\n Indication: {self.get_ind(event)}" + ret += f"\n Status: {self.get_sts(event)}" + if self.event_cnt > self.MAX_EVENT_CNT: + ret += "\nEvent count is bigger than maximal supported, " + ret += f"only first {self.MAX_EVENT_CNT} events are listed." + return ret + + +class EleMessageStartTrng(EleMessage): + """ELE Message Start True Random Generator.""" + + CMD = MessageIDs.START_RNG_REQ.tag + + +class EleMessageGetTrngState(EleMessage): + """ELE Message Get True Random Generator State.""" + + CMD = MessageIDs.GET_TRNG_STATE_REQ.tag + RESPONSE_PAYLOAD_WORDS_COUNT = 1 + + def __init__(self) -> None: + """Class object initialized.""" + super().__init__() + self.ele_trng_state = EleTrngState.ELE_TRNG_PROGRAM.tag + self.ele_csal_state = EleCsalState.ELE_CSAL_NOT_READY.tag + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.ele_trng_state, self.ele_csal_state, _ = unpack( + LITTLE_ENDIAN + UINT8 + UINT8 + "2s", response[8:12] + ) + + def response_info(self) -> str: + """Print specific information of ELE. + + :return: Information about the TRNG. + """ + return ( + f"EdgeLock Enclave TRNG state: {EleTrngState.get_description(self.ele_trng_state)}" + + f"\nEdgeLock Enclave CSAL state: {EleCsalState.get_description(self.ele_csal_state)}" + ) + + +class EleMessageCommit(EleMessage): + """ELE Message Get FW status.""" + + CMD = MessageIDs.ELE_COMMIT_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 1 + RESPONSE_PAYLOAD_WORDS_COUNT = 1 + + def __init__(self, info_to_commit: List[EleInfo2Commit]) -> None: + """Class object initialized.""" + super().__init__() + self.info_to_commit = info_to_commit + + @property + def info2commit_mask(self) -> int: + """Get info to commit mask used in command.""" + ret = 0 + for rule in self.info_to_commit: + ret |= rule.tag + return ret + + def mask_to_info2commit(self, mask: int) -> List[EleInfo2Commit]: + """Get list of info to commit from mask.""" + ret = [] + for bit in range(32): + bit_mask = 1 << bit + if mask and bit_mask: + ret.append(EleInfo2Commit.from_tag(bit)) + return ret + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT32, self.info2commit_mask) + return ret + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + mask = int.from_bytes(response[8:12], Endianness.LITTLE.value) + if mask != self.info2commit_mask: + logger.error( + f"Only those information has been committed: {[x.label for x in self.mask_to_info2commit(mask)]}," + f" from those:{[x.label for x in self.info_to_commit]}" + ) + + +class EleMessageGetFwStatus(EleMessage): + """ELE Message Get FW status.""" + + CMD = MessageIDs.GET_FW_STATUS_REQ.tag + RESPONSE_PAYLOAD_WORDS_COUNT = 1 + + def __init__(self) -> None: + """Class object initialized.""" + super().__init__() + self.ele_fw_status = EleFwStatus.ELE_FW_STATUS_NOT_IN_PLACE.tag + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.ele_fw_status, _ = unpack(LITTLE_ENDIAN + UINT8 + "3s", response[8:12]) + + def response_info(self) -> str: + """Print specific information of ELE. + + :return: Information about the ELE. + """ + return f"EdgeLock Enclave firmware state: {EleFwStatus.get_label(self.ele_fw_status)}" + + +class EleMessageGetFwVersion(EleMessage): + """ELE Message Get FW version.""" + + CMD = MessageIDs.GET_FW_VERSION_REQ.tag + RESPONSE_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self) -> None: + """Class object initialized.""" + super().__init__() + self.ele_fw_version_raw = 0 + self.ele_fw_version_sha1 = 0 + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.ele_fw_version_raw = int.from_bytes(response[8:12], Endianness.LITTLE.value) + self.ele_fw_version_sha1 = int.from_bytes(response[12:16], Endianness.LITTLE.value) + + def response_info(self) -> str: + """Print specific information of ELE. + + :return: Information about the ELE. + """ + ret = ( + f"EdgeLock Enclave firmware version: {self.ele_fw_version_raw:08X}\n" + f"Readable form: {(self.ele_fw_version_raw>>16) & 0xff}." + f"{(self.ele_fw_version_raw>>4) & 0xfff}.{self.ele_fw_version_raw & 0xf}\n" + f"Commit SHA1 (First 4 bytes): {self.ele_fw_version_sha1:08X}" + ) + if self.ele_fw_version_raw & 1 << 31: + ret += "\nDirty build" + return ret + + +class EleMessageReadCommonFuse(EleMessage): + """ELE Message Read common fuse.""" + + CMD = MessageIDs.READ_COMMON_FUSE.tag + COMMAND_PAYLOAD_WORDS_COUNT = 1 + RESPONSE_PAYLOAD_WORDS_COUNT = 1 + + def __init__(self, index: int) -> None: + """Constructor. + + Read common fuse. + + :param index: Fuse ID. + """ + super().__init__() + self.index = index + self.fuse_value = 0 + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT16 + UINT16, self.index, 0) + return ret + + def decode_response(self, response: bytes) -> None: + """Decode response from target. + + :param response: Data of response. + :raises SPSDKParsingError: Response parse detect some error. + """ + super().decode_response(response) + self.fuse_value = int.from_bytes(response[8:12], Endianness.LITTLE.value) + + def response_info(self) -> str: + """Print fuse value. + + :return: Read fuse value. + """ + return f"Fuse ID_{self.index}: 0x{self.fuse_value:08X}\n" + + +class EleMessageReadShadowFuse(EleMessageReadCommonFuse): + """ELE Message Read shadow fuse.""" + + CMD = MessageIDs.READ_SHADOW_FUSE.tag + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + ret += pack(LITTLE_ENDIAN + UINT32, self.index) + return ret + + +class EleMessageGetInfo(EleMessage): + """ELE Message Get Info.""" + + CMD = MessageIDs.GET_INFO_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 3 + MAX_RESPONSE_DATA_SIZE = 256 + + def __init__(self) -> None: + """Class object initialized.""" + super().__init__() + self.info_length = 0 + self.info_version = 0 + self.info_cmd = 0 + self.info_soc_rev = 0 + self.info_soc_id = 0 + self.info_life_cycle = 0 + self.info_sssm_state = 0 + self.info_uuid = bytes() + self.info_sha256_rom_patch = bytes() + self.info_sha256_fw = bytes() + self.info_oem_srkh = bytes() + self.info_imem_state = 0 + self.info_csal_state = 0 + self.info_trng_state = 0 + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + payload = pack( + LITTLE_ENDIAN + UINT32 + UINT32 + UINT16 + UINT16, + 0, + self.response_data_address, + self.response_data_size, + 0, + ) + return self.header_export() + payload + + def decode_response_data(self, response_data: bytes) -> None: + """Decode response data from target. + + :note: The response data are specific per command. + :param response_data: Data of response. + """ + (self.info_cmd, self.info_version, self.info_length) = unpack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, response_data[:4] + ) + + (self.info_soc_id, self.info_soc_rev) = unpack( + LITTLE_ENDIAN + UINT16 + UINT16, response_data[4:8] + ) + (self.info_life_cycle, self.info_sssm_state, _) = unpack( + LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, response_data[8:12] + ) + self.info_uuid = response_data[12:28] + self.info_sha256_rom_patch = response_data[28:60] + self.info_sha256_fw = response_data[60:92] + if self.info_version == 0x02: + self.info_oem_srkh = response_data[92:156] + self.info_oem_srkh = response_data[92:156] + (self.info_trng_state, self.info_csal_state, self.info_imem_state, _) = unpack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, response_data[156:160] + ) + + def response_info(self) -> str: + """Print specific information of ELE. + + :return: Information about the ELE. + """ + ret = f"Command: {hex(self.info_cmd)}\n" + ret += f"Version: {self.info_version}\n" + ret += f"Length: {self.info_length}\n" + ret += f"SoC ID: {self.info_soc_id:04X}\n" + ret += f"SoC version: {self.info_soc_rev:04X}\n" + ret += f"Life Cycle: {LifeCycle.get_label(self.info_life_cycle)} - 0x{self.info_life_cycle:04X}\n" + ret += f"SSSM state: {self.info_sssm_state}\n" + ret += f"UUID: {self.info_uuid.hex()}\n" + ret += f"SHA256 ROM PATCH: {self.info_sha256_rom_patch.hex()}\n" + ret += f"SHA256 FW: {self.info_sha256_fw.hex()}\n" + if self.info_version == 0x02: + ret += "Advanced information:\n" + ret += f" OEM SRKH: {self.info_oem_srkh.hex()}\n" + ret += f" IMEM state: {self.info_imem_state}\n" + ret += ( + f" CSAL state: " + f"{EleCsalState.get_description(self.info_csal_state, str(self.info_csal_state))}\n" + ) + ret += ( + f" TRNG state: " + f"{EleTrngState.get_description(self.info_trng_state, str(self.info_trng_state))}\n" + ) + + return ret + + +class EleMessageDeriveKey(EleMessage): + """ELE Message Derive Key.""" + + CMD = MessageIDs.ELE_DERIVE_KEY_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 6 + MAX_RESPONSE_DATA_SIZE = 32 + _MAX_COMMAND_DATA_SIZE = 65536 + SUPPORTED_KEY_SIZES = [16, 32] + + def __init__(self, key_size: int, context: Optional[bytes]) -> None: + """Class object initialized. + + :param key_size: Output key size [16,32] is valid + :param context: User's context to be used for key diversification + """ + if key_size not in self.SUPPORTED_KEY_SIZES: + raise SPSDKValueError( + f"Output Key size ({key_size}) must be in {self.SUPPORTED_KEY_SIZES}" + ) + if context and len(context) > self._MAX_COMMAND_DATA_SIZE: + raise SPSDKValueError( + f"User context length ({len(context)}) <= {self._MAX_COMMAND_DATA_SIZE}" + ) + super().__init__() + self.key_size = key_size + self._response_data_size = key_size + self.context = context + self.derived_key = b"" + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + payload = pack( + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32 + UINT32 + UINT16 + UINT16, + 0, + self.response_data_address, + 0, + self.command_data_address if self.context else 0, + self.key_size, + self.command_data_size, + ) + header = self.header_export() + return header + payload + self.get_msg_crc(header + payload) + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + return self.context if self.context else b"" + + def decode_response_data(self, response_data: bytes) -> None: + """Decode response data from target. + + :note: The response data are specific per command. + :param response_data: Data of response. + """ + self.derived_key = response_data[: self.key_size] + + def get_key(self) -> bytes: + """Get derived key.""" + return self.derived_key + + +class EleMessageSigned(EleMessage): + """ELE Message Signed.""" + + COMMAND_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self, signed_msg: bytes) -> None: + """Class object initialized. + + :param signed_msg: Signed message container. + """ + super().__init__() + self.signed_msg_binary = signed_msg + # Get the command inside the signed message + self.signed_msg = SignedMessage.parse(signed_msg) + self.signed_msg.update_fields() + assert self.signed_msg.message + self.command = self.signed_msg.message.cmd + self._command_data_size = len(self.signed_msg_binary) + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + payload = pack( + LITTLE_ENDIAN + UINT32 + UINT32, + 0, + self.command_data_address, + ) + return self.header_export() + payload + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + return self.signed_msg_binary + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + ret = super().info() + ret += "\n" + self.signed_msg.image_info().draw() + + return ret + + +class EleMessageGenerateKeyBlob(EleMessage): + """ELE Message Generate KeyBlob.""" + + KEYBLOB_NAME = "Unknown" + # List of supported algorithms and theirs key sizes + SUPPORTED_ALGORITHMS: Dict[SpsdkEnum, List[int]] = {} + + KEYBLOB_TAG = 0x81 + KEYBLOB_VERSION = 0x00 + CMD = MessageIDs.GENERATE_KEY_BLOB_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 7 + MAX_RESPONSE_DATA_SIZE = 512 + + def __init__( + self, key_identifier: int, algorithm: KeyBlobEncryptionAlgorithm, key: bytes + ) -> None: + """Constructor of Generate Key Blob class. + + :param key_identifier: ID of key + :param algorithm: Select supported algorithm + :param key: Key to be wrapped + """ + super().__init__() + self.key_id = key_identifier + self.algorithm = algorithm + + self.key = key + self.key_blob = bytes() + self.validate() + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + payload = pack( + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32 + UINT32 + UINT32 + UINT16 + UINT16, + self.key_id, + 0, + self.command_data_address, + 0, + self.response_data_address, + self.MAX_RESPONSE_DATA_SIZE, + 0, + ) + payload = self.header_export() + payload + return payload + EleMessage.get_msg_crc(payload) + + def validate(self) -> None: + """Validate generate keyblob message data. + + :raises SPSDKValueError: Invalid used key size or encryption algorithm + """ + if self.algorithm not in self.SUPPORTED_ALGORITHMS: + raise SPSDKValueError( + f"{self.algorithm} is not supported by {self.KEYBLOB_NAME} keyblob in ELE." + ) + + if len(self.key) * 8 not in self.SUPPORTED_ALGORITHMS[self.algorithm]: + raise SPSDKValueError( + f"Unsupported size of input key by {self.KEYBLOB_NAME} keyblob" + f" for {self.algorithm.label} algorithm." + f"The list of supported keys in bit count: {self.SUPPORTED_ALGORITHMS[self.algorithm]}" + ) + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + ret = super().info() + ret += "\n" + ret += f"KeyBlob type: {self.KEYBLOB_NAME}\n" + ret += f"Key ID: {self.key_id}\n" + ret += f"Algorithm: {self.algorithm.label}\n" + ret += f"Key size: {len(self.key)*8} bits\n" + return ret + + @classmethod + def get_supported_algorithms(cls) -> List[str]: + """Get the list of supported algorithms. + + :return: List of supported algorithm names. + """ + return list(x.label for x in cls.SUPPORTED_ALGORITHMS) + + @classmethod + def get_supported_key_sizes(cls) -> str: + """Get table with supported key sizes per algorithm. + + :return: Table with supported key size in text. + """ + ret = "" + for key, value in cls.SUPPORTED_ALGORITHMS.items(): + ret += key.label + ": " + str(value) + ",\n" + return ret + + def decode_response_data(self, response_data: bytes) -> None: + """Decode response data from target. + + :note: The response data are specific per command. + :param response_data: Data of response. + :raises SPSDKParsingError: Invalid response detected. + """ + ver, length, tag = unpack(LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, response_data[:4]) + if tag != self.KEYBLOB_TAG: + raise SPSDKParsingError("Invalid TAG in generated KeyBlob") + if ver != self.KEYBLOB_VERSION: + raise SPSDKParsingError("Invalid Version in generated KeyBlob") + if length > self.MAX_RESPONSE_DATA_SIZE: + raise SPSDKParsingError("Invalid Length in generated KeyBlob") + + self.key_blob = response_data[:length] + + +class EleMessageGenerateKeyBlobDek(EleMessageGenerateKeyBlob): + """ELE Message Generate DEK KeyBlob.""" + + KEYBLOB_NAME = "DEK" + # List of supported algorithms and theirs key sizes + SUPPORTED_ALGORITHMS = { + KeyBlobEncryptionAlgorithm.AES_CBC: [128, 192, 256], + KeyBlobEncryptionAlgorithm.SM4_CBC: [128], + } + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + header = pack( + LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, + self.KEYBLOB_VERSION, + 8 + len(self.key), + self.KEYBLOB_TAG, + ) + options = pack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, + 0x01, # Flags - DEK + len(self.key), + self.algorithm.tag, + 0, + ) + return header + options + self.key + + +class EleMessageGenerateKeyBLobOtfad(EleMessageGenerateKeyBlob): + """ELE Message Generate OTFAD KeyBlob.""" + + KEYBLOB_NAME = "OTFAD" + # List of supported algorithms and theirs key sizes + SUPPORTED_ALGORITHMS = {KeyBlobEncryptionAlgorithm.AES_CTR: [128]} + + def __init__( + self, + key_identifier: int, + key: bytes, + aes_counter: bytes, + start_address: int, + end_address: int, + read_only: bool = True, + decryption_enabled: bool = True, + configuration_valid: bool = True, + ) -> None: + """Constructor of generate OTFAD keyblob class. + + :param key_identifier: ID of Key + :param key: OTFAD key + :param aes_counter: AES counter value + :param start_address: Start address in memory to be encrypted + :param end_address: End address in memory to be encrypted + :param read_only: Read only flag, defaults to True + :param decryption_enabled: Decryption enable flag, defaults to True + :param configuration_valid: Configuration valid flag, defaults to True + """ + self.aes_counter = aes_counter + self.start_address = start_address + self.end_address = end_address + self.read_only = read_only + self.decryption_enabled = decryption_enabled + self.configuration_valid = configuration_valid + super().__init__(key_identifier, KeyBlobEncryptionAlgorithm.AES_CTR, key) + + def validate(self) -> None: + """Validate generate OTFAD keyblob.""" + # Validate general members + super().validate() + # 1 Validate OTFAD Key identifier + struct_index = self.key_id & 0xFF + peripheral_index = (self.key_id >> 8) & 0xFF + reserved = self.key_id & 0xFFFF0000 + + if struct_index > 3: + raise SPSDKValueError( + "Invalid OTFAD Key Identifier. Byte 0 must be in range [0-3]," + " to select used key struct, for proper scrambling." + ) + + if peripheral_index not in [1, 2]: + raise SPSDKValueError( + "Invalid OTFAD Key Identifier. Byte 1 must be in range [1-2]," + " to select used peripheral [FlexSPIx]." + ) + + if reserved != 0: + raise SPSDKValueError("Invalid OTFAD Key Identifier. Byte 2-3 must be set to 0.") + + # 2. validate AES counter + if len(self.aes_counter) != 8: + raise SPSDKValueError("Invalid AES counter length. It must be 64 bits.") + + # 3. start address + if self.start_address != 0 and self.start_address != align(self.start_address, 1024): + raise SPSDKValueError( + "Invalid OTFAD start address. Start address has to be aligned to 1024 bytes." + ) + + # 4. end address + if self.end_address != 0 and self.end_address != align(self.end_address, 1024): + raise SPSDKValueError( + "Invalid OTFAD end address. End address has to be aligned to 1024 bytes." + ) + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + header = pack( + LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, + self.KEYBLOB_VERSION, + 0x30, + self.KEYBLOB_TAG, + ) + options = pack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, + 0x02, # Flags - OTFAD + 0x28, + self.algorithm.tag, + 0, + ) + end_address = self.end_address + if self.read_only: + end_address |= 0x04 + if self.decryption_enabled: + end_address |= 0x02 + if self.configuration_valid: + end_address |= 0x01 + + otfad_config = pack( + LITTLE_ENDIAN + "16s" + "8s" + UINT32 + UINT32 + UINT32, + self.key, + self.aes_counter, + self.start_address, + end_address, + 0, + ) + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + crc: int = crc32_function(otfad_config) + return header + options + otfad_config + crc.to_bytes(4, Endianness.LITTLE.value) + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + ret = super().info() + ret += f"AES Counter: {self.aes_counter.hex()}\n" + ret += f"Start address: {self.start_address:08x}\n" + ret += f"End address: {self.end_address:08x}\n" + ret += f"Read_only: {self.read_only}\n" + ret += f"Enabled: {self.decryption_enabled}\n" + ret += f"Valid: {self.configuration_valid}\n" + return ret + + +class EleMessageGenerateKeyBlobIee(EleMessageGenerateKeyBlob): + """ELE Message Generate IEE KeyBlob.""" + + KEYBLOB_NAME = "IEE" + # List of supported algorithms and theirs key sizes + SUPPORTED_ALGORITHMS = { + KeyBlobEncryptionAlgorithm.AES_XTS: [256, 512], + KeyBlobEncryptionAlgorithm.AES_CTR: [128, 256], + } + + def __init__( + self, + key_identifier: int, + algorithm: KeyBlobEncryptionAlgorithm, + key: bytes, + ctr_mode: KeyBlobEncryptionIeeCtrModes, + aes_counter: bytes, + page_offset: int, + region_number: int, + bypass: bool = False, + locked: bool = False, + ) -> None: + """Constructor of generate IEE keyblob class. + + :param key_identifier: ID of key + :param algorithm: Used algorithm + :param key: IEE key + :param ctr_mode: In case of AES CTR algorithm, the CTR mode must be selected + :param aes_counter: AES counter in case of AES CTR algorithm + :param page_offset: IEE page offset + :param region_number: Region number + :param bypass: Encryption bypass flag, defaults to False + :param locked: Locked flag, defaults to False + """ + self.ctr_mode = ctr_mode + self.aes_counter = aes_counter + self.page_offset = page_offset + self.region_number = region_number + self.bypass = bypass + self.locked = locked + super().__init__(key_identifier, algorithm, key) + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + header = pack( + LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, + self.KEYBLOB_VERSION, + 88, + self.KEYBLOB_TAG, + ) + options = pack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, + 0x03, # Flags - IEE + len(self.key), + self.algorithm.tag, + 0, + ) + region_attribute = 0 + if self.bypass: + region_attribute |= 1 << 7 + if self.algorithm == KeyBlobEncryptionAlgorithm.AES_XTS: + region_attribute |= 0b01 << 4 + if len(self.key) == 64: + region_attribute |= 0x01 + else: + region_attribute |= self.ctr_mode.tag << 4 + if len(self.key) == 32: + region_attribute |= 0x01 + + if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: + key1 = align_block(self.key, 32, 0) + key2 = align_block(self.aes_counter, 32, 0) + else: + key_len = len(self.key) + key1 = align_block(self.key[: key_len // 2], 32, 0) + key2 = align_block(self.key[key_len // 2 :], 32, 0) + + lock_options = pack( + LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, + self.region_number, + 0x01 if self.locked else 0x00, + 0, + ) + + iee_config = pack( + LITTLE_ENDIAN + UINT32 + UINT32 + "32s" + "32s" + "4s", + region_attribute, + self.page_offset, + key1, + key2, + lock_options, + ) + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + crc: int = crc32_function(iee_config) + return header + options + iee_config + crc.to_bytes(4, Endianness.LITTLE.value) + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: + key1 = align_block(self.key, 32, 0) + key2 = align_block(self.aes_counter, 32, 0) + else: + key_len = len(self.key) + key1 = align_block(self.key[: key_len // 2], 32, 0) + key2 = align_block(self.key[key_len // 2 :], 32, 0) + ret = super().info() + if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: + ret += f"AES Counter mode:{KeyBlobEncryptionIeeCtrModes.get_description(self.ctr_mode.tag)}\n" + ret += f"AES Counter: {self.aes_counter.hex()}\n" + ret += f"Key1: {key1.hex()}\n" + ret += f"Key2: {key2.hex()}\n" + ret += f"Page offset: {self.page_offset:08x}\n" + ret += f"Region number: {self.region_number:02x}\n" + ret += f"Bypass: {self.bypass}\n" + ret += f"Locked: {self.locked}\n" + return ret + + +class EleMessageLoadKeyBLob(EleMessage): + """ELE Message Load KeyBlob.""" + + CMD = MessageIDs.LOAD_KEY_BLOB_REQ.tag + COMMAND_PAYLOAD_WORDS_COUNT = 3 + + def __init__(self, key_identifier: int, keyblob: bytes) -> None: + """Constructor of Load Key Blob class. + + :param key_identifier: ID of key + :param keyblob: Keyblob to be wrapped + """ + super().__init__() + self.key_id = key_identifier + + self.keyblob = keyblob + self.validate() + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + payload = pack( + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, self.key_id, 0, self.command_data_address + ) + payload = self.header_export() + payload + return payload + + @property + def command_data(self) -> bytes: + """Command data to be loaded into target memory space.""" + return self.keyblob + + def info(self) -> str: + """Print information including live data. + + :return: Information about the message. + """ + ret = super().info() + ret += "\n" + ret += f"Key ID: {self.key_id}\n" + ret += f"KeyBlob size: {len(self.keyblob)}\n" + return ret + + +class EleMessageWriteFuse(EleMessage): + """Write Fuse request.""" + + CMD = MessageIDs.WRITE_FUSE.tag + COMMAND_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self, bit_position: int, bit_length: int, lock: bool, payload: int) -> None: + """Constructor. + + This command allows to write to the fuses. + OEM Fuses are accessible depending on the chip lifecycle. + + :param bit_position: Fuse identifier expressed as its position in bit in the fuse map. + :param bit_length: Number of bits to be written. + :param lock: Write lock requirement. When set to 1, fuse words are locked. When unset, no write lock is done. + :param payload: Data to be written + """ + super().__init__() + self.bit_position = bit_position + self.bit_length = bit_length + self.lock = lock + self.payload = payload + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + + ret += pack( + LITTLE_ENDIAN + UINT16 + UINT16 + UINT32, + self.bit_position, + self.bit_length | 0x8000 if self.lock else 0, + self.payload, + ) + return ret + + +class EleMessageWriteShadowFuse(EleMessage): + """Write shadow fuse request.""" + + CMD = MessageIDs.WRITE_SHADOW_FUSE.tag + COMMAND_PAYLOAD_WORDS_COUNT = 2 + + def __init__(self, index: int, value: int) -> None: + """Constructor. + + This command allows to write to the shadow fuses. + + :param index: Fuse identifier expressed as its position in bit in the fuse map. + :param value: Data to be written. + """ + super().__init__() + self.index = index + self.value = value + + def export(self) -> bytes: + """Exports message to final bytes array. + + :return: Bytes representation of message object. + """ + ret = self.header_export() + + ret += pack( + LITTLE_ENDIAN + UINT32 + UINT32, + self.index, + self.value, + ) + return ret + + +class EleMessageEnableApc(EleMessage): + """Enable APC (Application core) ELE Message.""" + + CMD = MessageIDs.ELE_ENABLE_APC_REQ.tag + + +class EleMessageEnableRtc(EleMessage): + """Enable RTC (Real time core) ELE Message.""" + + CMD = MessageIDs.ELE_ENABLE_RTC_REQ.tag + + +class EleMessageResetApcContext(EleMessage): + """Send request to reset APC context ELE Message.""" + + CMD = MessageIDs.ELE_RESET_APC_CTX_REQ.tag diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/exceptions.py new file mode 100644 index 00000000..99a0e961 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/exceptions.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Base for SPSDK exceptions.""" +from typing import Optional + +####################################################################### +# # Secure Provisioning SDK Exceptions +####################################################################### + + +class SPSDKError(Exception): + """Secure Provisioning SDK Base Exception.""" + + fmt = "SPSDK: {description}" + + def __init__(self, desc: Optional[str] = None) -> None: + """Initialize the base SPSDK Exception.""" + super().__init__() + self.description = desc + + def __str__(self) -> str: + return self.fmt.format(description=self.description or "Unknown Error") + + +class SPSDKKeyError(SPSDKError, KeyError): + """SPSDK standard key error.""" + + +class SPSDKValueError(SPSDKError, ValueError): + """SPSDK standard value error.""" + + +class SPSDKTypeError(SPSDKError, TypeError): + """SPSDK standard type error.""" + + +class SPSDKIOError(SPSDKError, IOError): + """SPSDK standard IO error.""" + + +class SPSDKNotImplementedError(SPSDKError, NotImplementedError): + """SPSDK standard not implemented error.""" + + +class SPSDKLengthError(SPSDKError, ValueError): + """SPSDK parsing error of any AHAB containers. + + Input/output data must be of at least container declared length bytes long. + """ + + +class SPSDKOverlapError(SPSDKError, ValueError): + """Data overlap error.""" + + +class SPSDKAlignmentError(SPSDKError, ValueError): + """Data improperly aligned.""" + + +class SPSDKParsingError(SPSDKError): + """Cannot parse binary data.""" + + +class SPSDKCorruptedException(SPSDKError): + """Corrupted Exception.""" + + +class SPSDKUnsupportedOperation(SPSDKError): + """SPSDK unsupported operation error.""" + + +class SPSDKSyntaxError(SyntaxError, SPSDKError): + """SPSDK syntax error.""" + + +class SPSDKFileNotFoundError(FileNotFoundError, SPSDKError): + """SPSDK file not found error.""" + + +class SPSDKAttributeError(SPSDKError, AttributeError): + """SPSDK standard attribute error.""" + + +class SPSDKConnectionError(SPSDKError, ConnectionError): + """SPSDK standard connection error.""" + + +class SPSDKIndexError(SPSDKError, IndexError): + """SPSDK standard index error.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py new file mode 100644 index 00000000..ae89a397 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This module contains AHAB related code.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py new file mode 100644 index 00000000..43f76958 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""AHAB abstract classes.""" + +from struct import calcsize, unpack +from typing import Tuple + +from typing_extensions import Self + +from ...exceptions import SPSDKLengthError, SPSDKParsingError, SPSDKValueError +from ...utils.abstract import BaseClass +from ...utils.misc import check_range + +LITTLE_ENDIAN = "<" +UINT8 = "B" +UINT16 = "H" +UINT32 = "L" +UINT64 = "Q" +RESERVED = 0 + + +class Container(BaseClass): + """Base class for any container.""" + + @classmethod + def fixed_length(cls) -> int: + """Returns the length of a container which is fixed. + + i.e. part of a container holds fixed values, whereas some entries have + variable length. + """ + return calcsize(cls.format()) + + def __len__(self) -> int: + """Returns the total length of a container. + + The length includes the fixed as well as the variable length part. + """ + return self.fixed_length() + + def __repr__(self) -> str: + return "Base AHAB Container class: " + self.__class__.__name__ + + def __str__(self) -> str: + raise NotImplementedError("__str__() is not implemented in base AHAB container class") + + def export(self) -> bytes: + """Serialize object into bytes array.""" + raise NotImplementedError("export() is not implemented in base AHAB container class") + + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array.""" + raise NotImplementedError("parse() is not implemented in base AHAB container class") + + @classmethod + def format(cls) -> str: + """Returns the container data format as defined by struct package. + + The base returns only endianness (LITTLE_ENDIAN). + """ + return LITTLE_ENDIAN + + @classmethod + def _check_fixed_input_length(cls, binary: bytes) -> None: + """Checks the data length and container fixed length. + + This is just a helper function used throughout the code. + + :param Binary: Binary input data. + :raises SPSDKLengthError: If containers length is larger than data length. + """ + data_len = len(binary) + fixed_input_len = cls.fixed_length() + if data_len < fixed_input_len: + raise SPSDKLengthError( + f"Parsing error in fixed part of {cls.__name__} data!\n" + f"Input data must be at least {fixed_input_len} bytes!" + ) + + +class HeaderContainer(Container): + """A container with first byte defined as header - tag, length and version. + + Every "container" in AHAB consists of a header - tag, length and version. + + The only exception is the 'image array' or 'image array entry' respectively + which has no header at all and SRK record, which has 'signing algorithm' + instead of version. But this can be considered as a sort of SRK record + 'version'. + """ + + TAG = 0x00 + VERSION = 0x00 + + def __init__(self, tag: int, length: int, version: int): + """Class object initialized. + + :param tag: container tag. + :param length: container length. + :param version: container version. + """ + self.length = length + self.tag = tag + self.version = version + + def __eq__(self, other: object) -> bool: + if isinstance(other, (HeaderContainer, HeaderContainerInversed)): + if ( + self.tag == other.tag + and self.length == other.length + and self.version == other.version + ): + return True + + return False + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return super().format() + UINT8 + UINT16 + UINT8 + + def validate_header(self) -> None: + """Validates the header of container properties... + + i.e. tag e <0; 255>, otherwise an exception is raised. + :raises SPSDKValueError: Any MAndatory field has invalid value. + """ + if self.tag is None or not check_range(self.tag, end=0xFF): + raise SPSDKValueError(f"AHAB: Head of Container: Invalid TAG Value: {self.tag}") + if self.length is None or not check_range(self.length, end=0xFFFF): + raise SPSDKValueError(f"AHAB: Head of Container: Invalid Length Value: {self.length}") + if self.version is None or not check_range(self.version, end=0xFF): + raise SPSDKValueError(f"AHAB: Head of Container: Invalid Version Value: {self.version}") + + @classmethod + def parse_head(cls, binary: bytes) -> Tuple[int, int, int]: + """Parse binary data to get head members. + + :param binary: Binary data. + :raises SPSDKLengthError: Binary data length is not enough. + :return: Tuple with TAG, LENGTH, VERSION + """ + if len(binary) < 4: + raise SPSDKLengthError( + f"Parsing error in {cls.__name__} container head data!\n" + "Input data must be at least 4 bytes!" + ) + (version, length, tag) = unpack(HeaderContainer.format(), binary) + return tag, length, version + + @classmethod + def check_container_head(cls, binary: bytes) -> None: + """Compares the data length and container length. + + This is just a helper function used throughout the code. + + :param binary: Binary input data. + :raises SPSDKLengthError: If containers length is larger than data length. + :raises SPSDKParsingError: If containers header value doesn't match. + """ + cls._check_fixed_input_length(binary) + data_len = len(binary) + (tag, length, version) = cls.parse_head(binary[: HeaderContainer.fixed_length()]) + + if ( + isinstance(cls.TAG, int) + and tag != cls.TAG + or isinstance(cls.TAG, list) + and not tag in cls.TAG + ): + raise SPSDKParsingError( + f"Parsing error of {cls.__name__} data!\n" + f"Invalid TAG {hex(tag)} loaded, expected {hex(cls.TAG)}!" + ) + + if data_len < length: + raise SPSDKLengthError( + f"Parsing error of {cls.__name__} data!\n" + f"At least {length} bytes expected, got {data_len} bytes!" + ) + + if ( + isinstance(cls.VERSION, int) + and version != cls.VERSION + or isinstance(cls.VERSION, list) + and not version in cls.VERSION + ): + raise SPSDKParsingError( + f"Parsing error of {cls.__name__} data!\n" + f"Invalid VERSION {version} loaded, expected {cls.VERSION}!" + ) + + +class HeaderContainerInversed(HeaderContainer): + """A container with first byte defined as header - tag, length and version. + + It same as "HeaderContainer" only the tag/length/version are in reverse order in binary form. + """ + + @classmethod + def parse_head(cls, binary: bytes) -> Tuple[int, int, int]: + """Parse binary data to get head members. + + :param binary: Binary data. + :raises SPSDKLengthError: Binary data length is not enough. + :return: Tuple with TAG, LENGTH, VERSION + """ + if len(binary) < 4: + raise SPSDKLengthError( + f"Parsing error in {cls.__name__} container head data!\n" + "Input data must be at least 4 bytes!" + ) + # Only SRK Table has splitted tag and version in binary format + (tag, length, version) = unpack(HeaderContainer.format(), binary) + return tag, length, version diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py new file mode 100644 index 00000000..dc552abc --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py @@ -0,0 +1,3699 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Implementation of raw AHAB container support. + +This module represents a generic AHAB container implementation. You can set the +containers values at will. From this perspective, consult with your reference +manual of your device for allowed values. +""" +# pylint: disable=too-many-lines +import logging +import math +import os +from struct import calcsize, pack, unpack +from typing import Any, Dict, List, Optional, Tuple, Union + +from typing_extensions import Self + +spsdk_version = "2.1.0" +from ...crypto.hash import EnumHashAlgorithm, get_hash +from ...crypto.keys import ( + IS_OSCCA_SUPPORTED, + EccCurve, + PublicKey, + PublicKeyEcc, + PublicKeyRsa, + PublicKeySM2, +) +from ...crypto.signature_provider import SignatureProvider, get_signature_provider +from ...crypto.symmetric import ( + aes_cbc_decrypt, + aes_cbc_encrypt, + sm4_cbc_decrypt, + sm4_cbc_encrypt, +) +from ...crypto.types import SPSDKEncoding +from ...crypto.utils import extract_public_key, get_matching_key_id +from ...ele.ele_constants import KeyBlobEncryptionAlgorithm +from ...exceptions import SPSDKError, SPSDKLengthError, SPSDKParsingError, SPSDKValueError +from ...image.ahab.ahab_abstract_interfaces import ( + Container, + HeaderContainer, + HeaderContainerInversed, +) +from ...utils.database import DatabaseManager, get_db, get_families +from ...utils.images import BinaryImage +from ...utils.misc import ( + BinaryPattern, + Endianness, + align, + align_block, + check_range, + extend_block, + find_file, + load_binary, + load_configuration, + load_hex_string, + reverse_bytes_in_longs, + value_to_bytes, + value_to_int, + write_file, +) +from ...utils.schema_validator import CommentedConfig, check_config +from ...utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + +LITTLE_ENDIAN = "<" +UINT8 = "B" +UINT16 = "H" +UINT32 = "L" +UINT64 = "Q" +RESERVED = 0 +CONTAINER_ALIGNMENT = 8 +START_IMAGE_ADDRESS = 0x2000 +START_IMAGE_ADDRESS_NAND = 0x1C00 + + +TARGET_MEMORY_SERIAL_DOWNLOADER = "serial_downloader" +TARGET_MEMORY_NOR = "nor" +TARGET_MEMORY_NAND_4K = "nand_4k" +TARGET_MEMORY_NAND_2K = "nand_2k" + +TARGET_MEMORY_BOOT_OFFSETS = { + TARGET_MEMORY_SERIAL_DOWNLOADER: 0x400, + TARGET_MEMORY_NOR: 0x1000, + TARGET_MEMORY_NAND_4K: 0x400, + TARGET_MEMORY_NAND_2K: 0x400, +} + + +class AHABTags(SpsdkEnum): + """AHAB container related tags.""" + + BLOB = (0x81, "Blob (Wrapped Data Encryption Key).") + CONTAINER_HEADER = (0x87, "Container header.") + SIGNATURE_BLOCK = (0x90, "Signature block.") + CERTIFICATE_UUID = (0xA0, "Certificate with UUID.") + CERTIFICATE_NON_UUID = (0xAF, "Certificate without UUID.") + SRK_TABLE = (0xD7, "SRK table.") + SIGNATURE = (0xD8, "Signature part of signature block.") + SRK_RECORD = (0xE1, "SRK record.") + + +class AHABCoreId(SpsdkEnum): + """AHAB cored IDs.""" + + UNDEFINED = (0, "undefined", "Undefined core") + CORTEX_M33 = (1, "cortex-m33", "Cortex M33") + CORTEX_M4 = (2, "cortex-m4", "Cortex M4") + CORTEX_M7 = (2, "cortex-m7", "Cortex M7") + CORTEX_A55 = (2, "cortex-a55", "Cortex A55") + CORTEX_M4_1 = (3, "cortex-m4_1", "Cortex M4 alternative") + CORTEX_A53 = (4, "cortex-a53", "Cortex A53") + CORTEX_A35 = (4, "cortex-a35", "Cortex A35") + CORTEX_A72 = (5, "cortex-a72", "Cortex A72") + SECO = (6, "seco", "EL enclave") + HDMI_TX = (7, "hdmi-tx", "HDMI Tx") + HDMI_RX = (8, "hdmi-rx", "HDMI Rx") + V2X_1 = (9, "v2x-1", "V2X 1") + V2X_2 = (10, "v2x-2", "V2X 2") + + +def get_key_by_val(dictionary: Dict, val: Any) -> Any: + """Get Dictionary key by its value or default. + + :param dictionary: Dictionary to search in. + :param val: Value to search + :raises SPSDKValueError: In case that dictionary doesn't contains the value. + :return: Key. + """ + for key, value in dictionary.items(): + if value == val: + return key + raise SPSDKValueError( + f"The requested value [{val}] in dictionary [{dictionary}] is not available." + ) + + +class ImageArrayEntry(Container): + """Class representing image array entry as part of image array in the AHAB container. + + Image Array Entry content:: + + +-----+---------------------------------------------------------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+---------------------------------------------------------------+ + |0x00 | Image Offset | + +-----+---------------------------------------------------------------+ + |0x04 | Image Size | + +-----+---------------------------------------------------------------+ + |0x08 | | + |-----+ Load Address (64 bits) | + |0x0C | | + +-----+---------------------------------------------------------------+ + |0x10 | | + |-----+ Entry Point (64 bits) | + |0x14 | | + +-----+---------------------------------------------------------------+ + |0x18 | Flags | + +-----+---------------------------------------------------------------+ + |0x1C | Image meta data | + +-----+---------------------------------------------------------------+ + |0x20 | | + |-----+ Hash (512 bits) | + |.... | | + +-----+---------------------------------------------------------------+ + |0x60 | IV (256 bits) | + +-----+---------------------------------------------------------------+ + + """ + + IMAGE_OFFSET_LEN = 4 + IMAGE_SIZE_LEN = 4 + LOAD_ADDRESS_LEN = 8 + ENTRY_POINT_ADDRESS_LEN = 8 + FLAGS_LEN = 4 + IMAGE_META_DATA_LEN = 4 + HASH_LEN = 64 + IV_LEN = 32 + FLAGS_TYPE_OFFSET = 0 + FLAGS_TYPE_SIZE = 4 + FLAGS_TYPES = { + "csf": 0x01, + "scd": 0x02, + "executable": 0x03, + "data": 0x04, + "dcd_image": 0x05, + "seco": 0x06, + "provisioning_image": 0x07, + "dek_validation_fcb_chk": 0x08, + "provisioning_data": 0x09, + "executable_fast_boot_image": 0x0A, + "v2x_primary": 0x0B, + "v2x_secondary": 0x0C, + "v2x_rom_patch": 0x0D, + "v2x_dummy": 0x0E, + } + FLAGS_CORE_ID_OFFSET = 4 + FLAGS_CORE_ID_SIZE = 4 + FLAGS_HASH_OFFSET = 8 + FLAGS_HASH_SIZE = 3 + FLAGS_IS_ENCRYPTED_OFFSET = 11 + FLAGS_IS_ENCRYPTED_SIZE = 1 + FLAGS_BOOT_FLAGS_OFFSET = 16 + FLAGS_BOOT_FLAGS_SIZE = 15 + METADATA_START_CPU_ID_OFFSET = 0 + METADATA_START_CPU_ID_SIZE = 10 + METADATA_MU_CPU_ID_OFFSET = 10 + METADATA_MU_CPU_ID_SIZE = 10 + METADATA_START_PARTITION_ID_OFFSET = 20 + METADATA_START_PARTITION_ID_SIZE = 8 + + IMAGE_ALIGNMENTS = { + TARGET_MEMORY_SERIAL_DOWNLOADER: 512, + TARGET_MEMORY_NOR: 1024, + TARGET_MEMORY_NAND_2K: 2048, + TARGET_MEMORY_NAND_4K: 4096, + } + + def __init__( + self, + parent: "AHABContainer", + image: Optional[bytes] = None, + image_offset: int = 0, + load_address: int = 0, + entry_point: int = 0, + flags: int = 0, + image_meta_data: int = 0, + image_hash: Optional[bytes] = None, + image_iv: Optional[bytes] = None, + already_encrypted_image: bool = False, + ) -> None: + """Class object initializer. + + :param parent: Parent AHAB Container object. + :param image: Image in bytes. + :param image_offset: Offset in bytes from start of container to beginning of image. + :param load_address: Address the image is written to in memory (absolute address in system memory map). + :param entry_point: Entry point of image (absolute address). Only valid for executable image types. + For other image types the value is irrelevant. + :param flags: flags. + :param image_meta_data: image meta-data. + :param image_hash: SHA of image (512 bits) in big endian. Left + aligned and padded with zeroes for hash sizes below 512 bits. + :param image_iv: SHA256 of plain text image (256 bits) in big endian. + :param already_encrypted_image: The input image is already encrypted. + Used only for encrypted images. + """ + self._image_offset = 0 + self.parent = parent + self.flags = flags + self.already_encrypted_image = already_encrypted_image + self.image = image if image else b"" + self.image_offset = image_offset + self.image_size = self._get_valid_size(self.image) + self.load_address = load_address + self.entry_point = entry_point + self.image_meta_data = image_meta_data + self.image_hash = image_hash + self.image_iv = ( + image_iv or get_hash(self.plain_image, algorithm=EnumHashAlgorithm.SHA256) + if self.flags_is_encrypted + else bytes(self.IV_LEN) + ) + + @property + def _ahab_container(self) -> "AHABContainer": + """AHAB Container object.""" + return self.parent + + @property + def _ahab_image(self) -> "AHABImage": + """AHAB Image object.""" + return self._ahab_container.parent + + @property + def image_offset(self) -> int: + """Image offset.""" + return self._image_offset + self._ahab_container.container_offset + + @image_offset.setter + def image_offset(self, offset: int) -> None: + """Image offset. + + :param offset: Image offset. + """ + self._image_offset = offset - self._ahab_container.container_offset + + @property + def image_offset_real(self) -> int: + """Real offset in Bootable image.""" + target_memory = self._ahab_image.target_memory + return self.image_offset + TARGET_MEMORY_BOOT_OFFSETS[target_memory] + + def __eq__(self, other: object) -> bool: + if isinstance(other, ImageArrayEntry): + if ( + self.image_offset # pylint: disable=too-many-boolean-expressions + == other.image_offset + and self.image_size == other.image_size + and self.load_address == other.load_address + and self.entry_point == other.entry_point + and self.flags == other.flags + and self.image_meta_data == other.image_meta_data + and self.image_hash == other.image_hash + and self.image_iv == other.image_iv + ): + return True + + return False + + def __repr__(self) -> str: + return f"AHAB Image Array Entry, load address({hex(self.load_address)})" + + def __str__(self) -> str: + return ( + "AHAB Image Array Entry:\n" + f" Image size: {self.image_size}B\n" + f" Image offset in table: {hex(self.image_offset)}\n" + f" Image offset real: {hex(self.image_offset_real)}\n" + f" Entry point: {hex(self.entry_point)}\n" + f" Load address: {hex(self.load_address)}\n" + f" Flags: {hex(self.flags)})\n" + f" Meta data: {hex(self.image_meta_data)})\n" + f" Image hash: {self.image_hash.hex() if self.image_hash else 'Not available'})\n" + f" Image IV: {self.image_iv.hex()})\n" + ) + + @property + def image(self) -> bytes: + """Image data for this Image array entry. + + The class decide by flags if encrypted of plain data has been returned. + + :raises SPSDKError: Invalid Image - Image is not encrypted yet. + :return: Image bytes. + """ + # if self.flags_is_encrypted and not self.already_encrypted_image: + # raise SPSDKError("Image is NOT encrypted, yet.") + + if self.flags_is_encrypted and self.already_encrypted_image: + return self.encrypted_image + return self.plain_image + + @image.setter + def image(self, data: bytes) -> None: + """Image data for this Image array entry. + + The class decide by flags if encrypted of plain data has been stored. + """ + input_image = align_block( + data, 16 if self.flags_is_encrypted else 4, padding=RESERVED + ) # align to encryptable block + self.plain_image = input_image if not self.already_encrypted_image else b"" + self.encrypted_image = input_image if self.already_encrypted_image else b"" + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() # endianness from base class + + UINT32 # Image Offset + + UINT32 # Image Size + + UINT64 # Load Address + + UINT64 # Entry Point + + UINT32 # Flags + + UINT32 # Image Meta Data + + "64s" # HASH + + "32s" # Input Vector + ) + + def update_fields(self) -> None: + """Updates the image fields in container based on provided image.""" + # self.image = align_block(self.image, self.get_valid_alignment(), 0) + self.image_size = self._get_valid_size(self.image) + algorithm = self.get_hash_from_flags(self.flags) + self.image_hash = extend_block( + get_hash(self.image, algorithm=algorithm), + self.HASH_LEN, + padding=0, + ) + if not self.image_iv and self.flags_is_encrypted: + self.image_iv = get_hash(self.plain_image, algorithm=EnumHashAlgorithm.SHA256) + + @staticmethod + def create_meta(start_cpu_id: int = 0, mu_cpu_id: int = 0, start_partition_id: int = 0) -> int: + """Create meta data field. + + :param start_cpu_id: ID of CPU to start, defaults to 0 + :param mu_cpu_id: ID of MU for selected CPU to start, defaults to 0 + :param start_partition_id: ID of partition to start, defaults to 0 + :return: Image meta data field. + """ + meta_data = start_cpu_id + meta_data |= mu_cpu_id << 10 + meta_data |= start_partition_id << 20 + return meta_data + + @staticmethod + def create_flags( + image_type: str = "executable", + core_id: AHABCoreId = AHABCoreId.CORTEX_M33, + hash_type: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + is_encrypted: bool = False, + boot_flags: int = 0, + ) -> int: + """Create flags field. + + :param image_type: Type of image, defaults to "executable" + :param core_id: Core ID, defaults to "cortex-m33" + :param hash_type: Hash type, defaults to sha256 + :param is_encrypted: Is image encrypted, defaults to False + :param boot_flags: Boot flags controlling the SCFW boot, defaults to 0 + :return: Image flags data field. + """ + flags_data = ImageArrayEntry.FLAGS_TYPES[image_type] + flags_data |= core_id.tag << ImageArrayEntry.FLAGS_CORE_ID_OFFSET + flags_data |= { + EnumHashAlgorithm.SHA256: 0x0, + EnumHashAlgorithm.SHA384: 0x1, + EnumHashAlgorithm.SHA512: 0x2, + EnumHashAlgorithm.SM3: 0x3, + }[hash_type] << ImageArrayEntry.FLAGS_HASH_OFFSET + flags_data |= 1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET if is_encrypted else 0 + flags_data |= boot_flags << ImageArrayEntry.FLAGS_BOOT_FLAGS_OFFSET + + return flags_data + + @staticmethod + def get_hash_from_flags(flags: int) -> EnumHashAlgorithm: + """Get Hash algorithm name from flags. + + :param flags: Value of flags. + :return: Hash name. + """ + hash_val = (flags >> ImageArrayEntry.FLAGS_HASH_OFFSET) & ( + (1 << ImageArrayEntry.FLAGS_HASH_SIZE) - 1 + ) + return { + 0x00: EnumHashAlgorithm.SHA256, + 0x01: EnumHashAlgorithm.SHA384, + 0x02: EnumHashAlgorithm.SHA512, + 0x03: EnumHashAlgorithm.SM3, + }[hash_val] + + @property + def flags_image_type(self) -> str: + """Get Image type name from flags. + + :return: Image type name + """ + image_type_val = (self.flags >> ImageArrayEntry.FLAGS_TYPE_OFFSET) & ( + (1 << ImageArrayEntry.FLAGS_TYPE_SIZE) - 1 + ) + try: + return get_key_by_val(ImageArrayEntry.FLAGS_TYPES, image_type_val) + except SPSDKValueError: + return f"Unknown Image Type {image_type_val}" + + @property + def flags_core_id(self) -> int: + """Get Core ID from flags. + + :return: Core ID + """ + return (self.flags >> ImageArrayEntry.FLAGS_CORE_ID_OFFSET) & ( + (1 << ImageArrayEntry.FLAGS_CORE_ID_SIZE) - 1 + ) + + @property + def flags_is_encrypted(self) -> bool: + """Get Is encrypted property from flags. + + :return: True if is encrypted, false otherwise + """ + return bool( + (self.flags >> ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET) + & ((1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_SIZE) - 1) + ) + + @property + def flags_boot_flags(self) -> int: + """Get boot flags property from flags. + + :return: Boot flags + """ + return (self.flags >> ImageArrayEntry.FLAGS_BOOT_FLAGS_OFFSET) & ( + (1 << ImageArrayEntry.FLAGS_BOOT_FLAGS_SIZE) - 1 + ) + + @property + def metadata_start_cpu_id(self) -> int: + """Get CPU ID property from Meta data. + + :return: Start CPU ID + """ + return (self.image_meta_data >> ImageArrayEntry.METADATA_START_CPU_ID_OFFSET) & ( + (1 << ImageArrayEntry.METADATA_START_CPU_ID_SIZE) - 1 + ) + + @property + def metadata_mu_cpu_id(self) -> int: + """Get Start CPU Memory Unit ID property from Meta data. + + :return: Start CPU MU ID + """ + return (self.image_meta_data >> ImageArrayEntry.METADATA_MU_CPU_ID_OFFSET) & ( + (1 << ImageArrayEntry.METADATA_MU_CPU_ID_SIZE) - 1 + ) + + @property + def metadata_start_partition_id(self) -> int: + """Get Start Partition ID property from Meta data. + + :return: Start Partition ID + """ + return (self.image_meta_data >> ImageArrayEntry.METADATA_START_PARTITION_ID_OFFSET) & ( + (1 << ImageArrayEntry.METADATA_START_PARTITION_ID_SIZE) - 1 + ) + + def export(self) -> bytes: + """Serializes container object into bytes in little endian. + + The hash and IV are kept in big endian form. + + :return: bytes representing container content. + """ + # hash: fixed at 512 bits, left aligned and padded with zeros for hash below 512 bits. + # In case the hash is shorter, the pack() (in little endian mode) should grant, that the + # hash is left aligned and padded with zeros due to the '64s' formatter. + # iv: fixed at 256 bits. + data = pack( + self.format(), + self._image_offset, + self.image_size, + self.load_address, + self.entry_point, + self.flags, + self.image_meta_data, + self.image_hash, + self.image_iv, + ) + + return data + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + if self.image is None or self._get_valid_size(self.image) != self.image_size: + raise SPSDKValueError("Image Entry: Invalid Image binary.") + if self.image_offset is None or not check_range(self.image_offset, end=(1 << 32) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Offset: {self.image_offset}") + if self.image_size is None or not check_range(self.image_size, end=(1 << 32) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Size: {self.image_size}") + if self.load_address is None or not check_range(self.load_address, end=(1 << 64) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Load address: {self.load_address}") + if self.entry_point is None or not check_range(self.entry_point, end=(1 << 64) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Entry point: {self.entry_point}") + if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Flags: {self.flags}") + if self.image_meta_data is None or not check_range(self.image_meta_data, end=(1 << 32) - 1): + raise SPSDKValueError(f"Image Entry: Invalid Image Meta data: {self.image_meta_data}") + if ( + self.image_hash is None + or not any(self.image_hash) + or len(self.image_hash) != self.HASH_LEN + ): + raise SPSDKValueError("Image Entry: Invalid Image Hash.") + + @classmethod + def parse(cls, data: bytes, parent: "AHABContainer") -> Self: # type: ignore # pylint: disable=arguments-differ + """Parse input binary chunk to the container object. + + :param parent: Parent AHABContainer object. + :param data: Binary data with Image Array Entry block to parse. + :raises SPSDKLengthError: If invalid length of image is detected. + :raises SPSDKValueError: Invalid hash for image. + :return: Object recreated from the binary data. + """ + binary_size = len(data) + # Just updates offsets from AHAB Image start As is feature of none xip containers + ImageArrayEntry._check_fixed_input_length(data) + ( + image_offset, + image_size, + load_address, + entry_point, + flags, + image_meta_data, + image_hash, + image_iv, + ) = unpack(ImageArrayEntry.format(), data[: ImageArrayEntry.fixed_length()]) + + iae = cls( + parent=parent, + image_offset=0, + image=None, + load_address=load_address, + entry_point=entry_point, + flags=flags, + image_meta_data=image_meta_data, + image_hash=image_hash, + image_iv=image_iv, + already_encrypted_image=bool( + (flags >> ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET) + & ((1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_SIZE) - 1) + ), + ) + iae._image_offset = image_offset + + iae_offset = ( + AHABContainer.fixed_length() + + parent.image_array_len * ImageArrayEntry.fixed_length() + + parent.container_offset + ) + + logger.debug( + ( + "Parsing Image array Entry:\n" + f"Image offset: {hex(iae.image_offset)}\n" + f"Image offset raw: {hex(iae._image_offset)}\n" + f"Image offset real: {hex(iae.image_offset_real)}" + ) + ) + if iae.image_offset + image_size - iae_offset > binary_size: + raise SPSDKLengthError( + "Container data image is out of loaded binary:" + f"Image entry record has end of image at {hex(iae.image_offset + image_size - iae_offset)}," + f" but the loaded image length has only {hex(binary_size)}B size." + ) + image = data[iae.image_offset - iae_offset : iae.image_offset - iae_offset + image_size] + image_hash_cmp = extend_block( + get_hash(image, algorithm=ImageArrayEntry.get_hash_from_flags(flags)), + ImageArrayEntry.HASH_LEN, + padding=0, + ) + if image_hash != image_hash_cmp: + raise SPSDKValueError("Parsed Container data image has invalid HASH!") + iae.image = image + return iae + + @staticmethod + def load_from_config(parent: "AHABContainer", config: Dict[str, Any]) -> "ImageArrayEntry": + """Converts the configuration option into an AHAB image array entry object. + + "config" content of container configurations. + + :param parent: Parent AHABContainer object. + :param config: Configuration of ImageArray. + :return: Container Header Image Array Entry object. + """ + image_path = config.get("image_path") + search_paths = parent.search_paths + assert isinstance(image_path, str) + is_encrypted = config.get("is_encrypted", False) + meta_data = ImageArrayEntry.create_meta( + value_to_int(config.get("meta_data_start_cpu_id", 0)), + value_to_int(config.get("meta_data_mu_cpu_id", 0)), + value_to_int(config.get("meta_data_start_partition_id", 0)), + ) + image_data = load_binary(image_path, search_paths=search_paths) + flags = ImageArrayEntry.create_flags( + image_type=config.get("image_type", "executable"), + core_id=AHABCoreId.from_label(config.get("core_id", "cortex-m33")), + hash_type=EnumHashAlgorithm.from_label(config.get("hash_type", "sha256")), + is_encrypted=is_encrypted, + boot_flags=value_to_int(config.get("boot_flags", 0)), + ) + return ImageArrayEntry( + parent=parent, + image=image_data, + image_offset=value_to_int(config.get("image_offset", 0)), + load_address=value_to_int(config.get("load_address", 0)), + entry_point=value_to_int(config.get("entry_point", 0)), + flags=flags, + image_meta_data=meta_data, + image_iv=None, # IV data are updated by UpdateFields function + ) + + def create_config(self, index: int, image_index: int, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image data blob. + + :param index: Container index. + :param image_index: Data Image index. + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + ret_cfg: Dict[str, Union[str, int, bool]] = {} + image_name = "N/A" + if self.plain_image: + image_name = f"container{index}_image{image_index}_{self.flags_image_type}.bin" + write_file(self.plain_image, os.path.join(data_path, image_name), "wb") + if self.encrypted_image: + image_name_encrypted = ( + f"container{index}_image{image_index}_{self.flags_image_type}_encrypted.bin" + ) + write_file(self.encrypted_image, os.path.join(data_path, image_name_encrypted), "wb") + if image_name == "N/A": + image_name = image_name_encrypted + + ret_cfg["image_path"] = image_name + ret_cfg["image_offset"] = hex(self.image_offset) + ret_cfg["load_address"] = hex(self.load_address) + ret_cfg["entry_point"] = hex(self.entry_point) + ret_cfg["image_type"] = self.flags_image_type + core_ids = self.parent.parent._database.get_dict(DatabaseManager.AHAB, "core_ids") + ret_cfg["core_id"] = core_ids.get(self.flags_core_id, f"Unknown ID: {self.flags_core_id}") + ret_cfg["is_encrypted"] = bool(self.flags_is_encrypted) + ret_cfg["boot_flags"] = self.flags_boot_flags + ret_cfg["meta_data_start_cpu_id"] = self.metadata_start_cpu_id + ret_cfg["meta_data_mu_cpu_id"] = self.metadata_mu_cpu_id + ret_cfg["meta_data_start_partition_id"] = self.metadata_start_partition_id + ret_cfg["hash_type"] = self.get_hash_from_flags(self.flags).label + + return ret_cfg + + def get_valid_alignment(self) -> int: + """Get valid alignment for AHAB container and memory target. + + :return: AHAB valid alignment + """ + if ( + self.flags_image_type == "seco" + and self.parent.parent.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER + ): + return 4 + + return max([self.IMAGE_ALIGNMENTS[self._ahab_image.target_memory], 1024]) + + def _get_valid_size(self, image: Optional[bytes]) -> int: + """Get valid image size that will be stored. + + :return: AHAB valid image size + """ + if not image: + return 0 + return align(len(image), 4 if self.flags_image_type == "seco" else 1) + + def get_valid_offset(self, original_offset: int) -> int: + """Get valid offset for AHAB container. + + :param original_offset: Offset that should be updated to valid one + :return: AHAB valid offset + """ + alignment = self.get_valid_alignment() + alignment = max( + alignment, + self.parent.parent._database.get_int( + DatabaseManager.AHAB, "valid_offset_minimal_alignment", 4 + ), + ) + return align(original_offset, alignment) + + +class SRKRecord(HeaderContainerInversed): + """Class representing SRK (Super Root Key) record as part of SRK table in the AHAB container. + + The class holds information about RSA/ECDSA signing algorithms. + + SRK Record:: + + +-----+---------------------------------------------------------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+---------------------------------------------------------------+ + |0x00 | Tag | Length of SRK | Signing Algo | + +-----+---------------------------------------------------------------+ + |0x04 | Hash Algo | Key Size/Curve | Not Used | SRK Flags | + +-----+---------------------------------------------------------------+ + |0x08 | RSA modulus len / ECDSA X len | RSA exponent len / ECDSA Y len| + +-----+---------------------------------------------------------------+ + |0x0C | RSA modulus (big endian) / ECDSA X (big endian) | + +-----+---------------------------------------------------------------+ + |... | RSA exponent (big endian) / ECDSA Y (big endian) | + +-----+---------------------------------------------------------------+ + + """ + + TAG = AHABTags.SRK_RECORD.tag + VERSION = [0x21, 0x27, 0x28] # type: ignore + VERSION_ALGORITHMS = {"rsa": 0x21, "ecdsa": 0x27, "sm2": 0x28} + HASH_ALGORITHM = { + EnumHashAlgorithm.SHA256: 0x0, + EnumHashAlgorithm.SHA384: 0x1, + EnumHashAlgorithm.SHA512: 0x2, + EnumHashAlgorithm.SM3: 0x3, + } + ECC_KEY_TYPE = {EccCurve.SECP521R1: 0x3, EccCurve.SECP384R1: 0x2, EccCurve.SECP256R1: 0x1} + RSA_KEY_TYPE = {2048: 0x5, 4096: 0x7} + SM2_KEY_TYPE = 0x8 + KEY_SIZES = { + 0x1: (32, 32), + 0x2: (48, 48), + 0x3: (66, 66), + 0x5: (128, 128), + 0x7: (256, 256), + 0x8: (32, 32), + } + + FLAGS_CA_MASK = 0x80 + + def __init__( + self, + src_key: Optional[PublicKey] = None, + signing_algorithm: str = "rsa", + hash_type: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + key_size: int = 0, + srk_flags: int = 0, + crypto_param1: bytes = b"", + crypto_param2: bytes = b"", + ): + """Class object initializer. + + :param src_key: Optional source public key used to create the SRKRecord + :param signing_algorithm: signing algorithm type. + :param hash_type: hash algorithm type. + :param key_size: key (curve) size. + :param srk_flags: flags. + :param crypto_param1: RSA modulus (big endian) or ECDSA X (big endian) + :param crypto_param2: RSA exponent (big endian) or ECDSA Y (big endian) + """ + super().__init__( + tag=self.TAG, length=-1, version=self.VERSION_ALGORITHMS[signing_algorithm] + ) + self.signing_algorithm = signing_algorithm + self.src_key = src_key + self.hash_algorithm = self.HASH_ALGORITHM[hash_type] + self.key_size = key_size + self.srk_flags = srk_flags + self.crypto_param1 = crypto_param1 + self.crypto_param2 = crypto_param2 + + def __eq__(self, other: object) -> bool: + if isinstance(other, SRKRecord): + if ( + super().__eq__(other) # pylint: disable=too-many-boolean-expressions + and self.hash_algorithm == other.hash_algorithm + and self.key_size == other.key_size + and self.srk_flags == other.srk_flags + and self.crypto_param1 == other.crypto_param1 + and self.crypto_param2 == other.crypto_param2 + ): + return True + + return False + + def __len__(self) -> int: + return super().__len__() + len(self.crypto_param1) + len(self.crypto_param2) + + def __repr__(self) -> str: + return f"AHAB SRK record, key: {self.get_key_name()}" + + def __str__(self) -> str: + return ( + "AHAB SRK Record:\n" + f" Key: {self.get_key_name()}\n" + f" SRK flags: {hex(self.srk_flags)}\n" + f" Param 1 value: {self.crypto_param1.hex()})\n" + f" Param 2 value: {self.crypto_param2.hex()})\n" + ) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() + + UINT8 # Hash Algorithm + + UINT8 # Key Size / Curve + + UINT8 # Not Used + + UINT8 # SRK Flags + + UINT16 # crypto_param2_len + + UINT16 # crypto_param1_len + ) + + def update_fields(self) -> None: + """Update all fields depended on input values.""" + self.length = len(self) + + def export(self) -> bytes: + """Export one SRK record, little big endian format. + + The crypto parameters (X/Y for ECDSA or modulus/exponent) are kept in + big endian form. + + :return: bytes representing container content. + """ + return ( + pack( + self.format(), + self.tag, + self.length, + self.version, + self.hash_algorithm, + self.key_size, + RESERVED, + self.srk_flags, + len(self.crypto_param1), + len(self.crypto_param2), + ) + + self.crypto_param1 + + self.crypto_param2 + ) + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + self.validate_header() + if self.hash_algorithm is None or not check_range(self.hash_algorithm, end=2): + raise SPSDKValueError(f"SRK record: Invalid Hash algorithm: {self.hash_algorithm}") + + if self.srk_flags is None or not check_range(self.srk_flags, end=0xFF): + raise SPSDKValueError(f"SRK record: Invalid Flags: {self.srk_flags}") + + if self.version == 0x21: # Signing algorithm RSA + if self.key_size not in self.RSA_KEY_TYPE.values(): + raise SPSDKValueError( + f"SRK record: Invalid Key size in match to RSA signing algorithm: {self.key_size}" + ) + elif self.version == 0x27: # Signing algorithm ECDSA + if self.key_size not in self.ECC_KEY_TYPE.values(): + raise SPSDKValueError( + f"SRK record: Invalid Key size in match to ECDSA signing algorithm: {self.key_size}" + ) + elif self.version == 0x28: # Signing algorithm SM2 + if self.key_size != self.SM2_KEY_TYPE: + raise SPSDKValueError( + f"SRK record: Invalid Key size in match to SM2 signing algorithm: {self.key_size}" + ) + else: + raise SPSDKValueError(f"SRK record: Invalid Signing algorithm: {self.version}") + + # Check lengths + + if ( + self.crypto_param1 is None + or len(self.crypto_param1) != self.KEY_SIZES[self.key_size][0] + ): + raise SPSDKValueError( + f"SRK record: Invalid Crypto parameter 1: 0x{self.crypto_param1.hex()}" + ) + + if ( + self.crypto_param2 is None + or len(self.crypto_param2) != self.KEY_SIZES[self.key_size][1] + ): + raise SPSDKValueError( + f"SRK record: Invalid Crypto parameter 2: 0x{self.crypto_param2.hex()}" + ) + + computed_length = ( + self.fixed_length() + + self.KEY_SIZES[self.key_size][0] + + self.KEY_SIZES[self.key_size][1] + ) + if self.length != len(self) or self.length != computed_length: + raise SPSDKValueError( + f"SRK record: Invalid Length: Length of SRK:{self.length}" + f", Computed Length of SRK:{computed_length}" + ) + + @staticmethod + def create_from_key(public_key: PublicKey, srk_flags: int = 0) -> "SRKRecord": + """Create instance from key data. + + :param public_key: Loaded public key. + :param srk_flags: SRK flags for key. + :raises SPSDKValueError: Unsupported keys size is detected. + """ + if isinstance(public_key, PublicKeyRsa): + par_n: int = public_key.public_numbers.n + par_e: int = public_key.public_numbers.e + key_size = SRKRecord.RSA_KEY_TYPE[public_key.key_size] + return SRKRecord( + src_key=public_key, + signing_algorithm="rsa", + hash_type=EnumHashAlgorithm.SHA256, + key_size=key_size, + srk_flags=srk_flags, + crypto_param1=par_n.to_bytes( + length=SRKRecord.KEY_SIZES[key_size][0], byteorder=Endianness.BIG.value + ), + crypto_param2=par_e.to_bytes( + length=SRKRecord.KEY_SIZES[key_size][1], byteorder=Endianness.BIG.value + ), + ) + + elif isinstance(public_key, PublicKeyEcc): + par_x: int = public_key.x + par_y: int = public_key.y + key_size = SRKRecord.ECC_KEY_TYPE[public_key.curve] + + if not public_key.key_size in [256, 384, 521]: + raise SPSDKValueError( + f"Unsupported ECC key for AHAB container: {public_key.key_size}" + ) + hash_type = { + 256: EnumHashAlgorithm.SHA256, + 384: EnumHashAlgorithm.SHA384, + 521: EnumHashAlgorithm.SHA512, + }[public_key.key_size] + + return SRKRecord( + signing_algorithm="ecdsa", + hash_type=hash_type, + key_size=key_size, + srk_flags=srk_flags, + crypto_param1=par_x.to_bytes( + length=SRKRecord.KEY_SIZES[key_size][0], byteorder=Endianness.BIG.value + ), + crypto_param2=par_y.to_bytes( + length=SRKRecord.KEY_SIZES[key_size][1], byteorder=Endianness.BIG.value + ), + ) + + assert isinstance(public_key, PublicKeySM2), "Unsupported public key for SRK record" + param1: bytes = value_to_bytes("0x" + public_key.public_numbers[:64], byte_cnt=32) + param2: bytes = value_to_bytes("0x" + public_key.public_numbers[64:], byte_cnt=32) + assert len(param1 + param2) == 64, "Invalid length of the SM2 key" + key_size = SRKRecord.SM2_KEY_TYPE + return SRKRecord( + src_key=public_key, + signing_algorithm="sm2", + hash_type=EnumHashAlgorithm.SM3, + key_size=key_size, + srk_flags=srk_flags, + crypto_param1=param1, + crypto_param2=param2, + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with SRK record block to parse. + :raises SPSDKLengthError: Invalid length of SRK record data block. + :return: SRK record recreated from the binary data. + """ + SRKRecord.check_container_head(data) + ( + _, # tag + container_length, + signing_algo, + hash_algo, + key_size_curve, + _, # reserved + srk_flags, + crypto_param1_len, + crypto_param2_len, + ) = unpack(SRKRecord.format(), data[: SRKRecord.fixed_length()]) + + # Although we know from the total length, that we have enough bytes, + # the crypto param lengths may be set improperly and we may get into trouble + # while parsing. So we need to check the lengths as well. + param_length = SRKRecord.fixed_length() + crypto_param1_len + crypto_param2_len + if container_length < param_length: + raise SPSDKLengthError( + "Parsing error of SRK Record data." + "SRK record lengths mismatch. Sum of lengths declared in container " + f"({param_length} (= {SRKRecord.fixed_length()} + {crypto_param1_len} + " + f"{crypto_param2_len})) doesn't match total length declared in container ({container_length})!" + ) + crypto_param1 = data[ + SRKRecord.fixed_length() : SRKRecord.fixed_length() + crypto_param1_len + ] + crypto_param2 = data[ + SRKRecord.fixed_length() + + crypto_param1_len : SRKRecord.fixed_length() + + crypto_param1_len + + crypto_param2_len + ] + + return cls( + signing_algorithm=get_key_by_val(SRKRecord.VERSION_ALGORITHMS, signing_algo), + hash_type=get_key_by_val(SRKRecord.HASH_ALGORITHM, hash_algo), + key_size=key_size_curve, + srk_flags=srk_flags, + crypto_param1=crypto_param1, + crypto_param2=crypto_param2, + ) + + def get_key_name(self) -> str: + """Get text key name in SRK record. + + :return: Key name. + """ + if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "rsa": + return f"rsa{get_key_by_val(self.RSA_KEY_TYPE, self.key_size)}" + if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "ecdsa": + return get_key_by_val(self.ECC_KEY_TYPE, self.key_size) + if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2": + return "sm2" + return "Unknown Key name!" + + def get_public_key(self, encoding: SPSDKEncoding = SPSDKEncoding.PEM) -> bytes: + """Store the SRK public key as a file. + + :param encoding: Public key encoding style, default is PEM. + :raises SPSDKError: Unsupported public key + """ + par1 = int.from_bytes(self.crypto_param1, Endianness.BIG.value) + par2 = int.from_bytes(self.crypto_param2, Endianness.BIG.value) + key: Union[PublicKey, PublicKeyEcc, PublicKeyRsa, PublicKeySM2] + if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "rsa": + # RSA Key to store + key = PublicKeyRsa.recreate(par1, par2) + elif get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "ecdsa": + # ECDSA Key to store + curve = get_key_by_val(self.ECC_KEY_TYPE, self.key_size) + key = PublicKeyEcc.recreate(par1, par2, curve=curve) + elif get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2" and IS_OSCCA_SUPPORTED: + encoding = SPSDKEncoding.DER + key = PublicKeySM2.recreate(self.crypto_param1 + self.crypto_param2) + + return key.export(encoding=encoding) + + +class SRKTable(HeaderContainerInversed): + """Class representing SRK (Super Root Key) table in the AHAB container as part of signature block. + + SRK Table:: + + +-----+---------------------------------------------------------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+---------------------------------------------------------------+ + |0x00 | Tag | Length of SRK Table | Version | + +-----+---------------------------------------------------------------+ + |0x04 | SRK Record 1 | + +-----+---------------------------------------------------------------+ + |... | SRK Record 2 | + +-----+---------------------------------------------------------------+ + |... | SRK Record 3 | + +-----+---------------------------------------------------------------+ + |... | SRK Record 4 | + +-----+---------------------------------------------------------------+ + + """ + + TAG = AHABTags.SRK_TABLE.tag + VERSION = 0x42 + SRK_RECORDS_CNT = 4 + + def __init__(self, srk_records: Optional[List[SRKRecord]] = None) -> None: + """Class object initializer. + + :param srk_records: list of SRKRecord objects. + """ + super().__init__(tag=self.TAG, length=-1, version=self.VERSION) + self._srk_records: List[SRKRecord] = srk_records or [] + self.length = len(self) + + def __repr__(self) -> str: + return f"AHAB SRK TABLE, keys count: {len(self._srk_records)}" + + def __str__(self) -> str: + return ( + "AHAB SRK table:\n" + f" Keys count: {len(self._srk_records)}\n" + f" Length: {self.length}\n" + f"SRK table HASH: {self.compute_srk_hash().hex()}" + ) + + def clear(self) -> None: + """Clear the SRK Table Object.""" + self._srk_records.clear() + self.length = -1 + + def add_record(self, public_key: PublicKey, srk_flags: int = 0) -> None: + """Add SRK table record. + + :param public_key: Loaded public key. + :param srk_flags: SRK flags for key. + """ + self._srk_records.append( + SRKRecord.create_from_key(public_key=public_key, srk_flags=srk_flags) + ) + self.length = len(self) + + def __eq__(self, other: object) -> bool: + """Compares for equality with other SRK Table objects. + + :param other: object to compare with. + :return: True on match, False otherwise. + """ + if isinstance(other, SRKTable): + if super().__eq__(other) and self._srk_records == other._srk_records: + return True + + return False + + def __len__(self) -> int: + records_len = 0 + for record in self._srk_records: + records_len += len(record) + return super().__len__() + records_len + + def update_fields(self) -> None: + """Update all fields depended on input values.""" + for rec in self._srk_records: + rec.update_fields() + self.length = len(self) + + def compute_srk_hash(self) -> bytes: + """Computes a SHA256 out of all SRK records. + + :return: SHA256 computed over SRK records. + """ + return get_hash(data=self.export(), algorithm=EnumHashAlgorithm.SHA256) + + def get_source_keys(self) -> List[PublicKey]: + """Return list of source public keys. + + Either from the src_key field or recreate them. + :return: List of public keys. + """ + ret = [] + for srk in self._srk_records: + if srk.src_key: + # return src key if available + ret.append(srk.src_key) + else: + # recreate the key + ret.append(PublicKey.parse(srk.get_public_key())) + return ret + + def export(self) -> bytes: + """Serializes container object into bytes in little endian. + + :return: bytes representing container content. + """ + data = pack(self.format(), self.tag, self.length, self.version) + + for srk_record in self._srk_records: + data += srk_record.export() + + return data + + def validate(self, data: Dict[str, Any]) -> None: + """Validate object data. + + :param data: Additional validation data. + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + self.validate_header() + if self._srk_records is None or len(self._srk_records) != self.SRK_RECORDS_CNT: + raise SPSDKValueError(f"SRK table: Invalid SRK records: {self._srk_records}") + + # Validate individual SRK records + for srk_rec in self._srk_records: + srk_rec.validate() + + # Check if all SRK records has same type + srk_records_info = [ + (x.version, x.hash_algorithm, x.key_size, x.length, x.srk_flags) + for x in self._srk_records + ] + + messages = ["Signing algorithm", "Hash algorithm", "Key Size", "Length", "Flags"] + for i in range(4): + if not all(srk_records_info[0][i] == x[i] for x in srk_records_info): + raise SPSDKValueError( + f"SRK table: SRK records haven't same {messages[i]}: {[x[i] for x in srk_records_info]}" + ) + + if "srkh_sha_supports" in data.keys(): + if ( + get_key_by_val(SRKRecord.HASH_ALGORITHM, self._srk_records[0].hash_algorithm).label + not in data["srkh_sha_supports"] + ): + raise SPSDKValueError( + "SRK table: SRK records haven't supported hash algorithm:" + f" Used:{self._srk_records[0].hash_algorithm} is not member of" + f" {data['srkh_sha_supports']}" + ) + # Check container length + if self.length != len(self): + raise SPSDKValueError( + f"SRK table: Invalid Length of SRK table: {self.length} != {len(self)}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with SRK table block to parse. + :raises SPSDKLengthError: Invalid length of SRK table data block. + :return: Object recreated from the binary data. + """ + SRKTable.check_container_head(data) + srk_rec_offset = SRKTable.fixed_length() + _, container_length, _ = unpack(SRKTable.format(), data[:srk_rec_offset]) + if ((container_length - srk_rec_offset) % SRKTable.SRK_RECORDS_CNT) != 0: + raise SPSDKLengthError("SRK table: Invalid length of SRK records data.") + srk_rec_size = math.ceil((container_length - srk_rec_offset) / SRKTable.SRK_RECORDS_CNT) + + # try to parse records + srk_records: List[SRKRecord] = [] + for _ in range(SRKTable.SRK_RECORDS_CNT): + srk_record = SRKRecord.parse(data[srk_rec_offset:]) + srk_rec_offset += srk_rec_size + srk_records.append(srk_record) + + return cls(srk_records=srk_records) + + def create_config(self, index: int, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image SRK Table. + + :param index: Container Index. + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + ret_cfg: Dict[str, Union[List, bool]] = {} + cfg_srks = [] + + ret_cfg["flag_ca"] = bool(self._srk_records[0].srk_flags & SRKRecord.FLAGS_CA_MASK) + + for ix_srk, srk in enumerate(self._srk_records): + filename = f"container{index}_srk_public_key{ix_srk}_{srk.get_key_name()}.PEM" + write_file(data=srk.get_public_key(), path=os.path.join(data_path, filename), mode="wb") + cfg_srks.append(filename) + + ret_cfg["srk_array"] = cfg_srks + return ret_cfg + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "SRKTable": + """Converts the configuration option into an AHAB image object. + + "config" content of container configurations. + + :param config: array of AHAB containers configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: SRK Table object. + """ + srk_table = SRKTable() + flags = 0 + flag_ca = config.get("flag_ca", False) + if flag_ca: + flags |= SRKRecord.FLAGS_CA_MASK + srk_list = config.get("srk_array") + assert isinstance(srk_list, list) + for srk_key in srk_list: + assert isinstance(srk_key, str) + srk_key_path = find_file(srk_key, search_paths=search_paths) + srk_table.add_record(extract_public_key(srk_key_path), srk_flags=flags) + return srk_table + + +class ContainerSignature(HeaderContainer): + """Class representing the signature in AHAB container as part of the signature block. + + Signature:: + + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Tag | Length (MSB) | Length (LSB) | Version | + +-----+--------------+--------------+----------------+----------------+ + |0x04 | Reserved | + +-----+---------------------------------------------------------------+ + |0x08 | Signature Data | + +-----+---------------------------------------------------------------+ + + """ + + TAG = AHABTags.SIGNATURE.tag + VERSION = 0x00 + + def __init__( + self, + signature_data: Optional[bytes] = None, + signature_provider: Optional[SignatureProvider] = None, + ) -> None: + """Class object initializer. + + :param signature_data: signature. + :param signature_provider: Signature provider use to sign the image. + """ + super().__init__(tag=self.TAG, length=-1, version=self.VERSION) + self._signature_data = signature_data or b"" + self.signature_provider = signature_provider + self.length = len(self) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ContainerSignature): + if super().__eq__(other) and self._signature_data == other._signature_data: + return True + + return False + + def __len__(self) -> int: + if (not self._signature_data or len(self._signature_data) == 0) and self.signature_provider: + return super().__len__() + self.signature_provider.signature_length + + sign_data_len = len(self._signature_data) + if sign_data_len == 0: + return 0 + + return super().__len__() + sign_data_len + + def __repr__(self) -> str: + return "AHAB Container Signature" + + def __str__(self) -> str: + return ( + "AHAB Container Signature:\n" + f" Signature provider: {self.signature_provider.info() if self.signature_provider else 'Not available'}\n" + f" Signature: {self.signature_data.hex() if self.signature_data else 'Not available'}" + ) + + @property + def signature_data(self) -> bytes: + """Get the signature data. + + :return: signature data. + """ + return self._signature_data + + @signature_data.setter + def signature_data(self, value: bytes) -> None: + """Set the signature data. + + :param value: signature data. + """ + self._signature_data = value + self.length = len(self) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return super().format() + UINT32 # reserved + + def sign(self, data_to_sign: bytes) -> None: + """Sign the data_to_sign and store signature into class. + + :param data_to_sign: Data to be signed by store private key + :raises SPSDKError: Missing private key or raw signature data. + """ + if not self.signature_provider and len(self._signature_data) == 0: + raise SPSDKError( + "The Signature container doesn't have specified the private key to sign." + ) + + if self.signature_provider: + self._signature_data = self.signature_provider.get_signature(data_to_sign) + + def export(self) -> bytes: + """Export signature data that is part of Signature Block. + + :return: bytes representing container signature content. + """ + if len(self) == 0: + return b"" + + data = ( + pack( + self.format(), + self.version, + self.length, + self.tag, + RESERVED, + ) + + self._signature_data + ) + + return data + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + self.validate_header() + if self._signature_data is None or len(self._signature_data) < 20: + raise SPSDKValueError( + f"Signature: Invalid Signature data: 0x{self.signature_data.hex()}" + ) + if self.length != len(self): + raise SPSDKValueError( + f"Signature: Invalid Signature length: {self.length} != {len(self)}." + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with Container signature block to parse. + :return: Object recreated from the binary data. + """ + ContainerSignature.check_container_head(data) + fix_len = ContainerSignature.fixed_length() + + _, container_length, _, _ = unpack(ContainerSignature.format(), data[:fix_len]) + signature_data = data[fix_len:container_length] + + return cls(signature_data=signature_data) + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "ContainerSignature": + """Converts the configuration option into an AHAB image object. + + "config" content of container configurations. + + :param config: array of AHAB containers configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Container signature object. + """ + signature_provider = get_signature_provider( + sp_cfg=config.get("signature_provider"), + local_file_key=config.get("signing_key"), + search_paths=search_paths, + ) + assert signature_provider + return ContainerSignature(signature_provider=signature_provider) + + +class Certificate(HeaderContainer): + """Class representing certificate in the AHAB container as part of the signature block. + + The Certificate comes in two forms - with and without UUID. + + Certificate format 1:: + + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Tag | Length (MSB) | Length (LSB) | Version | + +-----+--------------+--------------+----------------+----------------+ + |0x04 | Permissions | Perm (invert)| Signature offset | + +-----+--------------+--------------+---------------------------------+ + |0x08 | Public Key | + +-----+---------------------------------------------------------------+ + |... | Signature | + +-----+---------------------------------------------------------------+ + + Certificate format 2:: + + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Tag | Length (MSB) | Length (LSB) | Version | + +-----+--------------+--------------+----------------+----------------+ + |0x04 | Permissions | Perm (invert)| Signature offset | + +-----+--------------+--------------+---------------------------------+ + |0x08 | UUID | + +-----+---------------------------------------------------------------+ + |... | Public Key | + +-----+---------------------------------------------------------------+ + |... | Signature | + +-----+---------------------------------------------------------------+ + + """ + + TAG = [AHABTags.CERTIFICATE_UUID.tag, AHABTags.CERTIFICATE_NON_UUID.tag] # type: ignore + UUID_LEN = 16 + UUID_OFFSET = 0x08 + VERSION = 0x00 + PERM_NXP = { + "secure_enclave_debug": 0x02, + "hdmi_debug": 0x04, + "life_cycle": 0x10, + "hdcp_fuses": 0x20, + } + PERM_OEM = { + "container": 0x01, + "phbc_debug": 0x02, + "soc_debug_domain_1": 0x04, + "soc_debug_domain_2": 0x08, + "life_cycle": 0x10, + "monotonic_counter": 0x20, + } + PERM_SIZE = 8 + + def __init__( + self, + permissions: int = 0, + uuid: Optional[bytes] = None, + public_key: Optional[SRKRecord] = None, + signature_provider: Optional[SignatureProvider] = None, + ): + """Class object initializer. + + :param permissions: used to indicate what a certificate can be used for. + :param uuid: optional 128-bit unique identifier. + :param public_key: public Key. SRK record entry describing the key. + :param signature_provider: Signature provider for certificate. Signature is calculated over + all data from beginning of the certificate up to, but not including the signature. + """ + tag = AHABTags.CERTIFICATE_UUID.tag if uuid else AHABTags.CERTIFICATE_NON_UUID.tag + super().__init__(tag=tag, length=-1, version=self.VERSION) + self._permissions = permissions + self.signature_offset = -1 + self._uuid = uuid + self.public_key = public_key + self.signature = ContainerSignature( + signature_data=b"", signature_provider=signature_provider + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Certificate): + if ( + super().__eq__(other) # pylint: disable=too-many-boolean-expressions + and self._permissions == other._permissions + and self.signature_offset == other.signature_offset + and self._uuid == other._uuid + and self.public_key == other.public_key + and self.signature == other.signature + ): + return True + + return False + + def __repr__(self) -> str: + return "AHAB Certificate" + + def __str__(self) -> str: + return ( + "AHAB Certificate:\n" + f" Permission: {hex(self._permissions)}\n" + f" UUID: {self._uuid.hex() if self._uuid else 'Not Available'}\n" + f" Public Key: {str(self.public_key) if self.public_key else 'Not available'}\n" + f" Signature: {str(self.signature) if self.signature else 'Not available'}" + ) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() # endianness, header: version, length, tag + + UINT16 # signature offset + + UINT8 # inverted permissions + + UINT8 # permissions + ) + + def __len__(self) -> int: + assert self.public_key + uuid_len = len(self._uuid) if self._uuid else 0 + return super().__len__() + uuid_len + len(self.public_key) + len(self.signature) + + @staticmethod + def create_permissions(permissions: List[str]) -> int: + """Create integer representation of permission field. + + :param permissions: List of string permissions. + :return: Integer representation of permissions. + """ + ret = 0 + permission_map = {} + permission_map.update(Certificate.PERM_NXP) + permission_map.update(Certificate.PERM_OEM) + for permission in permissions: + ret |= permission_map[permission] + + return ret + + @property + def permission_to_sign_container(self) -> bool: + """Certificate has permission to sign container.""" + return bool(self._permissions & self.PERM_OEM["container"]) + + def create_config_permissions(self, srk_set: str) -> List[str]: + """Create list of string representation of permission field. + + :param srk_set: SRK set to get proper string values. + :return: List of string representation of permissions. + """ + ret = [] + perm_maps = {"nxp": self.PERM_NXP, "oem": self.PERM_OEM} + perm_map = perm_maps.get(srk_set) + + for i in range(self.PERM_SIZE): + if self._permissions & (1 << i): + ret.append( + get_key_by_val(perm_map, 1 << i) + if perm_map and (1 << i) in perm_map.values() + else f"Unknown permission {hex(1< bytes: + """Returns binary data to be signed. + + The certificate block must be properly initialized, so the data are valid for + signing. There is signed whole certificate block without signature part. + + + :raises SPSDKValueError: if Signature Block or SRK Table is missing. + :return: bytes representing data to be signed. + """ + assert self.public_key + cert_data_to_sign = ( + pack( + self.format(), + self.version, + self.length, + self.tag, + self.signature_offset, + ~self._permissions & 0xFF, + self._permissions, + ) + + self.public_key.export() + ) + # if uuid is present, insert it into the cert data + if self._uuid: + cert_data_to_sign = ( + cert_data_to_sign[: self.UUID_OFFSET] + + self._uuid + + cert_data_to_sign[self.UUID_OFFSET :] + ) + + return cert_data_to_sign + + def update_fields(self) -> None: + """Update all fields depended on input values.""" + assert self.public_key + self.public_key.update_fields() + self.tag = ( + AHABTags.CERTIFICATE_UUID.tag if self._uuid else AHABTags.CERTIFICATE_NON_UUID.tag + ) + self.signature_offset = ( + super().__len__() + (len(self._uuid) if self._uuid else 0) + len(self.public_key) + ) + self.length = len(self) + self.signature.sign(self.get_signature_data()) + + def export(self) -> bytes: + """Export container certificate object into bytes. + + :return: bytes representing container content. + """ + assert self.public_key + cert = ( + pack( + self.format(), + self.version, + self.length, + self.tag, + self.signature_offset, + ~self._permissions & 0xFF, + self._permissions, + ) + + self.public_key.export() + + self.signature.export() + ) + # if uuid is present, insert it into the cert data + if self._uuid: + cert = cert[: self.UUID_OFFSET] + self._uuid + cert[self.UUID_OFFSET :] + assert self.length == len(cert) + return cert + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + self.validate_header() + if self._permissions is None or not check_range(self._permissions, end=0xFF): + raise SPSDKValueError(f"Certificate: Invalid Permission data: {self._permissions}") + if self.public_key is None: + raise SPSDKValueError("Certificate: Missing public key.") + self.public_key.validate() + + if not self.signature: + raise SPSDKValueError("Signature must be provided") + + self.signature.validate() + + expected_signature_offset = ( + super().__len__() + (len(self._uuid) if self._uuid else 0) + len(self.public_key) + ) + if self.signature_offset != expected_signature_offset: + raise SPSDKValueError( + f"Certificate: Invalid signature offset. " + f"{self.signature_offset} != {expected_signature_offset}" + ) + if self._uuid and len(self._uuid) != self.UUID_LEN: + raise SPSDKValueError( + f"Certificate: Invalid UUID size. {len(self._uuid)} != {self.UUID_LEN}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with Certificate block to parse. + :raises SPSDKValueError: Certificate permissions are invalid. + :return: Object recreated from the binary data. + """ + Certificate.check_container_head(data) + certificate_data_offset = Certificate.fixed_length() + image_format = Certificate.format() + ( + _, # version, + container_length, + tag, + signature_offset, + inverted_permissions, + permissions, + ) = unpack(image_format, data[:certificate_data_offset]) + + if inverted_permissions != ~permissions & 0xFF: + raise SPSDKValueError("Certificate parser: Invalid permissions record.") + + uuid = None + + if AHABTags.CERTIFICATE_UUID == tag: + uuid = data[certificate_data_offset : certificate_data_offset + Certificate.UUID_LEN] + certificate_data_offset += Certificate.UUID_LEN + + public_key = SRKRecord.parse(data[certificate_data_offset:]) + + signature = ContainerSignature.parse(data[signature_offset:container_length]) + + cert = cls( + permissions=permissions, + uuid=uuid, + public_key=public_key, + ) + cert.signature = signature + return cert + + def create_config(self, index: int, data_path: str, srk_set: str = "oem") -> Dict[str, Any]: + """Create configuration of the AHAB Image Certificate. + + :param index: Container Index. + :param data_path: Path to store the data files of configuration. + :param srk_set: SRK set to know how to create certificate permissions. + :return: Configuration dictionary. + """ + ret_cfg: Dict[str, Any] = {} + assert self.public_key + ret_cfg["permissions"] = self.create_config_permissions(srk_set) + if self._uuid: + ret_cfg["uuid"] = "0x" + self._uuid.hex() + filename = f"container{index}_certificate_public_key_{self.public_key.get_key_name()}.PEM" + write_file( + data=self.public_key.get_public_key(), path=os.path.join(data_path, filename), mode="wb" + ) + ret_cfg["public_key"] = filename + ret_cfg["signature_provider"] = "N/A" + + return ret_cfg + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Certificate": + """Converts the configuration option into an AHAB image signature block certificate object. + + "config" content of container configurations. + + :param config: array of AHAB containers configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Certificate object. + """ + cert_permissions_list = config.get("permissions", []) + cert_uuid_raw = config.get("uuid") + cert_uuid = value_to_bytes(cert_uuid_raw) if cert_uuid_raw else None + cert_public_key_path = config.get("public_key") + assert isinstance(cert_public_key_path, str) + cert_public_key_path = find_file(cert_public_key_path, search_paths=search_paths) + cert_public_key = extract_public_key(cert_public_key_path) + cert_srk_rec = SRKRecord.create_from_key(cert_public_key) + cert_signature_provider = get_signature_provider( + config.get("signature_provider"), + config.get("signing_key"), + search_paths=search_paths, + ) + return Certificate( + permissions=Certificate.create_permissions(cert_permissions_list), + uuid=cert_uuid, + public_key=cert_srk_rec, + signature_provider=cert_signature_provider, + ) + + @staticmethod + def get_validation_schemas() -> List[Dict[str, Any]]: + """Get list of validation schemas. + + :return: Validation list of schemas. + """ + return [DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)["ahab_certificate"]] + + @staticmethod + def generate_config_template() -> str: + """Generate AHAB configuration template. + + :return: Certificate configuration templates. + """ + yaml_data = CommentedConfig( + "Advanced High-Assurance Boot Certificate Configuration template.", + Certificate.get_validation_schemas(), + ).get_template() + + return yaml_data + + +class Blob(HeaderContainer): + """The Blob object used in Signature Container. + + Blob (DEK) content:: + + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Tag | Length (MSB) | Length (LSB) | Version | + +-----+--------------+--------------+----------------+----------------+ + |0x04 | Mode | Algorithm | Size | Flags | + +-----+--------------+--------------+----------------+----------------+ + |0x08 | Wrapped Key | + +-----+--------------+--------------+----------------+----------------+ + + """ + + TAG = AHABTags.BLOB.tag + VERSION = 0x00 + FLAGS = 0x80 # KEK key flag + SUPPORTED_KEY_SIZES = [128, 192, 256] + + def __init__( + self, + flags: int = 0x80, + size: int = 0, + algorithm: KeyBlobEncryptionAlgorithm = KeyBlobEncryptionAlgorithm.AES_CBC, + mode: int = 0, + dek: Optional[bytes] = None, + dek_keyblob: Optional[bytes] = None, + key_identifier: int = 0, + ) -> None: + """Class object initializer. + + :param flags: Keyblob flags + :param size: key size [128,192,256] + :param dek: DEK key + :param mode: DEK BLOB mode + :param algorithm: Encryption algorithm + :param dek_keyblob: DEK keyblob + :param key_identifier: Key identifier. Must be same as it was used for keyblob generation + """ + super().__init__(tag=self.TAG, length=56 + size // 8, version=self.VERSION) + self.mode = mode + self.algorithm = algorithm + self._size = size + self.flags = flags + self.dek = dek + self.dek_keyblob = dek_keyblob or b"" + self.key_identifier = key_identifier + + def __eq__(self, other: object) -> bool: + if isinstance(other, Blob): + if ( + super().__eq__(other) # pylint: disable=too-many-boolean-expressions + and self.mode == other.mode + and self.algorithm == other.algorithm + and self._size == other._size + and self.flags == other.flags + and self.dek_keyblob == other.dek_keyblob + and self.key_identifier == other.key_identifier + ): + return True + + return False + + def __repr__(self) -> str: + return "AHAB Blob" + + def __str__(self) -> str: + return ( + "AHAB Blob:\n" + f" Mode: {self.mode}\n" + f" Algorithm: {self.algorithm.label}\n" + f" Key Size: {self._size}\n" + f" Flags: {self.flags}\n" + f" Key identifier: {hex(self.key_identifier)}\n" + f" DEK keyblob: {self.dek_keyblob.hex() if self.dek_keyblob else 'N/A'}" + ) + + @staticmethod + def compute_keyblob_size(key_size: int) -> int: + """Compute Keyblob size. + + :param key_size: Input AES key size in bits + :return: Keyblob size in bytes. + """ + return (key_size // 8) + 48 + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() # endianness, header: tag, length, version + + UINT8 # mode + + UINT8 # algorithm + + UINT8 # size + + UINT8 # flags + ) + + def __len__(self) -> int: + # return super()._total_length() + len(self.dek_keyblob) + return self.length + + def export(self) -> bytes: + """Export Signature Block Blob. + + :return: bytes representing Signature Block Blob. + """ + blob = ( + pack( + self.format(), + self.version, + self.length, + self.tag, + self.flags, + self._size // 8, + self.algorithm.tag, + self.mode, + ) + + self.dek_keyblob + ) + + return blob + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of AHAB Blob + """ + self.validate_header() + + if self._size not in self.SUPPORTED_KEY_SIZES: + raise SPSDKValueError("AHAB Blob: Invalid key size.") + if self.mode is None: + raise SPSDKValueError("AHAB Blob: Invalid mode.") + if self.algorithm is None: + raise SPSDKValueError("AHAB Blob: Invalid algorithm.") + if self.dek and len(self.dek) != self._size // 8: + raise SPSDKValueError("AHAB Blob: Invalid DEK key size.") + if self.dek_keyblob is None or len(self.dek_keyblob) != self.compute_keyblob_size( + self._size + ): + raise SPSDKValueError("AHAB Blob: Invalid Wrapped key.") + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with Blob block to parse. + :return: Object recreated from the binary data. + """ + Blob.check_container_head(data) + ( + _, # version + container_length, + _, # tag + flags, + size, + algorithm, # algorithm + mode, # mode + ) = unpack(Blob.format(), data[: Blob.fixed_length()]) + + dek_keyblob = data[Blob.fixed_length() : container_length] + + return cls( + size=size * 8, + flags=flags, + dek_keyblob=dek_keyblob, + mode=mode, + algorithm=KeyBlobEncryptionAlgorithm.from_tag(algorithm), + ) + + def create_config(self, index: int, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image Blob. + + :param index: Container Index. + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + ret_cfg: Dict[str, Any] = {} + assert self.dek_keyblob + filename = f"container{index}_dek_keyblob.bin" + write_file(self.export(), os.path.join(data_path, filename), "wb") + ret_cfg["dek_key_size"] = self._size + ret_cfg["dek_key"] = "N/A" + ret_cfg["dek_keyblob"] = filename + ret_cfg["key_identifier"] = self.key_identifier + + return ret_cfg + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Blob": + """Converts the configuration option into an AHAB image signature block blob object. + + "config" content of container configurations. + + :param config: Blob configuration + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKValueError: Invalid configuration - Invalid DEK KeyBlob + :return: Blob object. + """ + dek_size = value_to_int(config.get("dek_key_size", 128)) + dek_input = config.get("dek_key") + dek_keyblob_input = config.get("dek_keyblob") + key_identifier = config.get("key_identifier", 0) + assert dek_input, "Missing DEK value" + assert dek_keyblob_input, "Missing DEK KEYBLOB value" + + dek = load_hex_string(dek_input, dek_size // 8, search_paths) + dek_keyblob_value = load_hex_string( + dek_keyblob_input, Blob.compute_keyblob_size(dek_size) + 8, search_paths + ) + if not dek_keyblob_value: + raise SPSDKValueError("Invalid DEK KeyBlob.") + + keyblob = Blob.parse(dek_keyblob_value) + keyblob.dek = dek + keyblob.key_identifier = key_identifier + return keyblob + + def encrypt_data(self, iv: bytes, data: bytes) -> bytes: + """Encrypt data. + + :param iv: Initial vector 128 bits length + :param data: Data to encrypt + :raises SPSDKError: Missing DEK, unsupported algorithm + :return: Encrypted data + """ + if not self.dek: + raise SPSDKError("The AHAB keyblob hasn't defined DEK to encrypt data") + + encryption_methods = { + KeyBlobEncryptionAlgorithm.AES_CBC: aes_cbc_encrypt, + KeyBlobEncryptionAlgorithm.SM4_CBC: sm4_cbc_encrypt, + } + + if not encryption_methods.get(self.algorithm): + raise SPSDKError(f"Unsupported encryption algorithm: {self.algorithm}") + return encryption_methods[self.algorithm](self.dek, data, iv) + + def decrypt_data(self, iv: bytes, encrypted_data: bytes) -> bytes: + """Encrypt data. + + :param iv: Initial vector 128 bits length + :param encrypted_data: Data to decrypt + :raises SPSDKError: Missing DEK, unsupported algorithm + :return: Plain data + """ + if not self.dek: + raise SPSDKError("The AHAB keyblob hasn't defined DEK to encrypt data") + + decryption_methods = { + KeyBlobEncryptionAlgorithm.AES_CBC: aes_cbc_decrypt, + KeyBlobEncryptionAlgorithm.SM4_CBC: sm4_cbc_decrypt, + } + + if not decryption_methods.get(self.algorithm): + raise SPSDKError(f"Unsupported encryption algorithm: {self.algorithm}") + return decryption_methods[self.algorithm](self.dek, encrypted_data, iv) + + +class SignatureBlock(HeaderContainer): + """Class representing signature block in the AHAB container. + + Signature Block:: + + +---------------+----------------+----------------+----------------+-----+ + | Byte 3 | Byte 2 | Byte 1 | Byte 0 | Fix | + |---------------+----------------+----------------+----------------+ len | + | Tag | Length | Version | | + |---------------+---------------------------------+----------------+ | + | SRK Table Offset | Certificate Offset | | + |--------------------------------+---------------------------------+ | + | Blob Offset | Signature Offset | | + |--------------------------------+---------------------------------+ | + | Key identifier in case that Blob is present | | + +------------------------------------------------------------------+-----+ Starting offset + | SRK Table | | + +------------------------------------------------------------------+-----+ Padding length + | 64 bit alignment | | + +------------------------------------------------------------------+-----+ Starting offset + | Signature | | + +------------------------------------------------------------------+-----+ Padding length + | 64 bit alignment | | + +------------------------------------------------------------------+-----+ Starting offset + | Certificate | | + +------------------------------------------------------------------+-----+ Padding length + | 64 bit alignment | | + +------------------------------------------------------------------+-----+ Starting offset + | Blob | | + +------------------------------------------------------------------+-----+ + + """ + + TAG = AHABTags.SIGNATURE_BLOCK.tag + VERSION = 0x00 + + def __init__( + self, + srk_table: Optional["SRKTable"] = None, + container_signature: Optional["ContainerSignature"] = None, + certificate: Optional["Certificate"] = None, + blob: Optional["Blob"] = None, + ): + """Class object initializer. + + :param srk_table: SRK table. + :param container_signature: container signature. + :param certificate: container certificate. + :param blob: container blob. + """ + super().__init__(tag=self.TAG, length=-1, version=self.VERSION) + self._srk_table_offset = 0 + self._certificate_offset = 0 + self._blob_offset = 0 + self.signature_offset = 0 + self.srk_table = srk_table + self.signature = container_signature + self.certificate = certificate + self.blob = blob + + def __eq__(self, other: object) -> bool: + """Compares for equality with other Signature Block objects. + + :param other: object to compare with. + :return: True on match, False otherwise. + """ + if isinstance(other, SignatureBlock): + if ( + super().__eq__(other) # pylint: disable=too-many-boolean-expressions + and self._srk_table_offset == other._srk_table_offset + and self._certificate_offset == other._certificate_offset + and self._blob_offset == other._blob_offset + and self.signature_offset == other.signature_offset + and self.srk_table == other.srk_table + and self.signature == other.signature + and self.certificate == other.certificate + and self.blob == other.blob + ): + return True + + return False + + def __len__(self) -> int: + self.update_fields() + return self.length + + def __repr__(self) -> str: + return "AHAB Signature Block" + + def __str__(self) -> str: + return ( + "AHAB Signature Block:\n" + f" SRK Table: {bool(self.srk_table)}\n" + f" Certificate: {bool(self.certificate)}\n" + f" Signature: {bool(self.signature)}\n" + f" Blob: {bool(self.blob)}" + ) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() + + UINT16 # certificate offset + + UINT16 # SRK table offset + + UINT16 # signature offset + + UINT16 # blob offset + + UINT32 # key_identifier if blob is used + ) + + def update_fields(self) -> None: + """Update all fields depended on input values.""" + # 1: Update SRK Table + # Nothing to do with SRK Table + last_offset = 0 + last_block_size = align(calcsize(self.format()), CONTAINER_ALIGNMENT) + if self.srk_table: + self.srk_table.update_fields() + last_offset = self._srk_table_offset = last_offset + last_block_size + last_block_size = align(len(self.srk_table), CONTAINER_ALIGNMENT) + else: + self._srk_table_offset = 0 + + # 2: Update Signature (at least length) + # Nothing to do with Signature - in this time , it MUST be ready + if self.signature: + last_offset = self.signature_offset = last_offset + last_block_size + last_block_size = align(len(self.signature), CONTAINER_ALIGNMENT) + else: + self.signature_offset = 0 + # 3: Optionally update Certificate + if self.certificate: + self.certificate.update_fields() + last_offset = self._certificate_offset = last_offset + last_block_size + last_block_size = align(len(self.certificate), CONTAINER_ALIGNMENT) + else: + self._certificate_offset = 0 + # 4: Optionally update Blob + if self.blob: + last_offset = self._blob_offset = last_offset + last_block_size + last_block_size = align(len(self.blob), CONTAINER_ALIGNMENT) + else: + self._blob_offset = 0 + + # 5: Update length of Signature block + self.length = last_offset + last_block_size + + def export(self) -> bytes: + """Export Signature block. + + :raises SPSDKLengthError: if exported data length doesn't match container length. + :return: bytes signature block content. + """ + extended_header = pack( + self.format(), + self.version, + self.length, + self.tag, + self._certificate_offset, + self._srk_table_offset, + self.signature_offset, + self._blob_offset, + self.blob.key_identifier if self.blob else RESERVED, + ) + + signature_block = bytearray(len(self)) + signature_block[0 : self.fixed_length()] = extended_header + if self.srk_table: + signature_block[ + self._srk_table_offset : self._srk_table_offset + len(self.srk_table) + ] = self.srk_table.export() + if self.signature: + signature_block[ + self.signature_offset : self.signature_offset + len(self.signature) + ] = self.signature.export() + if self.certificate: + signature_block[ + self._certificate_offset : self._certificate_offset + len(self.certificate) + ] = self.certificate.export() + if self.blob: + signature_block[ + self._blob_offset : self._blob_offset + len(self.blob) + ] = self.blob.export() + + return signature_block + + def validate(self, data: Dict[str, Any]) -> None: + """Validate object data. + + :param data: Additional validation data. + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + + def check_offset(name: str, min_offset: int, offset: int) -> None: + if offset < min_offset: + raise SPSDKValueError( + f"Signature Block: Invalid {name} offset: {offset} < minimal offset {min_offset}" + ) + if offset != align(offset, CONTAINER_ALIGNMENT): + raise SPSDKValueError( + f"Signature Block: Invalid {name} offset alignment: {offset} is not aligned to 64 bits!" + ) + + self.validate_header() + if self.length != len(self): + raise SPSDKValueError( + f"Signature Block: Invalid block length: {self.length} != {len(self)}" + ) + if bool(self._srk_table_offset) != bool(self.srk_table): + raise SPSDKValueError("Signature Block: Invalid setting of SRK table offset.") + if bool(self.signature_offset) != bool(self.signature): + raise SPSDKValueError("Signature Block: Invalid setting of Signature offset.") + if bool(self._certificate_offset) != bool(self.certificate): + raise SPSDKValueError("Signature Block: Invalid setting of Certificate offset.") + if bool(self._blob_offset) != bool(self.blob): + raise SPSDKValueError("Signature Block: Invalid setting of Blob offset.") + + min_offset = self.fixed_length() + if self.srk_table: + self.srk_table.validate(data) + check_offset("SRK table", min_offset, self._srk_table_offset) + min_offset = self._srk_table_offset + len(self.srk_table) + if self.signature: + self.signature.validate() + check_offset("Signature", min_offset, self.signature_offset) + min_offset = self.signature_offset + len(self.signature) + if self.certificate: + self.certificate.validate() + check_offset("Certificate", min_offset, self._certificate_offset) + min_offset = self._certificate_offset + len(self.certificate) + if self.blob: + self.blob.validate() + check_offset("Blob", min_offset, self._blob_offset) + min_offset = self._blob_offset + len(self.blob) + + if "flag_used_srk_id" in data.keys() and self.signature and self.srk_table: + public_keys = self.srk_table.get_source_keys() + if ( + self.signature.signature_provider + and self.certificate + and not self.certificate.permission_to_sign_container + ): + # Container is signed by SRK key. Get the matching key and verify that the private key + # belongs to the public key in SRK + srk_pair_id = get_matching_key_id(public_keys, self.signature.signature_provider) + if srk_pair_id != data["flag_used_srk_id"]: + raise SPSDKValueError( + f"Signature Block: Configured SRK ID ({data['flag_used_srk_id']})" + f" doesn't match detected SRK ID for signing key ({srk_pair_id})." + ) + elif self.certificate and self.certificate.permission_to_sign_container: + # In this case the certificate is signed by the key with given SRK ID + if not public_keys[data["flag_used_srk_id"]].verify_signature( + self.certificate.signature.signature_data, self.certificate.get_signature_data() + ): + raise SPSDKValueError( + f"Certificate signature cannot be verified with the key with SRK ID {data['flag_used_srk_id']} " + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary chunk to the container object. + + :param data: Binary data with Signature block to parse. + :return: Object recreated from the binary data. + """ + SignatureBlock.check_container_head(data) + ( + _, # version + _, # container_length + _, # tag + certificate_offset, + srk_table_offset, + signature_offset, + blob_offset, + key_identifier, + ) = unpack(SignatureBlock.format(), data[: SignatureBlock.fixed_length()]) + + signature_block = cls() + signature_block.srk_table = ( + SRKTable.parse(data[srk_table_offset:]) if srk_table_offset else None + ) + signature_block.certificate = ( + Certificate.parse(data[certificate_offset:]) if certificate_offset else None + ) + signature_block.signature = ( + ContainerSignature.parse(data[signature_offset:]) if signature_offset else None + ) + try: + signature_block.blob = Blob.parse(data[blob_offset:]) if blob_offset else None + if signature_block.blob: + signature_block.blob.key_identifier = key_identifier + except SPSDKParsingError as exc: + logger.warning( + "AHAB Blob parsing error. In case that no encrypted images" + " are presented in container, it should not be an big issue." + f"\n{str(exc)}" + ) + signature_block.blob = None + + return signature_block + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "SignatureBlock": + """Converts the configuration option into an AHAB Signature block object. + + "config" content of container configurations. + + :param config: array of AHAB signature block configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: AHAB Signature block object. + """ + signature_block = SignatureBlock() + # SRK Table + srk_table_cfg = config.get("srk_table") + signature_block.srk_table = ( + SRKTable.load_from_config(srk_table_cfg, search_paths) if srk_table_cfg else None + ) + + # Container Signature + srk_set = config.get("srk_set", "none") + signature_block.signature = ( + ContainerSignature.load_from_config(config, search_paths) if srk_set != "none" else None + ) + + # Certificate Block + signature_block.certificate = None + certificate_cfg = config.get("certificate") + + if certificate_cfg: + try: + cert_cfg = load_configuration(certificate_cfg) + check_config( + cert_cfg, Certificate.get_validation_schemas(), search_paths=search_paths + ) + signature_block.certificate = Certificate.load_from_config(cert_cfg) + except SPSDKError: + # this could be pre-exported binary certificate :-) + signature_block.certificate = Certificate.parse( + load_binary(certificate_cfg, search_paths) + ) + + # DEK blob + blob_cfg = config.get("blob") + signature_block.blob = Blob.load_from_config(blob_cfg, search_paths) if blob_cfg else None + + return signature_block + + +class AHABContainerBase(HeaderContainer): + """Class representing AHAB container base class (common for Signed messages and AHAB Image). + + Container header:: + + +---------------+----------------+----------------+----------------+ + | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +---------------+----------------+----------------+----------------+ + | Tag | Length | Version | + +---------------+---------------------------------+----------------+ + | Flags | + +---------------+----------------+---------------------------------+ + | # of images | Fuse version | SW version | + +---------------+----------------+---------------------------------+ + | Reserved | Signature Block Offset | + +--------------------------------+---------------------------------+ + | Payload (Signed Message or Image Array) | + +------------------------------------------------------------------+ + | Signature block | + +------------------------------------------------------------------+ + + """ + + TAG = 0x00 # Need to be updated by child class + VERSION = 0x00 + FLAGS_SRK_SET_OFFSET = 0 + FLAGS_SRK_SET_SIZE = 2 + FLAGS_SRK_SET_VAL = {"none": 0, "nxp": 1, "oem": 2} + FLAGS_USED_SRK_ID_OFFSET = 4 + FLAGS_USED_SRK_ID_SIZE = 2 + FLAGS_SRK_REVOKE_MASK_OFFSET = 8 + FLAGS_SRK_REVOKE_MASK_SIZE = 4 + + def __init__( + self, + flags: int = 0, + fuse_version: int = 0, + sw_version: int = 0, + signature_block: Optional["SignatureBlock"] = None, + ): + """Class object initializer. + + :param flags: flags. + :param fuse_version: value must be equal to or greater than the version + stored in the fuses to allow loading this container. + :param sw_version: used by PHBC (Privileged Host Boot Companion) to select + between multiple images with same fuse version field. + :param signature_block: signature block. + """ + super().__init__(tag=self.TAG, length=-1, version=self.VERSION) + self.flags = flags + self.fuse_version = fuse_version + self.sw_version = sw_version + self.signature_block = signature_block or SignatureBlock() + self.search_paths: List[str] = [] + self.lock = False + + def __eq__(self, other: object) -> bool: + if isinstance(other, AHABContainerBase): + if ( + super().__eq__(other) + and self.flags == other.flags + and self.fuse_version == other.fuse_version + and self.sw_version == other.sw_version + ): + return True + + return False + + def set_flags( + self, srk_set: str = "none", used_srk_id: int = 0, srk_revoke_mask: int = 0 + ) -> None: + """Set the flags value. + + :param srk_set: Super Root Key (SRK) set, defaults to "none" + :param used_srk_id: Which key from SRK set is being used, defaults to 0 + :param srk_revoke_mask: SRK revoke mask, defaults to 0 + """ + flags = self.FLAGS_SRK_SET_VAL[srk_set.lower()] + flags |= used_srk_id << 4 + flags |= srk_revoke_mask << 8 + self.flags = flags + + @property + def flag_srk_set(self) -> str: + """SRK set flag in string representation. + + :return: Name of SRK Set flag. + """ + srk_set = (self.flags >> self.FLAGS_SRK_SET_OFFSET) & ((1 << self.FLAGS_SRK_SET_SIZE) - 1) + return get_key_by_val(self.FLAGS_SRK_SET_VAL, srk_set) + + @property + def flag_used_srk_id(self) -> int: + """Used SRK ID flag. + + :return: Index of Used SRK ID. + """ + return (self.flags >> self.FLAGS_USED_SRK_ID_OFFSET) & ( + (1 << self.FLAGS_USED_SRK_ID_SIZE) - 1 + ) + + @property + def flag_srk_revoke_mask(self) -> str: + """SRK Revoke mask flag. + + :return: SRK revoke mask in HEX. + """ + srk_revoke_mask = (self.flags >> self.FLAGS_SRK_REVOKE_MASK_OFFSET) & ( + (1 << self.FLAGS_SRK_REVOKE_MASK_SIZE) - 1 + ) + return hex(srk_revoke_mask) + + @property + def _signature_block_offset(self) -> int: + """Returns current signature block offset. + + :return: Offset in bytes of Signature block. + """ + # Constant size of Container header + Image array Entry table + return align( + super().__len__(), + CONTAINER_ALIGNMENT, + ) + + @property + def image_array_len(self) -> int: + """Get image array length if available. + + :return: Length of image array. + """ + return 0 + + def __len__(self) -> int: + """Get total length of AHAB container. + + :return: Size in bytes of AHAB Container. + """ + # If there are no images just return length of header + return self.header_length() + + def header_length(self) -> int: + """Length of AHAB Container header. + + :return: Length in bytes of AHAB Container header. + """ + return super().__len__() + len( # This returns the fixed length of the container header + self.signature_block + ) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() + + UINT32 # Flags + + UINT16 # SW version + + UINT8 # Fuse version + + UINT8 # Number of Images + + UINT16 # Signature Block Offset + + UINT16 # Reserved + ) + + def update_fields(self) -> None: + """Updates all volatile information in whole container structure. + + :raises SPSDKError: When inconsistent image array length is detected. + """ + # Update the signature block to get overall size of it + self.signature_block.update_fields() + # Update the Container header length + self.length = self.header_length() + # # Sign the image header + if self.flag_srk_set != "none": + assert self.signature_block.signature + self.signature_block.signature.sign(self.get_signature_data()) + + def get_signature_data(self) -> bytes: + """Returns binary data to be signed. + + The container must be properly initialized, so the data are valid for + signing, i.e. the offsets, lengths etc. must be set prior invoking this + method, otherwise improper data will be signed. + + The whole container gets serialized first. Afterwards the binary data + is sliced so only data for signing get's returned. The signature data + length is evaluated based on offsets, namely the signature block offset, + the container signature offset and the container signature fixed data length. + + Signature data structure:: + + +---------------------------------------------------+----------------+ + | Container header | | + +---+---+-----------+---------+--------+------------+ Data | + | S | | tag | length | length | version | | + | i | +-----------+---------+--------+------------+ | + | g | | flags | to | + | n | +---------------------+---------------------+ | + | a | | srk table offset | certificate offset | | + | t | +---------------------+---------------------+ Sign | + | u | | blob offset | signature offset | | + | r | +---------------------+---------------------+ | + | e | | SRK Table | | + | +---+-----------+---------+--------+------------+----------------+ + | B | S | tag | length | length | version | Signature data | + | l | i +-----------+---------+--------+------------+ fixed length | + | o | g | Reserved | | + | c | n +-------------------------------------------+----------------+ + | k | a | Signature data | + | | t | | + | | u | | + | | r | | + | | e | | + +---+---+-------------------------------------------+ + + :raises SPSDKValueError: if Signature Block or SRK Table is missing. + :return: bytes representing data to be signed. + """ + if not self.signature_block.signature or not self.signature_block.srk_table: + raise SPSDKValueError( + "Can't retrieve data block to sign. Signature or SRK table is missing!" + ) + + signature_offset = self._signature_block_offset + self.signature_block.signature_offset + return self._export()[:signature_offset] + + def _export(self) -> bytes: + """Export container header into bytes. + + :return: bytes representing container header content including the signature block. + """ + return pack( + self.format(), + self.version, + self.length, + self.tag, + self.flags, + self.sw_version, + self.fuse_version, + self.image_array_len, + self._signature_block_offset, + RESERVED, # Reserved field + ) + + def validate(self, data: Dict[str, Any]) -> None: + """Validate object data. + + :param data: Additional validation data. + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + self.validate_header() + + if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): + raise SPSDKValueError(f"Container Header: Invalid flags: {hex(self.flags)}") + if self.sw_version is None or not check_range(self.sw_version, end=(1 << 16) - 1): + raise SPSDKValueError(f"Container Header: Invalid SW version: {hex(self.sw_version)}") + if self.fuse_version is None or not check_range(self.fuse_version, end=(1 << 8) - 1): + raise SPSDKValueError( + f"Container Header: Invalid Fuse version: {hex(self.fuse_version)}" + ) + self.signature_block.validate(data) + + @staticmethod + def _parse(binary: bytes) -> Tuple[int, int, int, int, int]: + """Parse input binary chunk to the container object. + + :param parent: AHABImage object. + :param binary: Binary data with Container block to parse. + :return: Object recreated from the binary data. + """ + AHABContainer.check_container_head(binary) + image_format = AHABContainer.format() + ( + _, # version + _, # container_length + _, # tag + flags, + sw_version, + fuse_version, + number_of_images, + signature_block_offset, + _, # reserved + ) = unpack(image_format, binary[: AHABContainer.fixed_length()]) + + return (flags, sw_version, fuse_version, number_of_images, signature_block_offset) + + def _create_config(self, index: int, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image. + + :param index: Container index. + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + cfg: Dict[str, Any] = {} + + cfg["srk_set"] = self.flag_srk_set + cfg["used_srk_id"] = self.flag_used_srk_id + cfg["srk_revoke_mask"] = self.flag_srk_revoke_mask + cfg["fuse_version"] = self.fuse_version + cfg["sw_version"] = self.sw_version + cfg["signing_key"] = "N/A" + + if self.signature_block.srk_table: + cfg["srk_table"] = self.signature_block.srk_table.create_config(index, data_path) + + if self.signature_block.certificate: + cert_cfg = self.signature_block.certificate.create_config( + index, data_path, self.flag_srk_set + ) + write_file( + CommentedConfig( + "Parsed AHAB Certificate", Certificate.get_validation_schemas() + ).get_config(cert_cfg), + os.path.join(data_path, "certificate.yaml"), + ) + cfg["certificate"] = "certificate.yaml" + + if self.signature_block.blob: + cfg["blob"] = self.signature_block.blob.create_config(index, data_path) + + return cfg + + def load_from_config_generic(self, config: Dict[str, Any]) -> None: + """Converts the configuration option into an AHAB image object. + + "config" content of container configurations. + + :param config: array of AHAB containers configuration dictionaries. + """ + self.set_flags( + srk_set=config.get("srk_set", "none"), + used_srk_id=value_to_int(config.get("used_srk_id", 0)), + srk_revoke_mask=value_to_int(config.get("srk_revoke_mask", 0)), + ) + self.fuse_version = value_to_int(config.get("fuse_version", 0)) + self.sw_version = value_to_int(config.get("sw_version", 0)) + + self.signature_block = SignatureBlock.load_from_config( + config, search_paths=self.search_paths + ) + + +class AHABContainer(AHABContainerBase): + """Class representing AHAB container. + + Container header:: + + +---------------+----------------+----------------+----------------+ + | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +---------------+----------------+----------------+----------------+ + | Tag | Length | Version | + +---------------+---------------------------------+----------------+ + | Flags | + +---------------+----------------+---------------------------------+ + | # of images | Fuse version | SW version | + +---------------+----------------+---------------------------------+ + | Reserved | Signature Block Offset | + +----+---------------------------+---------------------------------+ + | I |image0: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | + + m |-------------------------------------------------------------+ + | g |image1: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | + + . |-------------------------------------------------------------+ + | A |... | + | r |... | + | r | | + + a |-------------------------------------------------------------+ + | y |imageN: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | + +----+-------------------------------------------------------------+ + | Signature block | + +------------------------------------------------------------------+ + | | + | | + | | + +------------------------------------------------------------------+ + | Data block_0 | + +------------------------------------------------------------------+ + | | + | | + +------------------------------------------------------------------+ + | Data block_n | + +------------------------------------------------------------------+ + + """ + + TAG = AHABTags.CONTAINER_HEADER.tag + + def __init__( + self, + parent: "AHABImage", + flags: int = 0, + fuse_version: int = 0, + sw_version: int = 0, + image_array: Optional[List["ImageArrayEntry"]] = None, + signature_block: Optional["SignatureBlock"] = None, + container_offset: int = 0, + ): + """Class object initializer. + + :parent: Parent AHABImage object. + :param flags: flags. + :param fuse_version: value must be equal to or greater than the version + stored in the fuses to allow loading this container. + :param sw_version: used by PHBC (Privileged Host Boot Companion) to select + between multiple images with same fuse version field. + :param image_array: array of image entries, must be `number of images` long. + :param signature_block: signature block. + """ + super().__init__( + flags=flags, + fuse_version=fuse_version, + sw_version=sw_version, + signature_block=signature_block, + ) + self.parent = parent + assert self.parent is not None + self.image_array = image_array or [] + self.container_offset = container_offset + self.search_paths: List[str] = [] + + def __eq__(self, other: object) -> bool: + if isinstance(other, AHABContainer): + if super().__eq__(other) and self.image_array == other.image_array: + return True + + return False + + def __repr__(self) -> str: + return f"AHAB Container at offset {hex(self.container_offset)} " + + def __str__(self) -> str: + return ( + "AHAB Container:\n" + f" Index: {'0' if self.container_offset == 0 else '1'}\n" + f" Flags: {hex(self.flags)}\n" + f" Fuse version: {hex(self.fuse_version)}\n" + f" SW version: {hex(self.sw_version)}\n" + f" Images count: {self.image_array_len}" + ) + + @property + def image_array_len(self) -> int: + """Get image array length if available. + + :return: Length of image array. + """ + return len(self.image_array) + + @property + def _signature_block_offset(self) -> int: + """Returns current signature block offset. + + :return: Offset in bytes of Signature block. + """ + # Constant size of Container header + Image array Entry table + return align( + super().fixed_length() + len(self.image_array) * ImageArrayEntry.fixed_length(), + CONTAINER_ALIGNMENT, + ) + + def __len__(self) -> int: + """Get total length of AHAB container. + + :return: Size in bytes of AHAB Container. + """ + # Get image which has biggest offset + possible_sizes = [self.header_length()] + possible_sizes.extend([align(x.image_offset + x.image_size) for x in self.image_array]) + + return align(max(possible_sizes), CONTAINER_ALIGNMENT) + + def header_length(self) -> int: + """Length of AHAB Container header. + + :return: Length in bytes of AHAB Container header. + """ + return ( + super().fixed_length() # This returns the fixed length of the container header + # This returns the total length of all image array entries + + len(self.image_array) * ImageArrayEntry.fixed_length() + # This returns the length of signature block (including SRK table, + # blob etc. if present) + + len(self.signature_block) + ) + + def update_fields(self) -> None: + """Updates all volatile information in whole container structure. + + :raises SPSDKError: When inconsistent image array length is detected. + """ + # 1. Encrypt all images if applicable + for image_entry in self.image_array: + if ( + image_entry.flags_is_encrypted + and not image_entry.already_encrypted_image + and self.signature_block.blob + ): + image_entry.encrypted_image = self.signature_block.blob.encrypt_data( + image_entry.image_iv[16:], image_entry.plain_image + ) + image_entry.already_encrypted_image = True + + # 2. Update the signature block to get overall size of it + self.signature_block.update_fields() + # 3. Updates Image Entries + for image_entry in self.image_array: + image_entry.update_fields() + # 4. Update the Container header length + self.length = self.header_length() + # 5. Sign the image header + if self.flag_srk_set != "none": + assert self.signature_block.signature + self.signature_block.signature.sign(self.get_signature_data()) + + def decrypt_data(self) -> None: + """Decrypt all images if possible.""" + for i, image_entry in enumerate(self.image_array): + if image_entry.flags_is_encrypted: + if self.signature_block.blob is None: + raise SPSDKError("Cannot decrypt image without Blob!") + + decrypted_data = self.signature_block.blob.decrypt_data( + image_entry.image_iv[16:], image_entry.encrypted_image + ) + if image_entry.image_iv == get_hash( + decrypted_data, algorithm=EnumHashAlgorithm.SHA256 + ): + image_entry.plain_image = decrypted_data + logger.info( + f" Image{i} from AHAB container at offset {hex(self.container_offset)} has been decrypted." + ) + else: + logger.warning( + f" Image{i} from AHAB container at offset {hex(self.container_offset)} decryption failed." + ) + + def _export(self) -> bytes: + """Export container header into bytes. + + :return: bytes representing container header content including the signature block. + """ + return self.export() + + def export(self) -> bytes: + """Export container header into bytes. + + :return: bytes representing container header content including the signature block. + """ + container_header = bytearray(align(self.header_length(), CONTAINER_ALIGNMENT)) + container_header_only = super()._export() + + for image_array_entry in self.image_array: + container_header_only += image_array_entry.export() + + container_header[: self._signature_block_offset] = container_header_only + # Add Signature Block + container_header[ + self._signature_block_offset : self._signature_block_offset + + align(len(self.signature_block), CONTAINER_ALIGNMENT) + ] = self.signature_block.export() + + return container_header + + def validate(self, data: Dict[str, Any]) -> None: + """Validate object data. + + :param data: Additional validation data. + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + data["flag_used_srk_id"] = self.flag_used_srk_id + self.validate_header() + if self.length != self.header_length(): + raise SPSDKValueError( + f"Container 0x{self.container_offset:04X} " + f"Header: Invalid block length: {self.length} != {self.header_length()}" + ) + + super().validate(data) + + if self.image_array is None or len(self.image_array) == 0: + raise SPSDKValueError( + f"Container 0x{self.container_offset:04X} Header: Invalid Image Array: {self.image_array}" + ) + + for container, offset in zip(self.parent.ahab_containers, self.parent.ahab_address_map): + if self == container: + if self.container_offset != offset: + raise SPSDKValueError( + f"AHAB Container 0x{self.container_offset:04X}: Invalid Container Offset." + ) + + if self.signature_block.srk_table and self.signature_block.signature: + # Get public key with the SRK ID + key = self.signature_block.srk_table.get_source_keys()[self.flag_used_srk_id] + if self.signature_block.certificate: + # Verify signature of certificate + if not key.verify_signature( + self.signature_block.certificate.signature.signature_data, + self.signature_block.certificate.get_signature_data(), + ): + raise SPSDKValueError( + f"AHAB Container 0x{self.container_offset:04X}: Certificate block signature " + f"cannot be verified with SRK ID {self.flag_used_srk_id}" + ) + + if ( + self.signature_block.certificate + and self.signature_block.certificate.permission_to_sign_container + ): + # Container is signed by certificate, get public key from certificate + assert ( + self.signature_block.certificate.public_key + ), "Certificate must contain public key" + key = PublicKey.parse(self.signature_block.certificate.public_key.get_public_key()) + + if not key.verify_signature( + self.signature_block.signature.signature_data, self.get_signature_data() + ): + if ( + self.signature_block.certificate + and self.signature_block.certificate.permission_to_sign_container + ): + raise SPSDKValueError( + f"AHAB Container 0x{self.container_offset:04X}: " + "Signature cannot be verified with the public key from certificate" + ) + raise SPSDKValueError( + f"AHAB Container 0x{self.container_offset:04X}: " + f"Signature cannot be verified with SRK ID {self.flag_used_srk_id}" + ) + + for image in self.image_array: + image.validate() + + @classmethod + def parse(cls, data: bytes, parent: "AHABImage", container_id: int) -> Self: # type: ignore# type: ignore # pylint: disable=arguments-differ + """Parse input binary chunk to the container object. + + :param data: Binary data with Container block to parse. + :param parent: AHABImage object. + :param container_id: AHAB container ID. + :return: Object recreated from the binary data. + """ + if parent is None: + raise SPSDKValueError("Ahab Image must be specified.") + ( + flags, + sw_version, + fuse_version, + number_of_images, + signature_block_offset, + ) = AHABContainerBase._parse(data) + + parsed_container = cls( + parent=parent, + flags=flags, + fuse_version=fuse_version, + sw_version=sw_version, + container_offset=parent.ahab_address_map[container_id], + ) + parsed_container.signature_block = SignatureBlock.parse(data[signature_block_offset:]) + + for i in range(number_of_images): + image_array_entry = ImageArrayEntry.parse( + data[AHABContainer.fixed_length() + i * ImageArrayEntry.fixed_length() :], + parsed_container, + ) + parsed_container.image_array.append(image_array_entry) + # Lock the parsed container to any updates of offsets + parsed_container.lock = True + return parsed_container + + def create_config(self, index: int, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image. + + :param index: Container index. + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + ret_cfg = {} + cfg = self._create_config(index, data_path) + images_cfg = [] + + for img_ix, image in enumerate(self.image_array): + images_cfg.append(image.create_config(index, img_ix, data_path)) + cfg["images"] = images_cfg + + ret_cfg["container"] = cfg + return ret_cfg + + @staticmethod + def load_from_config( + parent: "AHABImage", config: Dict[str, Any], container_ix: int + ) -> "AHABContainer": + """Converts the configuration option into an AHAB image object. + + "config" content of container configurations. + + :param parent: AHABImage object. + :param config: array of AHAB containers configuration dictionaries. + :param container_ix: Container index that is loaded. + :return: AHAB Container object. + """ + ahab_container = AHABContainer(parent) + ahab_container.search_paths = parent.search_paths or [] + ahab_container.container_offset = parent.ahab_address_map[container_ix] + ahab_container.load_from_config_generic(config) + images = config.get("images") + assert isinstance(images, list) + for image in images: + ahab_container.image_array.append( + ImageArrayEntry.load_from_config(ahab_container, image) + ) + + return ahab_container + + def image_info(self) -> BinaryImage: + """Get Image info object. + + :return: AHAB Container Info object. + """ + ret = BinaryImage( + name="AHAB Container", + size=self.header_length(), + offset=0, + binary=self.export(), + description=(f"AHAB Container for {self.flag_srk_set}" f"_SWver:{self.sw_version}"), + ) + return ret + + +class AHABImage: + """Class representing an AHAB image. + + The image consists of multiple AHAB containers. + """ + + TARGET_MEMORIES = [ + TARGET_MEMORY_SERIAL_DOWNLOADER, + TARGET_MEMORY_NOR, + TARGET_MEMORY_NAND_4K, + TARGET_MEMORY_NAND_2K, + ] + + def __init__( + self, + family: str, + revision: str = "latest", + target_memory: str = TARGET_MEMORY_NOR, + ahab_containers: Optional[List[AHABContainer]] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """AHAB Image constructor. + + :param family: Name of device family. + :param revision: Device silicon revision, defaults to "latest" + :param target_memory: Target memory for AHAB image [serial_downloader, nor, nand], defaults to "nor" + :param ahab_containers: _description_, defaults to None + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKValueError: Invalid input configuration. + """ + if target_memory not in self.TARGET_MEMORIES: + raise SPSDKValueError( + f"Invalid AHAB target memory [{target_memory}]." + f" The list of supported images: [{','.join(self.TARGET_MEMORIES)}]" + ) + self.target_memory = target_memory + self.family = family + self.search_paths = search_paths + self._database = get_db(family, revision) + self.revision = self._database.name + self.ahab_address_map: List[int] = self._database.get_list(DatabaseManager.AHAB, "ahab_map") + self.start_image_address = ( + START_IMAGE_ADDRESS_NAND + if target_memory in [TARGET_MEMORY_NAND_2K, TARGET_MEMORY_NAND_4K] + else START_IMAGE_ADDRESS + ) + self.containers_max_cnt = self._database.get_int(DatabaseManager.AHAB, "containers_max_cnt") + self.images_max_cnt = self._database.get_int(DatabaseManager.AHAB, "oem_images_max_cnt") + self.srkh_sha_supports: List[str] = self._database.get_list( + DatabaseManager.AHAB, "srkh_sha_supports" + ) + self.ahab_containers: List[AHABContainer] = ahab_containers or [] + + def __repr__(self) -> str: + return f"AHAB Image for {self.family}" + + def __str__(self) -> str: + return ( + "AHAB Image:\n" + f" Family: {self.family}\n" + f" Revision: {self.revision}\n" + f" Target memory: {self.target_memory}\n" + f" Max cont. count: {self.containers_max_cnt}" + f" Max image. count: {self.images_max_cnt}" + f" Containers count: {len(self.ahab_containers)}" + ) + + def add_container(self, container: AHABContainer) -> None: + """Add new container into AHAB Image. + + The order of the added images is important. + :param container: New AHAB Container to be added. + :raises SPSDKLengthError: The container count in image is overflowed. + """ + if len(self.ahab_containers) >= self.containers_max_cnt: + raise SPSDKLengthError( + "Cannot add new container because the AHAB Image already reached" + f" the maximum count: {self.containers_max_cnt}" + ) + + self.ahab_containers.append(container) + + def clear(self) -> None: + """Clear list of containers.""" + self.ahab_containers.clear() + + def update_fields(self, update_offsets: bool = True) -> None: + """Automatically updates all volatile fields in every AHAB container. + + :param update_offsets: Update also offsets for serial_downloader. + """ + for ahab_container in self.ahab_containers: + ahab_container.update_fields() + + if self.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER and update_offsets: + # Update the Image offsets to be without gaps + offset = self.start_image_address + for ahab_container in self.ahab_containers: + for image in ahab_container.image_array: + if ahab_container.lock: + offset = image.image_offset + else: + image.image_offset = offset + offset = image.get_valid_offset(offset + image.image_size) + + ahab_container.update_fields() + + def __len__(self) -> int: + """Get maximal size of AHAB Image. + + :return: Size in Bytes of AHAB Image. + """ + lengths = [0] + for container in self.ahab_containers: + lengths.append(len(container)) + return align(max(lengths), CONTAINER_ALIGNMENT) + + def get_containers_size(self) -> int: + """Get maximal containers size. + + In fact get the offset where could be stored first data. + + :return: Size of containers. + """ + if len(self.ahab_containers) == 0: + return 0 + sizes = [ + container.header_length() + address + for container, address in zip(self.ahab_containers, self.ahab_address_map) + ] + return align(max(sizes), CONTAINER_ALIGNMENT) + + def get_first_data_image_address(self) -> int: + """Get first data image address. + + :return: Address of first data image. + """ + addresses = [] + for container in self.ahab_containers: + addresses.extend([x.image_offset for x in container.image_array]) + return min(addresses) + + def export(self) -> bytes: + """Export AHAB Image. + + :raises SPSDKValueError: mismatch between number of containers and offsets. + :raises SPSDKValueError: number of images mismatch. + :return: bytes AHAB Image. + """ + self.update_fields() + self.validate() + return self.image_info().export() + + def image_info(self) -> BinaryImage: + """Get Image info object.""" + ret = BinaryImage( + name="AHAB Image", + size=len(self), + offset=0, + description=f"AHAB Image for {self.family}_{self.revision}", + pattern=BinaryPattern("0xCA"), + ) + ahab_containers = BinaryImage( + name="AHAB Containers", + size=self.start_image_address, + offset=0, + description="AHAB Containers block", + pattern=BinaryPattern("zeros"), + ) + ret.add_image(ahab_containers) + + for cnt_ix, (container, address) in enumerate( + zip(self.ahab_containers, self.ahab_address_map) + ): + container_image = container.image_info() + container_image.name = container_image.name + f" {cnt_ix}" + container_image.offset = address + ahab_containers.add_image(container_image) + + # Add also all data images + for img_ix, image_entry in enumerate(container.image_array): + data_image = BinaryImage( + name=f"Container {cnt_ix} AHAB Data Image {img_ix}", + binary=image_entry.image, + size=image_entry.image_size, + offset=image_entry.image_offset, + description=( + f"AHAB {'encrypted ' if image_entry.flags_is_encrypted else ''}" + f"data block with {image_entry.flags_image_type} Image Type." + ), + ) + + ret.add_image(data_image) + + return ret + + def validate(self) -> None: + """Validate object data. + + :raises SPSDKValueError: Invalid any value of Image Array entry. + :raises SPSDKError: In case of Binary Image validation fail. + """ + if self.ahab_containers is None or len(self.ahab_containers) == 0: + raise SPSDKValueError("AHAB Image: Missing Containers.") + if len(self.ahab_containers) > self.containers_max_cnt: + raise SPSDKValueError( + "AHAB Image: Too much AHAB containers in image." + f" {len(self.ahab_containers)} > {self.containers_max_cnt}" + ) + # prepare additional validation data + data = {} + data["srkh_sha_supports"] = self.srkh_sha_supports + + for cnt_ix, container in enumerate(self.ahab_containers): + container.validate(data) + if len(container.image_array) > self.images_max_cnt: + raise SPSDKValueError( + f"AHAB Image: Too many binary images in AHAB Container [{cnt_ix}]." + f" {len(container.image_array)} > {self.images_max_cnt}" + ) + if self.target_memory != TARGET_MEMORY_SERIAL_DOWNLOADER: + for img_ix, image_entry in enumerate(container.image_array): + if image_entry.image_offset_real < self.start_image_address: + raise SPSDKValueError( + "AHAB Data Image: The offset of data image (container" + f"{cnt_ix}/image{img_ix}) is under minimal allowed value." + f" 0x{hex(image_entry.image_offset_real)} < {hex(self.start_image_address)}" + ) + + # Validate correct data image offsets + offset = self.start_image_address + alignment = self.ahab_containers[0].image_array[0].get_valid_alignment() + for container in self.ahab_containers: + for image in container.image_array: + if image.image_offset_real != align(image.image_offset_real, alignment): + raise SPSDKValueError( + f"Image Entry: Invalid Image Offset alignment for target memory '{self.target_memory}': " + f"{hex(image.image_offset_real)} " + f"should be with alignment {hex(alignment)}.\n" + f"For example: Bootable image offset ({hex(TARGET_MEMORY_BOOT_OFFSETS[self.target_memory])})" + " + offset (" + f"{hex(align(image.image_offset, alignment) - TARGET_MEMORY_BOOT_OFFSETS[self.target_memory])})" + " is correctly aligned." + ) + if self.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER: + if offset != image.image_offset and not container.lock: + raise SPSDKValueError( + "Invalid image offset for Serial Downloader mode." + f"\n Expected {hex(offset)}, Used:{hex(image.image_offset_real)}" + ) + else: + offset = image.image_offset + offset = image.get_valid_offset(offset + image.image_size) + alignment = image.get_valid_alignment() + + # Validate also overlapped images + try: + self.image_info().validate() + except SPSDKError as exc: + logger.error(self.image_info().draw()) + raise SPSDKError("Validation failed") from exc + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "AHABImage": + """Converts the configuration option into an AHAB image object. + + "config" content array of containers configurations. + + :param config: array of AHAB containers configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKValueError: if the count of AHAB containers is invalid. + :raises SPSDKParsingError: Cannot parse input binary AHAB container. + :return: Initialized AHAB Image. + """ + containers_config: List[Dict[str, Any]] = config["containers"] + family = config["family"] + revision = config.get("revision", "latest") + target_memory = config.get("target_memory") + if target_memory is None: + # backward compatible reading of obsolete image type + image_type = config["image_type"] + target_memory = { + "xip": "nor", + "non_xip": "nor", + "nand": "nand_2k", + "serial_downloader": "serial_downloader", + }[image_type] + logger.warning( + f"The obsolete key 'image_type':{image_type} has been converted into 'target_memory':{target_memory}" + ) + ahab = AHABImage( + family=family, revision=revision, target_memory=target_memory, search_paths=search_paths + ) + i = 0 + for container_config in containers_config: + binary_container = container_config.get("binary_container") + if binary_container: + assert isinstance(binary_container, dict) + path = binary_container.get("path") + assert path + ahab_bin = load_binary(path, search_paths=search_paths) + for j in range(ahab.containers_max_cnt): + try: + ahab.add_container( + AHABContainer.parse( + ahab_bin[ahab.ahab_address_map[j] :], parent=ahab, container_id=i + ) + ) + i += 1 + except SPSDKError as exc: + if j == 0: + raise SPSDKParsingError( + f"AHAB Binary Container parsing failed. ({str(exc)})" + ) from exc + else: + break + + else: + ahab.add_container( + AHABContainer.load_from_config(ahab, container_config["container"], i) + ) + i += 1 + + return ahab + + def parse(self, binary: bytes) -> None: + """Parse input binary chunk to the container object. + + :raises SPSDKError: No AHAB container found in binary data. + """ + self.clear() + + for i, address in enumerate(self.ahab_address_map): + try: + container = AHABContainer.parse(binary[address:], parent=self, container_id=i) + self.ahab_containers.append(container) + except SPSDKParsingError as exc: + logger.debug(f"AHAB Image parsing error:\n{str(exc)}") + except SPSDKError as exc: + raise SPSDKError(f"AHAB Container parsing failed: {str(exc)}.") from exc + if len(self.ahab_containers) == 0: + raise SPSDKError("No AHAB Container has been found in binary data.") + + @staticmethod + def get_supported_families() -> List[str]: + """Get all supported families for AHAB container. + + :return: List of supported families. + """ + return get_families(DatabaseManager.AHAB) + + @staticmethod + def get_validation_schemas() -> List[Dict[str, Any]]: + """Get list of validation schemas. + + :return: Validation list of schemas. + """ + sch = DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)["whole_ahab_image"] + sch["properties"]["family"]["enum"] = AHABImage.get_supported_families() + return [sch] + + @staticmethod + def generate_config_template(family: str) -> Dict[str, Any]: + """Generate AHAB configuration template. + + :param family: Family for which the template should be generated. + :return: Dictionary of individual templates (key is name of template, value is template itself). + """ + val_schemas = AHABImage.get_validation_schemas() + val_schemas[0]["properties"]["family"]["template_value"] = family + + yaml_data = CommentedConfig( + f"Advanced High-Assurance Boot Configuration template for {family}.", val_schemas + ).get_template() + + return {f"{family}_ahab": yaml_data} + + def create_config(self, data_path: str) -> Dict[str, Any]: + """Create configuration of the AHAB Image. + + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + cfg: Dict[str, Any] = {} + cfg["family"] = self.family + cfg["revision"] = self.revision + cfg["target_memory"] = self.target_memory + cfg["output"] = "N/A" + cfg_containers = [] + for cnt_ix, container in enumerate(self.ahab_containers): + cfg_containers.append(container.create_config(cnt_ix, data_path)) + cfg["containers"] = cfg_containers + + return cfg + + def create_srk_hash_blhost_script(self, container_ix: int = 0) -> str: + """Create BLHOST script to load SRK hash into fuses. + + :param container_ix: Container index. + :raises SPSDKValueError: Invalid input value - Non existing container or unsupported type. + :raises SPSDKError: Invalid SRK hash. + :return: Script used by BLHOST to load SRK hash. + """ + if container_ix > len(self.ahab_containers): + raise SPSDKValueError(f"Invalid Container index: {container_ix}.") + container_type = self.ahab_containers[container_ix].flag_srk_set + + fuses_start = self._database.get_int( + DatabaseManager.AHAB, f"{container_type}_srkh_fuses_start" + ) + fuses_count = self._database.get_int( + DatabaseManager.AHAB, f"{container_type}_srkh_fuses_count" + ) + fuses_size = self._database.get_int( + DatabaseManager.AHAB, f"{container_type}_srkh_fuses_size" + ) + if fuses_start is None or fuses_count is None or fuses_size is None: + raise SPSDKValueError( + f"Unsupported container type({container_type}) to create BLHOST script" + ) + + srk_table = self.ahab_containers[container_ix].signature_block.srk_table + if srk_table is None: + raise SPSDKError("The selected AHAB container doesn't contain SRK table.") + + srkh = srk_table.compute_srk_hash() + + if len(srkh) != fuses_count * fuses_size: + raise SPSDKError( + f"The SRK hash length ({len(srkh)}) doesn't fit to fuses space ({fuses_count*fuses_size})." + ) + ret = ( + "# BLHOST SRK Hash fuses programming script\n" + f"# Generated by SPSDK {spsdk_version}\n" + f"# Chip: {self.family} rev:{self.revision}\n" + f"# SRK Hash(Big Endian): {srkh.hex()}\n\n" + ) + srkh_rev = reverse_bytes_in_longs(srkh) + for fuse_ix in range(fuses_count): + value = srkh_rev[fuse_ix * 4 : fuse_ix * 4 + 4] + ret += f"# OEM SRKH{fuses_count-1-fuse_ix} fuses.\n" + ret += f"efuse-program-once {hex(fuses_start+fuse_ix)} 0x{value.hex()}\n" + + return ret diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py new file mode 100644 index 00000000..3922e69a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py @@ -0,0 +1,1556 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Implementation of raw AHAB container support. + +This module represents a generic AHAB container implementation. You can set the +containers values at will. From this perspective, consult with your reference +manual of your device for allowed values. +""" +import datetime +import logging +from abc import abstractmethod +from inspect import isclass +from struct import calcsize, pack, unpack +from typing import Any, Dict, List, Optional, Tuple, Type + +from typing_extensions import Self + +from ...exceptions import SPSDKError, SPSDKValueError +from ...image.ahab.ahab_abstract_interfaces import LITTLE_ENDIAN, Container +from ...image.ahab.ahab_container import ( + CONTAINER_ALIGNMENT, + RESERVED, + UINT8, + UINT16, + UINT32, + AHABContainerBase, + AHABImage, + SignatureBlock, +) +from ...utils.database import DatabaseManager +from ...utils.images import BinaryImage +from ...utils.misc import Endianness, align_block, check_range, load_hex_string, value_to_int +from ...utils.schema_validator import CommentedConfig +from ...utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + + +class SignedMessageTags(SpsdkEnum): + """Signed message container related tags.""" + + SIGNED_MSG = (0x89, "SIGNED_MSG", "Signed message.") + + +class MessageCommands(SpsdkEnum): + """Signed messages commands.""" + + KEYSTORE_REPROVISIONING_ENABLE_REQ = ( + 0x3F, + "KEYSTORE_REPROVISIONING_ENABLE_REQ", + "Key store reprovisioning enable", + ) + + KEY_EXCHANGE_REQ = ( + 0x47, + "KEY_EXCHANGE_REQ", + "Key exchange signed message content", + ) + + RETURN_LIFECYCLE_UPDATE_REQ = ( + 0xA0, + "RETURN_LIFECYCLE_UPDATE_REQ", + "Return lifecycle update request.", + ) + WRITE_SEC_FUSE_REQ = (0x91, "WRITE_SEC_FUSE_REQ", "Write secure fuse request.") + + +class Message(Container): + """Class representing the Signed message. + + Message:: + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Message header | + +-----+---------------------------------------------------------------+ + |0x10 | Message payload | + +-----+---------------------------------------------------------------+ + + + Message header:: + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Cert version | Permission | Issue date | + +-----+--------------+--------------+---------------------------------+ + |0x04 | Reserved | Command | Reserved | + +-----+--------------+--------------+---------------------------------+ + |0x08 | Unique ID (Lower 32 bits) | + +-----+---------------------------------------------------------------+ + |0x0c | Unique ID (Upper 32 bits) | + +-----+---------------------------------------------------------------+ + + The message header is common for all signed messages. + + """ + + UNIQUE_ID_LEN = 8 + TAG = 0 + PAYLOAD_LENGTH = 0 + + def __init__( + self, + cert_ver: int = 0, + permissions: int = 0, + issue_date: Optional[int] = None, + cmd: int = 0, + unique_id: Optional[bytes] = None, + ) -> None: + """Message used to sign and send to device with EdgeLock. + + :param cert_ver: Certificate version, defaults to 0 + :param permissions: Certificate permission, to be used in future + The stated permission must allow the operation requested by the signed message + , defaults to 0 + :param issue_date: Issue date, defaults to None (Current date will be applied) + :param cmd: Message command ID, defaults to 0 + :param unique_id: UUID of device (least 64 bits is used), defaults to None + """ + self.cert_ver = cert_ver + self.permissions = permissions + now = datetime.datetime.now() + self.issue_date = issue_date or (now.month << 12 | now.year) + self.cmd = cmd + self.unique_id = unique_id or b"" + + def __repr__(self) -> str: + return f"Message, {MessageCommands.get_description(self.TAG, 'Base Class')}" + + def __str__(self) -> str: + ret = repr(self) + ":\n" + ret += ( + f" Certificate version:{self.cert_ver}\n" + f" Permissions: {hex(self.permissions)}\n" + f" Issue date: {hex(self.issue_date)}\n" + f" UUID: {self.unique_id.hex() if self.unique_id else 'Not Available'}" + ) + return ret + + def __len__(self) -> int: + """Returns the total length of a container. + + The length includes the fixed as well as the variable length part. + """ + return self.fixed_length() + self.payload_len + + @property + def payload_len(self) -> int: + """Message payload length in bytes.""" + return self.PAYLOAD_LENGTH + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() + + UINT16 # Issue Date + + UINT8 # Permission + + UINT8 # Certificate version + + UINT16 # Reserved to zero + + UINT8 # Command + + UINT8 # Reserved + + "4s" # Unique ID (Lower 32 bits) + + "4s" # Unique ID (Upper 32 bits) + ) + + def validate(self) -> None: + """Validate general message properties.""" + if self.cert_ver is None or not check_range(self.cert_ver, end=(1 << 8) - 1): + raise SPSDKValueError( + f"Message: Invalid certificate version: {hex(self.cert_ver) if self.cert_ver else 'None'}" + ) + + if self.permissions is None or not check_range(self.permissions, end=(1 << 8) - 1): + raise SPSDKValueError( + f"Message: Invalid certificate permission: {hex(self.permissions) if self.permissions else 'None'}" + ) + + if self.issue_date is None or not check_range(self.issue_date, start=1, end=(1 << 16) - 1): + raise SPSDKValueError( + f"Message: Invalid issue date: {hex(self.issue_date) if self.issue_date else 'None'}" + ) + + if self.cmd is None or self.cmd not in MessageCommands.tags(): + raise SPSDKValueError( + f"Message: Invalid command: {hex(self.cmd) if self.cmd else 'None'}" + ) + + if self.unique_id is None or len(self.unique_id) < Message.UNIQUE_ID_LEN: + raise SPSDKValueError( + f"Message: Invalid unique ID: {self.unique_id.hex() if self.unique_id else 'None'}" + ) + + def export(self) -> bytes: + """Exports message into to bytes array. + + :return: Bytes representation of message object. + """ + msg = pack( + self.format(), + self.issue_date, + self.permissions, + self.cert_ver, + RESERVED, + self.cmd, + RESERVED, + self.unique_id[:4], + self.unique_id[4:8], + ) + msg += self.export_payload() + return msg + + @abstractmethod + def export_payload(self) -> bytes: + """Exports message payload to bytes array. + + :return: Bytes representation of message payload. + """ + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Message": + """Converts the configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Message object. + """ + command = config.get("command") + assert command and len(command) == 1 + msg_cls = Message.get_message_class(list(command.keys())[0]) + return msg_cls.load_from_config(config, search_paths=search_paths) + + @staticmethod + def load_from_config_generic(config: Dict[str, Any]) -> Tuple[int, int, Optional[int], bytes]: + """Converts the general configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :return: Message object. + """ + cert_ver = value_to_int(config.get("cert_version", 0)) + permission = value_to_int(config.get("cert_permission", 0)) + issue_date_raw = config.get("issue_date", None) + if issue_date_raw: + assert isinstance(issue_date_raw, str) + year, month = issue_date_raw.split("-") + issue_date = max(min(12, int(month)), 1) << 12 | int(year) + else: + issue_date = None + + uuid = bytes.fromhex(config.get("uuid", bytes(Message.UNIQUE_ID_LEN).hex())) + return (cert_ver, permission, issue_date, uuid) + + def _create_general_config(self) -> Dict[str, Any]: + """Create configuration of the general parts of Message. + + :return: Configuration dictionary. + """ + assert self.unique_id + cfg: Dict[str, Any] = {} + cfg["cert_version"] = self.cert_ver + cfg["cert_permission"] = self.permissions + cfg["issue_date"] = f"{(self.issue_date & 0xfff)}-{(self.issue_date>>12) & 0xf}" + cfg["uuid"] = self.unique_id.hex() + + return cfg + + @abstractmethod + def create_config(self) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :return: Configuration dictionary. + """ + + @classmethod + def get_message_class(cls, cmd: str) -> Type[Self]: + """Get the dedicated message class for command.""" + for var in globals(): + obj = globals()[var] + if isclass(obj) and issubclass(obj, Message) and obj is not Message: + assert issubclass(obj, Message) + if MessageCommands.from_label(cmd) == obj.TAG: + return obj # type: ignore + + raise SPSDKValueError(f"Command {cmd} is not supported.") + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary to the signed message object. + + :param data: Binary data with Container block to parse. + :return: Object recreated from the binary data. + """ + ( + issue_date, # issue Date + permission, # permission + certificate_version, # certificate version + _, # Reserved to zero + command, # Command + _, # Reserved + uuid_lower, # Unique ID (Lower 32 bits) + uuid_upper, # Unique ID (Upper 32 bits) + ) = unpack(Message.format(), data[: Message.fixed_length()]) + + cmd_name = MessageCommands.get_label(command) + msg_cls = Message.get_message_class(cmd_name) + parsed_msg = msg_cls( + cert_ver=certificate_version, + permissions=permission, + issue_date=issue_date, + unique_id=uuid_lower + uuid_upper, + ) + parsed_msg.parse_payload(data[Message.fixed_length() :]) + return parsed_msg # type: ignore + + @abstractmethod + def parse_payload(self, data: bytes) -> None: + """Parse payload. + + :param data: Binary data with Payload to parse. + """ + + +class MessageReturnLifeCycle(Message): + """Return life cycle request message class representation.""" + + TAG = MessageCommands.RETURN_LIFECYCLE_UPDATE_REQ.tag + PAYLOAD_LENGTH = 4 + + def __init__( + self, + cert_ver: int = 0, + permissions: int = 0, + issue_date: Optional[int] = None, + unique_id: Optional[bytes] = None, + life_cycle: int = 0, + ) -> None: + """Message used to sign and send to device with EdgeLock. + + :param cert_ver: Certificate version, defaults to 0 + :param permissions: Certificate permission, to be used in future + The stated permission must allow the operation requested by the signed message + , defaults to 0 + :param issue_date: Issue date, defaults to None (Current date will be applied) + :param unique_id: UUID of device (least 64 bits is used), defaults to None + :param life_cycle: Requested life cycle, defaults to 0 + """ + super().__init__( + cert_ver=cert_ver, + permissions=permissions, + issue_date=issue_date, + cmd=self.TAG, + unique_id=unique_id, + ) + self.life_cycle = life_cycle + + def __str__(self) -> str: + ret = super().__str__() + ret += f" Life Cycle: {hex(self.life_cycle)}" + return ret + + def export_payload(self) -> bytes: + """Exports message payload to bytes array. + + :return: Bytes representation of message payload. + """ + return self.life_cycle.to_bytes(length=4, byteorder=Endianness.LITTLE.value) + + def parse_payload(self, data: bytes) -> None: + """Parse payload. + + :param data: Binary data with Payload to parse. + """ + self.life_cycle = int.from_bytes(data[:4], byteorder=Endianness.LITTLE.value) + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Message": + """Converts the configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid configuration detected. + :return: Message object. + """ + command = config.get("command", {}) + if not isinstance(command, dict) or len(command) != 1: + raise SPSDKError(f"Invalid config field command: {command}") + command_name = list(command.keys())[0] + if MessageCommands.from_label(command_name) != MessageReturnLifeCycle.TAG: + raise SPSDKError("Invalid configuration for Return Life Cycle Request command.") + + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + + life_cycle = command.get("RETURN_LIFECYCLE_UPDATE_REQ") + assert isinstance(life_cycle, int) + + return MessageReturnLifeCycle( + cert_ver=cert_ver, + permissions=permission, + issue_date=issue_date, + unique_id=uuid, + life_cycle=life_cycle, + ) + + def create_config(self) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :return: Configuration dictionary. + """ + cfg = self._create_general_config() + cmd_cfg = {} + cmd_cfg[MessageCommands.get_label(self.TAG)] = self.life_cycle + cfg["command"] = cmd_cfg + + return cfg + + def validate(self) -> None: + """Validate general message properties.""" + super().validate() + if self.life_cycle is None: + raise SPSDKValueError("Message Return Life Cycle request: Invalid life cycle") + + +class MessageWriteSecureFuse(Message): + """Write secure fuse request message class representation.""" + + TAG = MessageCommands.WRITE_SEC_FUSE_REQ.tag + PAYLOAD_FORMAT = LITTLE_ENDIAN + UINT16 + UINT8 + UINT8 + + def __init__( + self, + cert_ver: int = 0, + permissions: int = 0, + issue_date: Optional[int] = None, + unique_id: Optional[bytes] = None, + fuse_id: int = 0, + length: int = 0, + flags: int = 0, + data: Optional[List[int]] = None, + ) -> None: + """Message used to sign and send to device with EdgeLock. + + :param cert_ver: Certificate version, defaults to 0 + :param permissions: Certificate permission, to be used in future + The stated permission must allow the operation requested by the signed message + , defaults to 0 + :param issue_date: Issue date, defaults to None (Current date will be applied) + :param unique_id: UUID of device (least 64 bits is used), defaults to None + :param fuse_id: Fuse ID, defaults to 0 + :param length: Fuse length, defaults to 0 + :param flags: Fuse flags, defaults to 0 + :param data: List of fuse values + """ + super().__init__( + cert_ver=cert_ver, + permissions=permissions, + issue_date=issue_date, + cmd=self.TAG, + unique_id=unique_id, + ) + self.fuse_id = fuse_id + self.length = length + self.flags = flags + self.fuse_data: List[int] = data or [] + + def __str__(self) -> str: + ret = super().__str__() + ret += f" Fuse Index: {hex(self.fuse_id)}, {self.fuse_id}\n" + ret += f" Fuse Length: {self.length}\n" + ret += f" Fuse Flags: {hex(self.flags)}\n" + for i, data in enumerate(self.fuse_data): + ret += f" Fuse{i} Value: 0x{data:08X}" + return ret + + @property + def payload_len(self) -> int: + """Message payload length in bytes.""" + return 4 + len(self.fuse_data) * 4 + + def export_payload(self) -> bytes: + """Exports message payload to bytes array. + + :return: Bytes representation of message payload. + """ + payload = pack(self.PAYLOAD_FORMAT, self.fuse_id, self.length, self.flags) + for data in self.fuse_data: + payload += data.to_bytes(4, Endianness.LITTLE.value) + return payload + + def parse_payload(self, data: bytes) -> None: + """Parse payload. + + :param data: Binary data with Payload to parse. + """ + self.fuse_id, self.length, self.flags = unpack(self.PAYLOAD_FORMAT, data[:4]) + self.fuse_data.clear() + for i in range(self.length): + self.fuse_data.append( + int.from_bytes(data[4 + i * 4 : 8 + i * 4], Endianness.LITTLE.value) + ) + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Message": + """Converts the configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid configuration detected. + :return: Message object. + """ + command = config.get("command", {}) + if not isinstance(command, dict) or len(command) != 1: + raise SPSDKError(f"Invalid config field command: {command}") + command_name = list(command.keys())[0] + if MessageCommands.from_label(command_name) != MessageWriteSecureFuse.TAG: + raise SPSDKError("Invalid configuration for Write secure fuse Request command.") + + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + + secure_fuse = command.get("WRITE_SEC_FUSE_REQ") + assert isinstance(secure_fuse, dict) + fuse_id = secure_fuse.get("id") + assert isinstance(fuse_id, int) + flags: int = secure_fuse.get("flags", 0) + data_list: List = secure_fuse.get("data", []) + data = [] + for x in data_list: + data.append(value_to_int(x)) + length = len(data_list) + return MessageWriteSecureFuse( + cert_ver=cert_ver, + permissions=permission, + issue_date=issue_date, + unique_id=uuid, + fuse_id=fuse_id, + length=length, + flags=flags, + data=data, + ) + + def create_config(self) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :return: Configuration dictionary. + """ + cfg = self._create_general_config() + write_fuse_cfg: Dict[str, Any] = {} + cmd_cfg = {} + write_fuse_cfg["id"] = self.fuse_id + write_fuse_cfg["flags"] = self.flags + write_fuse_cfg["data"] = [f"0x{x:08X}" for x in self.fuse_data] + + cmd_cfg[MessageCommands.get_label(self.TAG)] = write_fuse_cfg + cfg["command"] = cmd_cfg + + return cfg + + def validate(self) -> None: + """Validate general message properties.""" + super().validate() + if self.fuse_data is None: + raise SPSDKValueError("Message Write secure fuse request: Missing fuse data") + if len(self.fuse_data) != self.length: + raise SPSDKValueError( + "Message Write secure fuse request: The fuse value list " + f"has invalid length: ({len(self.fuse_data)} != {self.length})" + ) + + for i, val in enumerate(self.fuse_data): + if val >= 1 << 32: + raise SPSDKValueError( + f"Message Write secure fuse request: The fuse value({i}) is bigger than 32 bit: ({val})" + ) + + +class MessageKeyStoreReprovisioningEnable(Message): + """Key store reprovisioning enable request message class representation.""" + + TAG = MessageCommands.KEYSTORE_REPROVISIONING_ENABLE_REQ.tag + PAYLOAD_LENGTH = 12 + PAYLOAD_FORMAT = LITTLE_ENDIAN + UINT8 + UINT8 + UINT16 + UINT32 + UINT32 + + FLAGS = 0 # 0 : HSM storage. + TARGET = 0 # Target ELE + + def __init__( + self, + cert_ver: int = 0, + permissions: int = 0, + issue_date: Optional[int] = None, + unique_id: Optional[bytes] = None, + monotonic_counter: int = 0, + user_sab_id: int = 0, + ) -> None: + """Key store reprovisioning enable signed message class init. + + :param cert_ver: Certificate version, defaults to 0 + :param permissions: Certificate permission, to be used in future + The stated permission must allow the operation requested by the signed message + , defaults to 0 + :param issue_date: Issue date, defaults to None (Current date will be applied) + :param unique_id: UUID of device (least 64 bits is used), defaults to None + :param monotonic_counter: Monotonic counter value, defaults to 0 + :param user_sab_id: User SAB id, defaults to 0 + """ + super().__init__( + cert_ver=cert_ver, + permissions=permissions, + issue_date=issue_date, + cmd=self.TAG, + unique_id=unique_id, + ) + self.flags = self.FLAGS + self.target = self.TARGET + self.reserved = RESERVED + self.monotonic_counter = monotonic_counter + self.user_sab_id = user_sab_id + + def export_payload(self) -> bytes: + """Exports message payload to bytes array. + + :return: Bytes representation of message payload. + """ + return pack( + self.PAYLOAD_FORMAT, + self.flags, + self.target, + self.reserved, + self.monotonic_counter, + self.user_sab_id, + ) + + def parse_payload(self, data: bytes) -> None: + """Parse payload. + + :param data: Binary data with Payload to parse. + """ + self.flags, self.target, self.reserved, self.monotonic_counter, self.user_sab_id = unpack( + self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH] + ) + + def validate(self) -> None: + """Validate general message properties.""" + super().validate() + if self.flags != self.FLAGS: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid flags: {self.flags}" + ) + if self.target != self.TARGET: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid target: {self.target}" + ) + if self.reserved != RESERVED: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid reserved field: {self.reserved}" + ) + if self.monotonic_counter >= 1 << 32: + raise SPSDKValueError( + "Message Key store reprovisioning enable request: Invalid monotonic " + f"counter field (not fit in 32bit): {self.monotonic_counter}" + ) + + if self.user_sab_id >= 1 << 32: + raise SPSDKValueError( + "Message Key store reprovisioning enable request: Invalid user SAB ID " + f"field (not fit in 32bit): {self.user_sab_id}" + ) + + def __str__(self) -> str: + ret = super().__str__() + ret += ( + f" Monotonic counter value: 0x{self.monotonic_counter:08X}, {self.monotonic_counter}\n" + ) + ret += f" User SAB id: 0x{self.user_sab_id:08X}, {self.user_sab_id}" + return ret + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Message": + """Converts the configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid configuration detected. + :return: Message object. + """ + command = config.get("command", {}) + if not isinstance(command, dict) or len(command) != 1: + raise SPSDKError(f"Invalid config field command: {command}") + command_name = list(command.keys())[0] + if MessageCommands.from_label(command_name) != MessageKeyStoreReprovisioningEnable.TAG: + raise SPSDKError("Invalid configuration for Write secure fuse Request command.") + + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + + keystore_repr_en = command.get("KEYSTORE_REPROVISIONING_ENABLE_REQ") + assert isinstance(keystore_repr_en, dict) + monotonic_counter = value_to_int(keystore_repr_en.get("monotonic_counter", 0)) + user_sab_id = value_to_int(keystore_repr_en.get("user_sab_id", 0)) + return MessageKeyStoreReprovisioningEnable( + cert_ver=cert_ver, + permissions=permission, + issue_date=issue_date, + unique_id=uuid, + monotonic_counter=monotonic_counter, + user_sab_id=user_sab_id, + ) + + def create_config(self) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :return: Configuration dictionary. + """ + cfg = self._create_general_config() + keystore_repr_en_cfg: Dict[str, Any] = {} + cmd_cfg = {} + keystore_repr_en_cfg["monotonic_counter"] = f"0x{self.monotonic_counter:08X}" + keystore_repr_en_cfg["user_sab_id"] = f"0x{self.user_sab_id:08X}" + + cmd_cfg[MessageCommands.get_label(self.TAG)] = keystore_repr_en_cfg + cfg["command"] = cmd_cfg + + return cfg + + +class MessageKeyExchange(Message): + """Key exchange request message class representation.""" + + TAG = MessageCommands.KEY_EXCHANGE_REQ.tag + PAYLOAD_LENGTH = 27 * 4 + PAYLOAD_VERSION = 0x07 + PAYLOAD_FORMAT = ( + LITTLE_ENDIAN + + UINT8 # TAG + + UINT8 # Version + + UINT16 # Reserved + + UINT32 # Key store ID + + UINT32 # Key exchange algorithm + + UINT16 # Salt Flags + + UINT16 # Derived key group + + UINT16 # Derived key size bits + + UINT16 # Derived key type + + UINT32 # Derived key lifetime + + UINT32 # Derived key usage + + UINT32 # Derived key permitted algorithm + + UINT32 # Derived key lifecycle + + UINT32 # Derived key ID + + UINT32 # Private key ID + + "32s" # Input peer public key digest word [0-7] + + "32s" # Input user fixed info digest word [0-7] + ) + + class KeyExchangeAlgorithm(SpsdkEnum): + """Key Exchange Algorithm valid values.""" + + HKDF_SHA256 = (0x09020109, "HKDF SHA256") + HKDF_SHA384 = (0x0902010A, "HKDF SHA384") + + class KeyDerivationAlgorithm(SpsdkEnum): + """Key Derivation Algorithm valid values.""" + + HKDF_SHA256 = (0x08000109, "HKDF SHA256", "HKDF SHA256 (HMAC two-step)") + HKDF_SHA384 = (0x0800010A, "HKDF SHA384", "HKDF SHA384 (HMAC two-step)") + + class DerivedKeyType(SpsdkEnum): + """Derived Key Type valid values.""" + + AES = (0x2400, "AES SHA256", "Possible bit widths: 128/192/256") + HMAC = (0x1100, "HMAC SHA384", "Possible bit widths: 224/256/384/512") + OEM_IMPORT_MK_SK = (0x9200, "OEM_IMPORT_MK_SK", "Possible bit widths: 128/192/256") + + class LifeCycle(SpsdkEnum): + """Chip life cycle valid values.""" + + CURRENT = (0x00, "CURRENT", "Current device lifecycle") + OPEN = (0x01, "OPEN") + CLOSED = (0x02, "CLOSED") + LOCKED = (0x04, "LOCKED") + + class LifeTime(SpsdkEnum): + """Edgelock Enclave life time valid values.""" + + VOLATILE = (0x00, "VOLATILE", "Standard volatile key") + PERSISTENT = (0x01, "PERSISTENT", "Standard persistent key") + PERMANENT = (0xFF, "PERMANENT", "Standard permanent key") + + class DerivedKeyUsage(SpsdkEnum): + """Derived Key Usage valid values.""" + + CACHE = ( + 0x00000004, + "Cache", + ( + "Permission to cache the key in the ELE internal secure memory. " + "This usage is set by default by ELE FW for all keys generated or imported." + ), + ) + ENCRYPT = ( + 0x00000100, + "Encrypt", + ( + "Permission to encrypt a message with the key. It could be cipher encryption," + " AEAD encryption or asymmetric encryption operation." + ), + ) + DECRYPT = ( + 0x00000200, + "Decrypt", + ( + "Permission to decrypt a message with the key. It could be cipher decryption," + " AEAD decryption or asymmetric decryption operation." + ), + ) + SIGN_MSG = ( + 0x00000400, + "Sign message", + ( + "Permission to sign a message with the key. It could be a MAC generation or an " + "asymmetric message signature operation." + ), + ) + VERIFY_MSG = ( + 0x00000800, + "Verify message", + ( + "Permission to verify a message signature with the key. It could be a MAC " + "verification or an asymmetric message signature verification operation." + ), + ) + SIGN_HASH = ( + 0x00001000, + "Sign hash", + ( + "Permission to sign a hashed message with the key with an asymmetric signature " + "operation. Setting this permission automatically sets the Sign Message usage." + ), + ) + VERIFY_HASH = ( + 0x00002000, + "Sign message", + ( + "Permission to verify a hashed message signature with the key with an asymmetric " + "signature verification operation. Setting this permission automatically sets the Verify Message usage." + ), + ) + DERIVE = (0x00004000, "Derive", "Permission to derive other keys from this key.") + + def __init__( + self, + cert_ver: int = 0, + permissions: int = 0, + issue_date: Optional[int] = None, + unique_id: Optional[bytes] = None, + key_store_id: int = 0, + key_exchange_algorithm: KeyExchangeAlgorithm = KeyExchangeAlgorithm.HKDF_SHA256, + salt_flags: int = 0, + derived_key_grp: int = 0, + derived_key_size_bits: int = 0, + derived_key_type: DerivedKeyType = DerivedKeyType.AES, + derived_key_lifetime: LifeTime = LifeTime.PERSISTENT, + derived_key_usage: Optional[List[DerivedKeyUsage]] = None, + derived_key_permitted_algorithm: KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDF_SHA256, + derived_key_lifecycle: LifeCycle = LifeCycle.OPEN, + derived_key_id: int = 0, + private_key_id: int = 0, + input_peer_public_key_digest: bytes = bytes(), + input_user_fixed_info_digest: bytes = bytes(), + ) -> None: + """Key exchange signed message class init. + + :param cert_ver: Certificate version, defaults to 0 + :param permissions: Certificate permission, to be used in future + The stated permission must allow the operation requested by the signed message + , defaults to 0 + :param issue_date: Issue date, defaults to None (Current date will be applied) + :param unique_id: UUID of device (least 64 bits is used), defaults to None + :param key_store_id: Key store ID where to store the derived key. It must be the key store ID + related to the key management handle set in the command API, defaults to 0 + :param key_exchange_algorithm: Algorithm used by the key exchange process: + + | HKDF SHA256 0x09020109 + | HKDF SHA384 0x0902010A + | , defaults to HKDF_SHA256 + + :param salt_flags: Bit field indicating the requested operations: + + | Bit 0: Salt in step #1 (HKDF-extract) of HMAC based two-step key derivation process: + | - 0: Use zeros salt; + | - 1:Use peer public key hash as salt; + | Bit 1: In case of ELE import, salt used to derive OEM_IMPORT_WRAP_SK and OEM_IMPORT_CMAC_SK: + | - 0: Zeros string; + | - 1: Device SRKH. + | Bit 2 to 15: Reserved, defaults to 0 + + :param derived_key_grp: Derived key group. 100 groups are available per key store. It must be a + value in the range [0; 99]. Keys belonging to the same group can be managed through + the Manage key group command, defaults to 0 + :param derived_key_size_bits: Derived key size bits attribute, defaults to 0 + :param derived_key_type: + + +-------------------+-------+------------------+ + |Key type | Value | Key size in bits | + +===================+=======+==================+ + | AES |0x2400 | 128/192/256 | + +-------------------+-------+------------------+ + | HMAC |0x1100 | 224/256/384/512 | + +-------------------+-------+------------------+ + | OEM_IMPORT_MK_SK* |0x9200 | 128/192/256 | + +-------------------+-------+------------------+ + + , defaults to AES + + :param derived_key_lifetime: Derived key lifetime attribute + + | VOLATILE 0x00 Standard volatile key. + | PERSISTENT 0x01 Standard persistent key. + | PERMANENT 0xFF Standard permanent key., defaults to PERSISTENT + + :param derived_key_usage: Derived key usage attribute. + + | Cache 0x00000004 Permission to cache the key in the ELE internal secure memory. + | This usage is set by default by ELE FW for all keys generated or imported. + | Encrypt 0x00000100 Permission to encrypt a message with the key. It could be cipher + | encryption, AEAD encryption or asymmetric encryption operation. + | Decrypt 0x00000200 Permission to decrypt a message with the key. It could be + | cipher decryption, AEAD decryption or asymmetric decryption operation. + | Sign message 0x00000400 Permission to sign a message with the key. It could be + | a MAC generation or an asymmetric message signature operation. + | Verify message 0x00000800 Permission to verify a message signature with the key. + | It could be a MAC verification or an asymmetric message signature + | verification operation. + | Sign hash 0x00001000 Permission to sign a hashed message with the key + | with an asymmetric signature operation. Setting this permission automatically + | sets the Sign Message usage. + | Verify hash 0x00002000 Permission to verify a hashed message signature with + | the key with an asymmetric signature verification operation. + | Setting this permission automatically sets the Verify Message usage. + | Derive 0x00004000 Permission to derive other keys from this key. + | , defaults to 0 + + :param derived_key_permitted_algorithm: Derived key permitted algorithm attribute + + | HKDF SHA256 (HMAC two-step) 0x08000109 + | HKDF SHA384 (HMAC two-step) 0x0800010A, defaults to HKDF_SHA256 + + :param derived_key_lifecycle: Derived key lifecycle attribute + + | CURRENT 0x00 Key is usable in current lifecycle. + | OPEN 0x01 Key is usable in open lifecycle. + | CLOSED 0x02 Key is usable in closed lifecycle. + | CLOSED and LOCKED 0x04 Key is usable in closed and locked lifecycle. + | , defaults to OPEN + + :param derived_key_id: Derived key ID attribute. It could be: + + - Wanted key identifier of the generated key: only supported by persistent + and permanent keys; + - 0x00000000 to let the FW chose the key identifier: supported by all + keys (all persistence levels). , defaults to 0 + + :param private_key_id: Identifier in the ELE key storage of the private key to use with the peer + public key during the key agreement process, defaults to 0 + :param input_peer_public_key_digest: Input peer public key digest buffer. + The algorithm used to generate the digest must be SHA256, defaults to list(8) + :param input_user_fixed_info_digest: Input user fixed info digest buffer. + The algorithm used to generate the digest must be SHA256, defaults to list(8) + """ + super().__init__( + cert_ver=cert_ver, + permissions=permissions, + issue_date=issue_date, + cmd=self.TAG, + unique_id=unique_id, + ) + self.tag = self.TAG + self.version = self.PAYLOAD_VERSION + self.reserved = RESERVED + self.key_store_id = key_store_id + self.key_exchange_algorithm = key_exchange_algorithm + self.salt_flags = salt_flags + self.derived_key_grp = derived_key_grp + self.derived_key_size_bits = derived_key_size_bits + self.derived_key_type = derived_key_type + self.derived_key_lifetime = derived_key_lifetime + self.derived_key_usage = derived_key_usage or [] + self.derived_key_permitted_algorithm = derived_key_permitted_algorithm + self.derived_key_lifecycle = derived_key_lifecycle + self.derived_key_id = derived_key_id + self.private_key_id = private_key_id + self.input_peer_public_key_digest = input_peer_public_key_digest + self.input_user_fixed_info_digest = input_user_fixed_info_digest + + def export_payload(self) -> bytes: + """Exports message payload to bytes array. + + :return: Bytes representation of message payload. + """ + derived_key_usage = 0 + for usage in self.derived_key_usage: + derived_key_usage |= usage.tag + return pack( + self.PAYLOAD_FORMAT, + self.tag, + self.version, + self.reserved, + self.key_store_id, + self.key_exchange_algorithm.tag, + self.derived_key_grp, + self.salt_flags, + self.derived_key_type.tag, + self.derived_key_size_bits, + self.derived_key_lifetime.tag, + derived_key_usage, + self.derived_key_permitted_algorithm.tag, + self.derived_key_lifecycle.tag, + self.derived_key_id, + self.private_key_id, + self.input_peer_public_key_digest, + self.input_user_fixed_info_digest, + ) + + def parse_payload(self, data: bytes) -> None: + """Parse payload. + + :param data: Binary data with Payload to parse. + """ + ( + self.tag, + self.version, + self.reserved, + self.key_store_id, + key_exchange_algorithm, + self.derived_key_grp, + self.salt_flags, + derived_key_type, + self.derived_key_size_bits, + derived_key_lifetime, + derived_key_usage, + derived_key_permitted_algorithm, + derived_key_lifecycle, + self.derived_key_id, + self.private_key_id, + input_peer_public_key_digest, + input_user_fixed_info_digest, + ) = unpack(self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH]) + + # Do some post process + self.key_exchange_algorithm = self.KeyExchangeAlgorithm.from_tag(key_exchange_algorithm) + self.derived_key_type = self.DerivedKeyType.from_tag(derived_key_type) + self.derived_key_lifetime = self.LifeTime.from_tag(derived_key_lifetime) + self.derived_key_permitted_algorithm = self.KeyDerivationAlgorithm.from_tag( + derived_key_permitted_algorithm + ) + self.derived_key_lifecycle = self.LifeCycle.from_tag(derived_key_lifecycle) + + self.input_peer_public_key_digest = input_peer_public_key_digest + self.input_user_fixed_info_digest = input_user_fixed_info_digest + self.derived_key_usage.clear() + for tag in self.DerivedKeyUsage.tags(): + if tag & derived_key_usage: + self.derived_key_usage.append(self.DerivedKeyUsage.from_tag(tag)) + + def validate(self) -> None: + """Validate general message properties.""" + super().validate() + if self.tag != self.TAG: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid tag: {self.tag}" + ) + if self.version != self.version: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid verssion: {self.version}" + ) + if self.reserved != RESERVED: + raise SPSDKValueError( + f"Message Key store reprovisioning enable request: Invalid reserved field: {self.reserved}" + ) + + def __str__(self) -> str: + ret = super().__str__() + ret += f" KeyStore ID value: 0x{self.key_store_id:08X}, {self.key_store_id}\n" + ret += f" Key exchange algorithm value: {self.key_exchange_algorithm.label}\n" + ret += f" Salt flags value: 0x{self.salt_flags:08X}, {self.salt_flags}\n" + ret += f" Derived key group value: 0x{self.derived_key_grp:08X}, {self.derived_key_grp}\n" + ret += f" Derived key bit size value: 0x{self.derived_key_size_bits:08X}, {self.derived_key_size_bits}\n" + ret += f" Derived key type value: {self.derived_key_type.label}\n" + ret += f" Derived key life time value: {self.derived_key_lifetime.label}\n" + ret += f" Derived key usage value: {[x.label for x in self.derived_key_usage]}\n" + ret += f" Derived key permitted algorithm value: {self.derived_key_permitted_algorithm.label}\n" + ret += f" Derived key life cycle value: {self.derived_key_lifecycle.label}\n" + ret += f" Derived key ID value: 0x{self.derived_key_id:08X}, {self.derived_key_id}\n" + ret += f" Private key ID value: 0x{self.private_key_id:08X}, {self.private_key_id}\n" + ret += f" Input peer public key digest value: {self.input_peer_public_key_digest.hex()}\n" + ret += f" Input user public fixed info digest value: {self.input_peer_public_key_digest.hex()}\n" + return ret + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "Message": + """Converts the configuration option into an message object. + + "config" content of container configurations. + + :param config: Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid configuration detected. + :return: Message object. + """ + command = config.get("command", {}) + if not isinstance(command, dict) or len(command) != 1: + raise SPSDKError(f"Invalid config field command: {command}") + command_name = list(command.keys())[0] + if MessageCommands.from_label(command_name) != MessageKeyExchange.TAG: + raise SPSDKError("Invalid configuration forKey Exchange Request command.") + + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + + key_exchange = command.get("KEY_EXCHANGE_REQ") + assert isinstance(key_exchange, dict) + + key_store_id = value_to_int(key_exchange.get("key_store_id", 0)) + key_exchange_algorithm = MessageKeyExchange.KeyExchangeAlgorithm.from_attr( + key_exchange.get("key_exchange_algorithm", "HKDF SHA256") + ) + salt_flags = value_to_int(key_exchange.get("salt_flags", 0)) + derived_key_grp = value_to_int(key_exchange.get("derived_key_grp", 0)) + derived_key_size_bits = value_to_int(key_exchange.get("derived_key_size_bits", 128)) + derived_key_type = MessageKeyExchange.DerivedKeyType.from_attr( + key_exchange.get("derived_key_type", "AES SHA256") + ) + derived_key_lifetime = MessageKeyExchange.LifeTime.from_attr( + key_exchange.get("derived_key_lifetime", "PERSISTENT") + ) + derived_key_usage = [ + MessageKeyExchange.DerivedKeyUsage.from_attr(x) + for x in key_exchange.get("derived_key_usage", []) + ] + derived_key_permitted_algorithm = MessageKeyExchange.KeyDerivationAlgorithm.from_attr( + key_exchange.get("derived_key_permitted_algorithm", "HKDF SHA256") + ) + derived_key_lifecycle = MessageKeyExchange.LifeCycle.from_attr( + key_exchange.get("derived_key_lifecycle", "OPEN") + ) + derived_key_id = value_to_int(key_exchange.get("derived_key_id", 0)) + private_key_id = value_to_int(key_exchange.get("private_key_id", 0)) + input_peer_public_key_digest = load_hex_string( + source=key_exchange.get("input_peer_public_key_digest", bytes(32)), + expected_size=32, + search_paths=search_paths, + ) + input_user_fixed_info_digest = load_hex_string( + source=key_exchange.get("input_user_fixed_info_digest", bytes(32)), + expected_size=32, + search_paths=search_paths, + ) + + return MessageKeyExchange( + cert_ver=cert_ver, + permissions=permission, + issue_date=issue_date, + unique_id=uuid, + key_store_id=key_store_id, + key_exchange_algorithm=key_exchange_algorithm, + salt_flags=salt_flags, + derived_key_grp=derived_key_grp, + derived_key_size_bits=derived_key_size_bits, + derived_key_type=derived_key_type, + derived_key_lifetime=derived_key_lifetime, + derived_key_usage=derived_key_usage, + derived_key_permitted_algorithm=derived_key_permitted_algorithm, + derived_key_lifecycle=derived_key_lifecycle, + derived_key_id=derived_key_id, + private_key_id=private_key_id, + input_peer_public_key_digest=input_peer_public_key_digest, + input_user_fixed_info_digest=input_user_fixed_info_digest, + ) + + def create_config(self) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :return: Configuration dictionary. + """ + cfg = self._create_general_config() + key_exchange_cfg: Dict[str, Any] = {} + cmd_cfg = {} + key_exchange_cfg["key_store_id"] = f"0x{self.key_store_id:08X}" + key_exchange_cfg["key_exchange_algorithm"] = self.key_exchange_algorithm.label + key_exchange_cfg["salt_flags"] = f"0x{self.salt_flags:08X}" + key_exchange_cfg["derived_key_grp"] = self.derived_key_grp + key_exchange_cfg["derived_key_size_bits"] = self.derived_key_size_bits + key_exchange_cfg["derived_key_type"] = self.derived_key_type.label + key_exchange_cfg["derived_key_lifetime"] = self.derived_key_lifetime.label + key_exchange_cfg["derived_key_usage"] = [x.label for x in self.derived_key_usage] + key_exchange_cfg[ + "derived_key_permitted_algorithm" + ] = self.derived_key_permitted_algorithm.label + key_exchange_cfg["derived_key_lifecycle"] = self.derived_key_lifecycle.label + key_exchange_cfg["derived_key_id"] = self.derived_key_id + key_exchange_cfg["private_key_id"] = self.private_key_id + key_exchange_cfg["input_peer_public_key_digest"] = self.input_peer_public_key_digest.hex() + key_exchange_cfg["input_user_fixed_info_digest"] = ( + self.input_user_fixed_info_digest.hex() + if self.input_user_fixed_info_digest + else bytes(32).hex() + ) + + cmd_cfg[MessageCommands.get_label(self.TAG)] = key_exchange_cfg + cfg["command"] = cmd_cfg + + return cfg + + +class SignedMessage(AHABContainerBase): + """Class representing the Signed message. + + Signed Message:: + + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Tag | Length (MSB) | Length (LSB) | Version | + +-----+--------------+--------------+----------------+----------------+ + |0x04 | Flags | + +-----+--------------+--------------+---------------------------------+ + |0x08 | Reserved | Fuse version | Software version | + +-----+--------------+--------------+---------------------------------+ + |0x10 | Message descriptor | + +-----+---------------------------------------------------------------+ + |0x34 | Message header | + +-----+---------------------------------------------------------------+ + |0x44 | Message payload | + +-----+---------------------------------------------------------------+ + |0xXX | Signature Block | + +-----+---------------------------------------------------------------+ + + Message descriptor:: + +-----+--------------+--------------+----------------+----------------+ + |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | + +-----+--------------+--------------+----------------+----------------+ + |0x00 | Reserved | Flags | + +-----+----------------------------------------------+----------------+ + |0x04 | IV (256 bits) | + +-----+---------------------------------------------------------------+ + + """ + + TAG = SignedMessageTags.SIGNED_MSG.tag + ENCRYPT_IV_LEN = 32 + + def __init__( + self, + flags: int = 0, + fuse_version: int = 0, + sw_version: int = 0, + message: Optional[Message] = None, + signature_block: Optional[SignatureBlock] = None, + encrypt_iv: Optional[bytes] = None, + ): + """Class object initializer. + + :param flags: flags. + :param fuse_version: value must be equal to or greater than the version + stored in the fuses to allow loading this container. + :param sw_version: used by PHBC (Privileged Host Boot Companion) to select + between multiple images with same fuse version field. + :param message: Message command to be signed. + :param signature_block: signature block. + :param encrypt_iv: Encryption Initial Vector - if defined the encryption is used. + """ + super().__init__( + flags=flags, + fuse_version=fuse_version, + sw_version=sw_version, + signature_block=signature_block, + ) + self.message = message + self.encrypt_iv = encrypt_iv + + def __eq__(self, other: object) -> bool: + if isinstance(other, SignedMessage): + if super().__eq__(other) and self.message == other.message: + return True + + return False + + def __repr__(self) -> str: + return f"Signed Message, {'Encrypted' if self.encrypt_iv else 'Plain'}" + + def __str__(self) -> str: + return ( + f" Flags: {hex(self.flags)}\n" + f" Fuse version: {hex(self.fuse_version)}\n" + f" SW version: {hex(self.sw_version)}\n" + f" Signature Block:\n{str(self.signature_block)}\n" + f" Message:\n{str(self.message)}\n" + f" Encryption IV: {self.encrypt_iv.hex() if self.encrypt_iv else 'Not Available'}" + ) + + @property + def _signature_block_offset(self) -> int: + """Returns current signature block offset. + + :return: Offset in bytes of Signature block. + """ + # Constant size of Container header + Image array Entry table + assert self.message + return calcsize(self.format()) + len(self.message) + + def __len__(self) -> int: + """Get total length of AHAB container. + + :return: Size in bytes of Message. + """ + return self._signature_block_offset + len(self.signature_block) + + @classmethod + def format(cls) -> str: + """Format of binary representation.""" + return ( + super().format() + + UINT8 # Descriptor Flags + + UINT8 # Reserved + + UINT16 # Reserved + + "32s" # IV - Initial Vector if encryption is enabled + ) + + def update_fields(self) -> None: + """Updates all volatile information in whole container structure. + + :raises SPSDKError: When inconsistent image array length is detected. + """ + # 0. Update length + self.length = len(self) + # 1. Update the signature block to get overall size of it + self.signature_block.update_fields() + # 2. Sign the image header + if self.flag_srk_set != "none": + assert self.signature_block.signature + self.signature_block.signature.sign(self.get_signature_data()) + + def _export(self) -> bytes: + """Export raw data without updates fields into bytes. + + :return: bytes representing container header content including the signature block. + """ + signed_message = pack( + self.format(), + self.version, + len(self), + self.tag, + self.flags, + self.sw_version, + self.fuse_version, + RESERVED, + self._signature_block_offset, + RESERVED, # Reserved field + 1 if self.encrypt_iv else 0, + RESERVED, + RESERVED, + self.encrypt_iv if self.encrypt_iv else bytes(32), + ) + # Add Message Header + Message Payload + assert self.message + signed_message += self.message.export() + # Add Signature Block + signed_message += align_block(self.signature_block.export(), CONTAINER_ALIGNMENT) + return signed_message + + def export(self) -> bytes: + """Export the signed image into one chunk. + + :raises SPSDKValueError: if the number of images doesn't correspond the the number of + entries in image array info. + :return: images exported into single binary + """ + self.update_fields() + self.validate({}) + return self._export() + + def validate(self, data: Dict[str, Any]) -> None: + """Validate object data. + + :param data: Additional validation data. + :raises SPSDKValueError: Invalid any value of Image Array entry + """ + data["flag_used_srk_id"] = self.flag_used_srk_id + + if self.length != len(self): + raise SPSDKValueError( + f"Container Header: Invalid block length: {self.length} != {len(self)}" + ) + super().validate(data) + if self.encrypt_iv and len(self.encrypt_iv) != self.ENCRYPT_IV_LEN: + raise SPSDKValueError( + "Signed Message: Invalid Encryption initialization vector length: " + f"{len(self.encrypt_iv)*8} Bits != {self.ENCRYPT_IV_LEN * 8} Bits" + ) + if self.message is None: + raise SPSDKValueError("Signed Message: Invalid Message payload.") + self.message.validate() + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse input binary to the signed message object. + + :param data: Binary data with Container block to parse. + :return: Object recreated from the binary data. + """ + SignedMessage.check_container_head(data) + image_format = SignedMessage.format() + ( + _, # version + _, # container_length + _, # tag + flags, + sw_version, + fuse_version, + _, # number_of_images + signature_block_offset, + _, # reserved + descriptor_flags, + _, # reserved + _, # reserved + iv, + ) = unpack(image_format, data[: SignedMessage.fixed_length()]) + + parsed_signed_msg = cls( + flags=flags, + fuse_version=fuse_version, + sw_version=sw_version, + encrypt_iv=iv if bool(descriptor_flags & 0x01) else None, + ) + parsed_signed_msg.signature_block = SignatureBlock.parse(data[signature_block_offset:]) + + # Parse also Message itself + parsed_signed_msg.message = Message.parse( + data[SignedMessage.fixed_length() : signature_block_offset] + ) + return parsed_signed_msg + + def create_config(self, data_path: str) -> Dict[str, Any]: + """Create configuration of the Signed Message. + + :param data_path: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + self.validate({}) + cfg = self._create_config(0, data_path) + cfg["family"] = "N/A" + cfg["revision"] = "N/A" + cfg["output"] = "N/A" + + assert self.message + cfg["message"] = self.message.create_config() + + return cfg + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "SignedMessage": + """Converts the configuration option into an Signed message object. + + "config" content of container configurations. + + :param config: Signed Message configuration dictionaries. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Message object. + """ + signed_msg = SignedMessage() + signed_msg.search_paths = search_paths or [] + AHABContainerBase.load_from_config_generic(signed_msg, config) + + message = config.get("message") + assert isinstance(message, dict) + + signed_msg.message = Message.load_from_config(message, search_paths=search_paths) + + return signed_msg + + def image_info(self) -> BinaryImage: + """Get Image info object. + + :return: Signed Message Info object. + """ + self.validate({}) + assert self.message + ret = BinaryImage( + name="Signed Message", + size=len(self), + offset=0, + binary=self.export(), + description=(f"Signed Message for {MessageCommands.get_label(self.message.TAG)}"), + ) + return ret + + @staticmethod + def get_validation_schemas() -> List[Dict[str, Any]]: + """Get list of validation schemas. + + :return: Validation list of schemas. + """ + sch = DatabaseManager().db.get_schema_file(DatabaseManager.SIGNED_MSG) + sch["properties"]["family"]["enum"] = AHABImage.get_supported_families() + return [sch] + + @staticmethod + def generate_config_template( + family: str, message: Optional[MessageCommands] = None + ) -> Dict[str, Any]: + """Generate AHAB configuration template. + + :param family: Family for which the template should be generated. + :param message: Generate the template just for one message type, if not used , its generated for all messages + :return: Dictionary of individual templates (key is name of template, value is template itself). + """ + val_schemas = SignedMessage.get_validation_schemas() + val_schemas[0]["properties"]["family"]["template_value"] = family + + if family not in AHABImage.get_supported_families(): + raise SPSDKValueError( + f"Unsupported value for family: {family} not in {AHABImage.get_supported_families()}" + ) + + if message: + for cmd_sch in val_schemas[0]["properties"]["message"]["properties"]["command"][ + "oneOf" + ]: + cmd_sch["skip_in_template"] = bool(message.label not in cmd_sch["properties"]) + + yaml_data = CommentedConfig( + f"Signed message Configuration template for {family}.", val_schemas + ).get_template() + + return {f"{family}_signed_msg": yaml_data} diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py new file mode 100644 index 00000000..8d12aeae --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""AHAB utils module.""" +import logging +from typing import Optional + +from ...apps.utils.utils import SPSDKError +from ...image.ahab.ahab_container import AHABContainerBase, AHABImage, Blob, SignatureBlock +from ...utils.database import DatabaseManager, get_db +from ...utils.misc import load_binary + +logger = logging.getLogger(__name__) + + +def ahab_update_keyblob( + family: str, + binary: str, + keyblob: str, + container_id: int, + mem_type: Optional[str], +) -> None: + """Update keyblob in AHAB image. + + :param family: MCU family + :param binary: Path to AHAB image binary + :param keyblob: Path to keyblob + :param container_id: Index of the container to be updated + :param mem_type: Memory type used for bootable image + :raises SPSDKError: In case the container id not present + :raises SPSDKError: In case the AHAB image does not contain blob + :raises SPSDKError: In case the length of keyblobs don't match + """ + DATA_READ = 0x2000 + offset = 0 + if mem_type: + database = get_db(family) + offset = database.get_dict( + DatabaseManager.BOOTABLE_IMAGE, ["mem_types", mem_type, "segments"] + )["ahab_container"] + + keyblob_data = load_binary(keyblob) + image = AHABImage(family) + + try: + address = image.ahab_address_map[container_id] + except IndexError as exc: + raise SPSDKError(f"No container ID {container_id}") from exc + + with open(binary, "r+b") as f: + logger.debug(f"Trying to find AHAB container header at offset {hex(address + offset)}") + f.seek(address + offset) + data = f.read(DATA_READ) + ( + _, + _, + _, + _, + signature_block_offset, + ) = AHABContainerBase._parse(data) + f.seek(signature_block_offset + address + offset) + signature_block = SignatureBlock.parse(f.read(DATA_READ)) + blob = Blob.parse(keyblob_data) + blob.validate() + signature_block.update_fields() + signature_block.validate({}) + if not signature_block.blob: + raise SPSDKError("AHAB Container must contain BLOB in order to update it") + if not len(signature_block.blob.export()) == len(blob.export()): + raise SPSDKError("The size of the BLOB must be same") + logger.debug(f"AHAB container found at offset {hex(address + offset)}") + logger.debug(f"New keyblob: \n{blob}") + logger.debug(f"Old keyblob: \n{signature_block.blob}") + f.seek(signature_block_offset + address + signature_block._blob_offset + offset) + f.write(blob.export()) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py new file mode 100644 index 00000000..1f6abc97 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2017-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Header.""" + +from struct import calcsize, pack, unpack_from +from typing import Optional, Union + +from typing_extensions import Self + +from spsdk.exceptions import SPSDKError, SPSDKParsingError +from spsdk.utils.abstract import BaseClass +from spsdk.utils.spsdk_enum import SpsdkEnum + +######################################################################################################################## +# Enums +######################################################################################################################## + + +class SegTag(SpsdkEnum): + """Segments Tag.""" + + XMCD = (0xC0, "XMCD", "External Memory Configuration Data") + DCD = (0xD2, "DCD", "Device Configuration Data") + CSF = (0xD4, "CSF", "Command Sequence File Data") + # i.MX6, i.MX7, i.MX8M + IVT2 = (0xD1, "IVT2", "Image Vector Table (Version 2)") + CRT = (0xD7, "CRT", "Certificate") + SIG = (0xD8, "SIG", "Signature") + EVT = (0xDB, "EVT", "Event") + RVT = (0xDD, "RVT", "ROM Vector Table") + WRP = (0x81, "WRP", "Wrapped Key") + MAC = (0xAC, "MAC", "Message Authentication Code") + # i.MX8QXP_A0, i.MX8QM_A0 + IVT3 = (0xDE, "IVT3", "Image Vector Table (Version 3)") + # i.MX8QXP_B0, i.MX8QM_B0 + BIC1 = (0x87, "BIC1", "Boot Images Container") + SIGB = (0x90, "SIGB", "Signature block") + + +class CmdTag(SpsdkEnum): + """CSF/DCD Command Tag.""" + + SET = (0xB1, "SET", "Set") + INS_KEY = (0xBE, "INS_KEY", "Install Key") + AUT_DAT = (0xCA, "AUT_DAT", "Authenticate Data") + WRT_DAT = (0xCC, "WRT_DAT", "Write Data") + CHK_DAT = (0xCF, "CHK_DAT", "Check Data") + NOP = (0xC0, "NOP", "No Operation (NOP)") + INIT = (0xB4, "INIT", "Initialize") + UNLK = (0xB2, "UNLK", "Unlock") + + +######################################################################################################################## +# Classes +######################################################################################################################## + + +class Header(BaseClass): + """Header element type.""" + + FORMAT = ">BHB" + SIZE = calcsize(FORMAT) + + @property + def size(self) -> int: + """Header size in bytes.""" + return self.SIZE + + def __init__(self, tag: int = 0, param: int = 0, length: Optional[int] = None) -> None: + """Constructor. + + :param tag: section tag + :param param: TODO + :param length: length of the segment or command; if not specified, size of the header is used + :raises SPSDKError: If invalid length + """ + self._tag = tag + self.param: int = param + self.length: int = self.SIZE if length is None else length + if self.SIZE > self.length or self.length >= 65536: + raise SPSDKError("Invalid length") + + @property + def tag(self) -> int: + """:return: section tag: command tag or segment tag, ...""" + return self._tag + + @property + def tag_name(self) -> str: + """Returns the header's tag name.""" + return SegTag.get_label(self.tag) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.tag_name}, {self.param}, {self.length})" + + def __str__(self) -> str: + return ( + f"{self.__class__.__name__} " + ) + + def export(self) -> bytes: + """Binary representation of the header.""" + return pack(self.FORMAT, self.tag, self.length, self.param) + + @classmethod + def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: + """Parse header. + + :param data: Raw data as bytes or bytearray + :param required_tag: Check header TAG if specified value or ignore if is None + :return: Header object + :raises SPSDKParsingError: if required header tag does not match + """ + tag, length, param = unpack_from(cls.FORMAT, data) + if required_tag is not None and tag != required_tag: + raise SPSDKParsingError( + f" Invalid header tag: '0x{tag:02X}' expected '0x{required_tag:02X}' " + ) + + return cls(tag, param, length) + + +class CmdHeader(Header): + """Command header.""" + + def __init__( + self, tag: Union[CmdTag, int], param: int = 0, length: Optional[int] = None + ) -> None: + """Constructor. + + :param tag: command tag + :param param: TODO + :param length: of the command binary section, in bytes + :raises SPSDKError: If invalid command tag + """ + tag = tag.tag if isinstance(tag, CmdTag) else tag + super().__init__(tag, param, length) + if tag not in CmdTag.tags(): + raise SPSDKError("Invalid command tag") + + @property + def tag(self) -> int: + """Command tag.""" + return self._tag + + @classmethod + def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: + """Create Header from binary data. + + :param data: binary data to convert into header + :param required_tag: CmdTag, None if not required + :return: parsed instance + :raises SPSDKParsingError: If required header tag does not match + :raises SPSDKError: If invalid tag + """ + if required_tag is not None: + if required_tag not in CmdTag.tags(): + raise SPSDKError("Invalid tag") + return super(CmdHeader, cls).parse(data, required_tag) + + +class Header2(Header): + """Header element type.""" + + FORMAT = " bytes: + """Binary representation of the header.""" + return pack(self.FORMAT, self.param, self.length, self.tag) + + @classmethod + def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: + """Parse header. + + :param data: Raw data as bytes or bytearray + :param required_tag: Check header TAG if specified value or ignore if is None + :raises SPSDKParsingError: Raises an error if required tag is empty or not valid + :return: Header2 object + """ + param, length, tag = unpack_from(cls.FORMAT, data) + if required_tag is not None and tag != required_tag: + raise SPSDKParsingError( + f" Invalid header tag: '0x{tag:02X}' expected '0x{required_tag:02X}' " + ) + + return cls(tag, param, length) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py new file mode 100644 index 00000000..b47aeedf --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2017-2018 Martin Olejar +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Misc.""" +import io +from io import SEEK_CUR +from typing import Optional, Union + +from spsdk.exceptions import SPSDKError +from spsdk.utils.registers import value_to_int + +from .header import Header + + +class RawDataException(SPSDKError): + """Raw data read failed.""" + + +class StreamReadFailed(RawDataException): + """Read_raw_data could not read stream.""" + + +class NotEnoughBytesException(RawDataException): + """Read_raw_data could not read enough data.""" + + +def hexdump_fmt(data: bytes, tab: int = 4, length: int = 16, sep: str = ":") -> str: + """Dump some potentially larger data in hex.""" + text = " " * tab + for i, j in enumerate(data): + text += f"{j:02x}{sep}" + if ((i + 1) % length) == 0: + text += "\n" + " " * tab + return text + + +def modulus_fmt(modulus: bytes, tab: int = 4, length: int = 15, sep: str = ":") -> str: + """Modulus format.""" + return hexdump_fmt(b"\0" + modulus, tab, length, sep) + + +def read_raw_data( + stream: Union[io.BufferedReader, io.BytesIO], + length: int, + index: Optional[int] = None, + no_seek: bool = False, +) -> bytes: + """Read raw data.""" + if index is not None: + if index < 0: + raise SPSDKError(f" Index must be non-negative, found {index}") + if index != stream.tell(): + stream.seek(index) + + if length < 0: + raise SPSDKError(f" Length must be non-negative, found {length}") + + try: + data = stream.read(length) + except Exception as exc: + raise StreamReadFailed(f" stream.read() failed, requested {length} bytes") from exc + + if len(data) != length: + raise NotEnoughBytesException( + f" Could not read enough bytes, expected {length}, found {len(data)}" + ) + + if no_seek: + stream.seek(-length, SEEK_CUR) + + return data + + +def read_raw_segment( + buffer: Union[io.BufferedReader, io.BytesIO], segment_tag: int, index: Optional[int] = None +) -> bytes: + """Read raw segment.""" + hrdata = read_raw_data(buffer, Header.SIZE, index) + length = Header.parse(hrdata, segment_tag).length - Header.SIZE + return hrdata + read_raw_data(buffer, length) + + +def dict_diff(main: dict, mod: dict) -> dict: + """Return a difference between two dictionaries if key is not present in main, it's skipped.""" + diff = {} + for key, value in mod.items(): + if isinstance(value, dict): + sub = dict_diff(main[key], value) + if sub: + diff[key] = sub + else: + if key not in main: + continue + main_value = main[key] if isinstance(main, dict) else main + try: + if value_to_int(main_value) != value_to_int(value): + diff[key] = value + except (SPSDKError, TypeError): + # Not a number! + if main_value != value: + diff[key] = value + return diff diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py new file mode 100644 index 00000000..4148d6ec --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py @@ -0,0 +1,932 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2017-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Commands and responses used by SDP module.""" +import math +from hashlib import sha256 +from struct import pack, unpack, unpack_from +from typing import Any, Iterator, List, Optional, Union + +from typing_extensions import Self + +from ..crypto.certificate import Certificate, ExtensionNotFound +from ..crypto.keys import EccCurve, PublicKeyEcc, PublicKeyRsa, get_ecc_curve +from ..crypto.types import SPSDKKeyUsage +from ..exceptions import SPSDKError +from ..utils.abstract import BaseClass +from ..utils.misc import Endianness +from ..utils.spsdk_enum import SpsdkEnum + +from .header import Header, SegTag +from .misc import hexdump_fmt, modulus_fmt + + +class EnumAlgorithm(SpsdkEnum): + """Algorithm types.""" + + ANY = (0x00, "ANY", "Algorithm type ANY") + HASH = (0x01, "HASH", "Hash algorithm type") + SIG = (0x02, "SIG", "Signature algorithm type") + F = (0x03, "F", "Finite field arithmetic") + EC = (0x04, "EC", "Elliptic curve arithmetic") + CIPHER = (0x05, "CIPHER", "Cipher algorithm type") + MODE = (0x06, "MODE", "Cipher/hash modes") + WRAP = (0x07, "WRAP", "Key wrap algorithm type") + # Hash algorithms + SHA1 = (0x11, "SHA1", "SHA-1 algorithm ID") + SHA256 = (0x17, "SHA256", "SHA-256 algorithm ID") + SHA512 = (0x1B, "SHA512", "SHA-512 algorithm ID") + # Signature algorithms + PKCS1 = (0x21, "PKCS1", "PKCS#1 RSA signature algorithm") + ECDSA = (0x27, "ECDSA", "NIST ECDSA signature algorithm") + # Cipher algorithms + AES = (0x55, "AES", "AES algorithm ID") + # Cipher or hash modes + CCM = (0x66, "CCM", "Counter with CBC-MAC") + # Key wrap algorithms + BLOB = (0x71, "BLOB", "SHW-specific key wrap") + + +class EnumSRK(SpsdkEnum): + """Entry type in the System Root Key Table.""" + + KEY_PUBLIC = (0xE1, "KEY_PUBLIC", "Public key type: data present") + KEY_HASH = (0xEE, "KEY_HASH", "Any key: hash only") + + +class BaseSecretClass(BaseClass): + """Base SPSDK class.""" + + def __init__(self, tag: SegTag, version: int = 0x40): + """Constructor. + + :param tag: section TAG + :param version: format version + """ + self._header = Header(tag=tag.tag, param=version) + + @property + def version(self) -> int: + """Format version.""" + return self._header.param + + @property + def version_major(self) -> int: + """Major format version.""" + return self.version >> 4 + + @property + def version_minor(self) -> int: + """Minor format version.""" + return self.version & 0xF + + @property + def size(self) -> int: + """Size of the exported binary data. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + +class SecretKeyBlob: + """Secret Key Blob.""" + + @property + def blob(self) -> bytes: + """Data of Secret Key Blob.""" + return self._data + + @blob.setter + def blob(self, value: Union[bytes, bytearray]) -> None: + assert isinstance(value, (bytes, bytearray)) + self._data = value + + @property + def size(self) -> int: + """Size of Secret Key Blob.""" + return len(self._data) + 4 + + def __init__(self, mode: int, algorithm: int, flag: int) -> None: + """Initialize Secret Key Blob.""" + self.mode = mode + self.algorithm = algorithm + self.flag = flag + self._data = bytearray() + + def __eq__(self, obj: Any) -> bool: + return isinstance(obj, SecretKeyBlob) and vars(obj) == vars(self) + + def __ne__(self, obj: Any) -> bool: + return not self.__eq__(obj) + + def __repr__(self) -> str: + return ( + f"SecKeyBlob " + ) + + def __str__(self) -> str: + """String representation of the Secret Key Blob.""" + msg = "-" * 60 + "\n" + msg += "SecKeyBlob\n" + msg += "-" * 60 + "\n" + msg += f"Mode: {self.mode}\n" + msg += f"Algorithm: {self.algorithm}\n" + msg += f"Flag: 0x{self.flag:02X}\n" + msg += f"Size: {len(self._data)} Bytes\n" + return msg + + def export(self) -> bytes: + """Export of Secret Key Blob.""" + raw_data = pack("4B", self.mode, self.algorithm, self.size, self.flag) + raw_data += bytes(self._data) + return raw_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse of Secret Key Blob.""" + (mode, alg, size, flg) = unpack_from("4B", data) + obj = cls(mode, alg, flg) + obj.blob = data[4 : 4 + size] + return obj + + +class CertificateImg(BaseSecretClass): + """Certificate structure for bootable image.""" + + @property + def size(self) -> int: + """Size of Certificate structure for bootable image.""" + return Header.SIZE + len(self._data) + + def __init__(self, version: int = 0x40, data: Optional[bytes] = None) -> None: + """Initialize the certificate structure for bootable image.""" + super().__init__(SegTag.CRT, version) + self._data = bytearray() if data is None else bytearray(data) + + def __len__(self) -> int: + return len(self._data) + + def __getitem__(self, key: int) -> int: + return self._data[key] + + def __setitem__(self, key: int, value: int) -> None: + self._data[key] = value + + def __iter__(self) -> Iterator[int]: + return self._data.__iter__() + + def __repr__(self) -> str: + return ( + f"Certificate " + ) + + def __str__(self) -> str: + """String representation of the CertificateImg.""" + msg = "-" * 60 + "\n" + msg += ( + f"Certificate (Ver: {self.version >> 4:X}.{self.version & 0xF:X}, " + f"Size: {len(self._data)})\n" + ) + msg += "-" * 60 + "\n" + return msg + + def export(self) -> bytes: + """Export.""" + self._header.length = self.size + raw_data = self._header.export() + raw_data += self._data + return raw_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse.""" + header = Header.parse(data, SegTag.CRT.tag) + return cls(header.param, data[Header.SIZE : header.length]) + + +class Signature(BaseSecretClass): + """Class representing a signature.""" + + @property + def size(self) -> int: + """Size of a signature.""" + return Header.SIZE + len(self._data) + + def __init__(self, version: int = 0x40, data: Optional[bytes] = None) -> None: + """Initialize the signature.""" + super().__init__(tag=SegTag.SIG, version=version) + self._data = bytearray() if data is None else bytearray(data) + + def __len__(self) -> int: + return len(self._data) + + def __getitem__(self, key: int) -> int: + return self._data[key] + + def __setitem__(self, key: int, value: int) -> None: + self._data[key] = value + + def __iter__(self) -> Iterator[int]: + return self._data.__iter__() + + def __repr__(self) -> str: + return f"Signature > 4}.{self.version & 0xF}, Size: {len(self._data)}>" + + def __str__(self) -> str: + """String representation of the signature.""" + msg = "-" * 60 + "\n" + msg += f"Signature (Ver: {self.version >> 4:X}.{self.version & 0xF:X}, Size: {len(self._data)})\n" + msg += "-" * 60 + "\n" + return msg + + @property + def data(self) -> bytes: + """Signature data.""" + return bytes(self._data) + + @data.setter + def data(self, value: Union[bytes, bytearray]) -> None: + """Signature data.""" + self._data = bytearray(value) + + def export(self) -> bytes: + """Export.""" + self._header.length = self.size + raw_data = self._header.export() + raw_data += self.data + return raw_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse.""" + header = Header.parse(data, SegTag.SIG.tag) + return cls(header.param, data[Header.SIZE : header.length]) + + +class MAC(BaseSecretClass): + """Structure that holds initial parameter for AES encryption/decryption. + + - nonce - initialization vector for AEAD AES128 decryption + - mac - message authentication code to verify the decryption was successful + """ + + # AES block size in bytes; This also match size of the MAC and + AES128_BLK_LEN = 16 + + def __init__( + self, + version: int = 0x40, + nonce_len: int = 0, + mac_len: int = AES128_BLK_LEN, + data: Optional[bytes] = None, + ): + """Constructor. + + :param version: format version, should be 0x4x + :param nonce_len: number of NONCE bytes + :param mac_len: number of MAC bytes + :param data: nonce and mac bytes joined together + """ + super().__init__(tag=SegTag.MAC, version=version) + self.nonce_len = nonce_len + self.mac_len = mac_len + self._data: bytes = bytes() if data is None else bytes(data) + if data: + self._validate_data() + + @property + def size(self) -> int: + """Size of binary representation in bytes.""" + return Header.SIZE + 4 + self.nonce_len + self.mac_len + + def _validate_data(self) -> None: + """Validates the data. + + :raises SPSDKError: If data length does not match with parameters + """ + if len(self.data) != self.nonce_len + self.mac_len: + raise SPSDKError( + f"length of data ({len(self.data)}) does not match with " + f"nonce_bytes({self.nonce_len})+mac_bytes({self.mac_len})" + ) + + @property + def data(self) -> bytes: + """NONCE and MAC bytes joined together.""" + return self._data + + @data.setter + def data(self, value: bytes) -> None: + """Setter. + + :param value: NONCE and MAC bytes joined together + """ + self._data = value + self._validate_data() + + @property + def nonce(self) -> bytes: + """NONCE bytes for the encryption/decryption.""" + self._validate_data() + return self._data[0 : self.nonce_len] + + @property + def mac(self) -> bytes: + """MAC bytes for the encryption/decryption.""" + self._validate_data() + return self._data[self.nonce_len : self.nonce_len + self.mac_len] + + def update_aead_encryption_params(self, nonce: bytes, mac: bytes) -> None: + """Update AEAD encryption parameters for encrypted image. + + :param nonce: initialization vector, length depends on image size, + :param mac: message authentication code used to authenticate decrypted data, 16 bytes + :raises SPSDKError: If incorrect length of mac + :raises SPSDKError: If incorrect length of nonce + :raises SPSDKError: If incorrect number of MAC bytes" + """ + if len(mac) != MAC.AES128_BLK_LEN: + raise SPSDKError("Incorrect length of mac") + if len(nonce) < 11 or len(nonce) > 13: + raise SPSDKError("Incorrect length of nonce") + self.nonce_len = len(nonce) + if self.mac_len != MAC.AES128_BLK_LEN: + raise SPSDKError("Incorrect number of MAC bytes") + self.data = nonce + mac + + def __len__(self) -> int: + return len(self._data) + + def __repr__(self) -> str: + return ( + f"MAC " + ) + + def __str__(self) -> str: + """Text info about the instance.""" + msg = "-" * 60 + "\n" + msg += f"MAC (Version: {self.version >> 4:X}.{self.version & 0xF:X})\n" + msg += "-" * 60 + "\n" + msg += f"Nonce Len: {self.nonce_len} Bytes\n" + msg += f"MAC Len: {self.mac_len} Bytes\n" + msg += f"[{self._data.hex()}]\n" + return msg + + def export(self) -> bytes: + """Export instance into binary form (serialization). + + :return: binary form + """ + self._validate_data() + self._header.length = self.size + raw_data = self._header.export() + raw_data += pack(">4B", 0, self.nonce_len, 0, self.mac_len) + raw_data += self.data + return raw_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse binary data and creates the instance (deserialization). + + :param data: being parsed + :return: the instance + """ + header = Header.parse(data, SegTag.MAC.tag) + (_, nonce_bytes, _, mac_bytes) = unpack_from(">4B", data, Header.SIZE) + return cls( + header.param, + nonce_bytes, + mac_bytes, + data[Header.SIZE + 4 : header.length], + ) + + +class SRKException(SPSDKError): + """SRK table processing exceptions.""" + + +class NotImplementedSRKPublicKeyType(SRKException): + """This SRK public key algorithm is not yet implemented.""" + + +class NotImplementedSRKCertificate(SRKException): + """This SRK public key algorithm is not yet implemented.""" + + +class NotImplementedSRKItem(SRKException): + """This type of SRK table item is not implemented.""" + + +class SrkItem: + """Base class for items in the SRK Table, see `SrkTable` class. + + We do not inherit from BaseClass because our header parameter + is an algorithm identifier, not a version number. + """ + + def __eq__(self, other: Any) -> bool: + return isinstance(other, self.__class__) and vars(other) == vars(self) + + def __ne__(self, obj: Any) -> bool: + return not self.__eq__(obj) + + @property + def size(self) -> int: + """Size of the exported binary data. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def __str__(self) -> str: + """Description about the instance. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def sha256(self) -> bytes: + """Export SHA256 hash of the original data. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def hashed_entry(self) -> "SrkItem": + """This SRK item should be replaced with an incomplete entry with its digest. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def export(self) -> bytes: + """Serialization to binary form. + + :return: binary representation of the instance + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + @classmethod + def parse(cls, data: bytes) -> Self: + """Pick up the right implementation of an SRK item. + + :param data: The bytes array of SRK segment + :return: SrkItem: One of the SrkItem subclasses + :raises NotImplementedSRKPublicKeyType: Unsupported key algorithm + :raises NotImplementedSRKItem: Unsupported tag + """ + header = Header.parse(data) + if header.tag == EnumSRK.KEY_PUBLIC: + if header.param == EnumAlgorithm.PKCS1: + return SrkItemRSA.parse(data) # type: ignore + elif header.param == EnumAlgorithm.ECDSA: + return SrkItemEcc.parse(data) # type: ignore + raise NotImplementedSRKPublicKeyType(f"{header.param}") + if header.tag == EnumSRK.KEY_HASH: + return SrkItemHash.parse(data) # type: ignore + raise NotImplementedSRKItem(f"TAG = {header.tag}, PARAM = {header.param}") + + @classmethod + def from_certificate(cls, cert: Certificate) -> "SrkItem": + """Pick up the right implementation of an SRK item.""" + assert isinstance(cert, Certificate) + try: + return SrkItemRSA.from_certificate(cert) + except SPSDKError: + pass + try: + return SrkItemEcc.from_certificate(cert) + except SPSDKError: + pass + raise NotImplementedSRKCertificate() + + +class SrkItemHash(SrkItem): + """Hashed stub of some public key. + + This is a valid entry of the SRK table, it represents + some public key of unknown algorithm. + Can only provide its hashed value of itself. + """ + + @property + def algorithm(self) -> int: + """Hashing algorithm used.""" + return self._header.param + + @property + def size(self) -> int: + """Size of an SRK item.""" + return self._header.length + + def __init__(self, algorithm: int, digest: bytes) -> None: + """Build the stub entry with public key hash only. + + :param algorithm: int: Hash algorithm, only SHA256 now + :param digest: bytes: Hash digest value + :raises SPSDKError: If incorrect algorithm + """ + if algorithm != EnumAlgorithm.SHA256: + raise SPSDKError("Incorrect algorithm") + self._header = Header(tag=EnumSRK.KEY_HASH.tag, param=algorithm) + self.digest = digest + self._header.length += len(digest) + + def __repr__(self) -> str: + return f"SRK Hash " + + def __str__(self) -> str: + """String representation of SrkItemHash.""" + msg = str() + msg += f"Hash algorithm: {EnumAlgorithm.from_tag(self._header.param)}\n" + msg += "Hash value:\n" + msg += hexdump_fmt(self.digest) + return msg + + def sha256(self) -> bytes: + """Export SHA256 hash of the original data.""" + return self.digest + + def hashed_entry(self) -> "SrkItemHash": + """This SRK item should be replaced with an incomplete entry with its digest.""" + return self + + def export(self) -> bytes: + """Export.""" + data = self._header.export() + data += self.digest + return data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse SRK table item data. + + :param data: The bytes array of SRK segment + :return: SrkItemHash: SrkItemHash object + :raises NotImplementedSRKItem: Unknown tag + """ + header = Header.parse(data, EnumSRK.KEY_HASH.tag) + rest = data[header.SIZE :] + if header.param == EnumAlgorithm.SHA256: + digest = rest[: sha256().digest_size] + return cls(EnumAlgorithm.SHA256.tag, digest) + raise NotImplementedSRKItem(f"TAG = {header.tag}, PARAM = {header.param}") + + +class SrkItemRSA(SrkItem): + """RSA public key in SRK Table, see `SrkTable` class.""" + + @property + def algorithm(self) -> int: + """Algorithm.""" + return self._header.param + + @property + def size(self) -> int: + """Size of an SRK item.""" + return self._header.length + + @property + def flag(self) -> int: + """Flag.""" + return self._flag + + @flag.setter + def flag(self, value: int) -> None: + if value not in (0, 0x80): + raise SPSDKError("Incorrect flag") + self._flag = value + + @property + def key_length(self) -> int: + """Key length of Item in SRK Table.""" + return len(self.modulus) * 8 + + def __init__(self, modulus: bytes, exponent: bytes, flag: int = 0) -> None: + """Initialize the srk table item.""" + assert isinstance(modulus, bytes) + assert isinstance(exponent, bytes) + self._header = Header(tag=EnumSRK.KEY_PUBLIC.tag, param=EnumAlgorithm.PKCS1.tag) + self.flag = flag + self.modulus = modulus + self.exponent = exponent + self._header.length += 8 + len(self.modulus) + len(self.exponent) + + def __repr__(self) -> str: + return ( + f"SRK " + ) + + def __str__(self) -> str: + """String representation of SrkItemRSA.""" + exp = int.from_bytes(self.exponent, Endianness.BIG.value) + return ( + f"Algorithm: {EnumAlgorithm.from_tag(self.algorithm)}\n" + f"Flag: 0x{self.flag:02X} {'(CA)' if self.flag == 0x80 else ''}\n" + f"Length: {self.key_length} bit\n" + "Modulus:\n" + f"{modulus_fmt(self.modulus)}\n" + f"Exponent: {exp} (0x{exp:X})\n" + ) + + def sha256(self) -> bytes: + """Export SHA256 hash of the data.""" + srk_data = self.export() + return sha256(srk_data).digest() + + def hashed_entry(self) -> "SrkItemHash": + """This SRK item should be replaced with an incomplete entry with its digest.""" + return SrkItemHash(EnumAlgorithm.SHA256.tag, self.sha256()) + + def export(self) -> bytes: + """Export.""" + data = self._header.export() + data += pack(">4B2H", 0, 0, 0, self.flag, len(self.modulus), len(self.exponent)) + data += bytes(self.modulus) + data += bytes(self.exponent) + return data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse SRK table item data. + + :param data: The bytes array of SRK segment + :return: SrkItemRSA: SrkItemRSA object + """ + Header.parse(data, EnumSRK.KEY_PUBLIC.tag) + (flag, modulus_len, exponent_len) = unpack_from(">B2H", data, Header.SIZE + 3) + offset = 5 + Header.SIZE + 3 + modulus = data[offset : offset + modulus_len] + offset += modulus_len + exponent = data[offset : offset + exponent_len] + return cls(modulus, exponent, flag) + + @classmethod + def from_certificate(cls, cert: Certificate) -> "SrkItemRSA": + """Create SRKItemRSA from certificate.""" + assert isinstance(cert, Certificate) + flag = 0 + try: + key_usage = cert.extensions.get_extension_for_class(SPSDKKeyUsage) + assert isinstance(key_usage.value, SPSDKKeyUsage) + if key_usage.value.key_cert_sign: + flag = 0x80 + except ExtensionNotFound: + pass + try: + public_key = cert.get_public_key() + if not isinstance(public_key, PublicKeyRsa): + raise SPSDKError("Not an RSA key") + # get modulus and exponent of public key since we are RSA + modulus_len = math.ceil(public_key.n.bit_length() / 8) + exponent_len = math.ceil(public_key.e.bit_length() / 8) + modulus = public_key.n.to_bytes(modulus_len, Endianness.BIG.value) + exponent = public_key.e.to_bytes(exponent_len, Endianness.BIG.value) + + return cls(modulus, exponent, flag) + except SPSDKError as exc: + raise NotImplementedSRKCertificate() from exc + + +class SrkItemEcc(SrkItem): + """ECC public key in SRK Table, see `SrkTable` class.""" + + ECC_KEY_TYPE = { + EccCurve.SECP256R1: 0x4B, + EccCurve.SECP384R1: 0x4D, + EccCurve.SECP521R1: 0x4E, + } + + @property + def algorithm(self) -> int: + """Algorithm.""" + return self._header.param + + @property + def size(self) -> int: + """Size of an SRK item.""" + return self._header.length + + @property + def flag(self) -> int: + """Flag.""" + return self._flag + + @flag.setter + def flag(self, value: int) -> None: + # Check + if value not in (0, 0x80): + raise SPSDKError("Incorrect flag") + self._flag = value + + def __init__(self, key_size: int, x_coordinate: int, y_coordinate: int, flag: int = 0) -> None: + """Initialize the srk table item.""" + self._header = Header(tag=EnumSRK.KEY_PUBLIC.tag, param=EnumAlgorithm.ECDSA.tag) + self.x_coordinate = x_coordinate + self.y_coordinate = y_coordinate + self.key_size = key_size + self.coordinate_size = math.ceil(key_size / 8) + self.flag = flag + self._header.length += ( + 8 + + len(self.x_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value)) + + len(self.y_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value)) + ) + + def __repr__(self) -> str: + return ( + f"SRK " + ) + + def __str__(self) -> str: + """String representation of SrkItemEcc.""" + return ( + f"Algorithm: {EnumAlgorithm.from_tag(self.algorithm)}\n" + f"Flag: 0x{self.flag:02X} {'(CA)' if self.flag == 0x80 else ''}\n" + f"Key size: {self.key_size} bit\n" + f"X coordinate: {self.x_coordinate}\n" + f"Y coordinate: {self.y_coordinate}\n" + ) + + def sha256(self) -> bytes: + """Export SHA256 hash of the data.""" + srk_data = self.export() + return sha256(srk_data).digest() + + def hashed_entry(self) -> "SrkItemHash": + """This SRK item should be replaced with an incomplete entry with its digest.""" + return SrkItemHash(EnumAlgorithm.SHA256.tag, self.sha256()) + + def export(self) -> bytes: + """Export.""" + data = self._header.export() + curve_id = self.ECC_KEY_TYPE[get_ecc_curve(self.key_size // 8)] + data += pack( + ">8B", 0, 0, 0, self.flag, curve_id, 0, self.key_size >> 8 & 0xFF, self.key_size & 0xFF + ) + data += self.x_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value) + data += self.y_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value) + return data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse SRK table item data. + + :param data: The bytes array of SRK segment + :return: SrkItemEcc: SrkItemEcc object + """ + Header.parse(data, EnumSRK.KEY_PUBLIC.tag) + (flag, curve_id, _, key_size) = unpack_from(">3BH", data, Header.SIZE + 3) + if curve_id not in list(cls.ECC_KEY_TYPE.values()): + raise SPSDKError(f"Unknown curve with id {curve_id}") + offset = 5 + Header.SIZE + 3 + coordinate_size = math.ceil(key_size / 8) + x_coordinate = data[offset : offset + coordinate_size] + offset += coordinate_size + y_coordinate = data[offset : offset + coordinate_size] + return cls( + key_size, + int.from_bytes(x_coordinate, Endianness.BIG.value), + int.from_bytes(y_coordinate, Endianness.BIG.value), + flag, + ) + + @classmethod + def from_certificate(cls, cert: Certificate) -> "SrkItemEcc": + """Create SrkItemEcc from certificate.""" + flag = 0 + try: + key_usage = cert.extensions.get_extension_for_class(SPSDKKeyUsage) + assert isinstance(key_usage.value, SPSDKKeyUsage) + if key_usage.value.key_cert_sign: + flag = 0x80 + except ExtensionNotFound: + pass + + try: + public_key = cert.get_public_key() + if not isinstance(public_key, PublicKeyEcc): + raise SPSDKError("Not an ECC key") + return cls(public_key.key_size, public_key.x, public_key.y, flag) + except SPSDKError as exc: + raise NotImplementedSRKCertificate() from exc + + +class SrkTable(BaseSecretClass): + """SRK table.""" + + @property + def size(self) -> int: + """Size of SRK table.""" + size = Header.SIZE + for key in self._keys: + size += key.size + return size + + def __init__(self, version: int = 0x40) -> None: + """Initialize SRT Table. + + :param version: format version + """ + super().__init__(tag=SegTag.CRT, version=version) + self._keys: List[SrkItem] = [] + + def __len__(self) -> int: + return len(self._keys) + + def __getitem__(self, key: int) -> SrkItem: + return self._keys[key] + + def __setitem__(self, key: int, value: SrkItem) -> None: + assert isinstance(value, SrkItem) + self._keys[key] = value + + def __iter__(self) -> Iterator[SrkItem]: + return self._keys.__iter__() + + def __repr__(self) -> str: + return ( + f"SRK_Table " + ) + + def __str__(self) -> str: + """Text info about the instance.""" + msg = "-" * 60 + "\n" + msg += ( + f"SRK Table (Version: {self.version_major:X}.{self.version_minor:X}, " + f"#Keys: {len(self._keys)})\n" + ) + msg += "-" * 60 + "\n" + for i, srk in enumerate(self._keys): + msg += f"SRK Key Index: {i} \n" + msg += str(srk) + msg += "\n" + return msg + + def append(self, srk: SrkItem) -> None: + """Add SRK item. + + :param srk: item to be added + """ + self._keys.append(srk) + + def get_fuse(self, index: int) -> int: + """Retrieve fuse value for the given index. + + :param index: of the fuse, 0-7 + :return: value of the specified fuse; the value is in format, that cane be used as parameter for SDP + `efuse_read_once` or `efuse_write_once` + :raises SPSDKError: If incorrect index of the fuse + :raises SPSDKError: If incorrect length of SRK items + """ + if index < 0 or index >= 8: + raise SPSDKError("Incorrect index of the fuse") + int_data = self.export_fuses()[index * 4 : (1 + index) * 4] + if len(int_data) != 4: + raise SPSDKError("Incorrect length of SRK items") + return unpack(" bytes: + """SRK items in binary form, see `SRK_fuses.bin` file.""" + data = b"" + for srk in self._keys: + data += srk.sha256() + return sha256(data).digest() + + def export(self) -> bytes: + """Export into binary form (serialization). + + :return: binary representation of the instance + """ + self._header.length = self.size + raw_data = self._header.export() + for srk in self._keys: + raw_data += srk.export() + return raw_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse of SRK table.""" + header = Header.parse(data, SegTag.CRT.tag) + offset = Header.SIZE + obj = cls(header.param) + obj._header.length = header.length # pylint: disable=protected-access + length = header.length - Header.SIZE + while length > 0: + srk = SrkItem.parse(data[offset:]) + offset += srk.size + length -= srk.size + obj.append(srk) + return obj diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py new file mode 100644 index 00000000..f6af88e5 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing communication with the MCU Bootloader.""" + +from typing import Union + +from .interfaces.buspal import MbootBuspalI2CInterface, MbootBuspalSPIInterface +from .interfaces.sdio import MbootSdioInterface +from .interfaces.uart import MbootUARTInterface +from .interfaces.usb import MbootUSBInterface +from .interfaces.usbsio import MbootUsbSioI2CInterface, MbootUsbSioSPIInterface +from .mcuboot import McuBoot + +MbootDeviceTypes = Union[ + MbootBuspalI2CInterface, + MbootBuspalSPIInterface, + MbootSdioInterface, + MbootUARTInterface, + MbootUSBInterface, + MbootUsbSioI2CInterface, + MbootUsbSioSPIInterface, +] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py new file mode 100644 index 00000000..9f9dc4d5 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Commands and responses used by MBOOT module.""" + +from struct import pack, unpack, unpack_from +from typing import Dict, List, Optional, Type + +from spsdk.utils.interfaces.commands import CmdPacketBase, CmdResponseBase +from spsdk.utils.spsdk_enum import SpsdkEnum + +from .error_codes import StatusCode +from .exceptions import McuBootError + +######################################################################################################################## +# McuBoot Commands and Responses Tags +######################################################################################################################## + +# fmt: off +class CommandTag(SpsdkEnum): + """McuBoot Commands.""" + + NO_COMMAND = (0x00, "NoCommand", "No Command") + FLASH_ERASE_ALL = (0x01, "FlashEraseAll", "Erase Complete Flash") + FLASH_ERASE_REGION = (0x02, "FlashEraseRegion", "Erase Flash Region") + READ_MEMORY = (0x03, "ReadMemory", "Read Memory") + WRITE_MEMORY = (0x04, "WriteMemory", "Write Memory") + FILL_MEMORY = (0x05, "FillMemory", "Fill Memory") + FLASH_SECURITY_DISABLE = (0x06, "FlashSecurityDisable", "Disable Flash Security") + GET_PROPERTY = (0x07, "GetProperty", "Get Property") + RECEIVE_SB_FILE = (0x08, "ReceiveSBFile", "Receive SB File") + EXECUTE = (0x09, "Execute", "Execute") + CALL = (0x0A, "Call", "Call") + RESET = (0x0B, "Reset", "Reset MCU") + SET_PROPERTY = (0x0C, "SetProperty", "Set Property") + FLASH_ERASE_ALL_UNSECURE = (0x0D, "FlashEraseAllUnsecure", "Erase Complete Flash and Unlock") + FLASH_PROGRAM_ONCE = (0x0E, "FlashProgramOnce", "Flash Program Once") + FLASH_READ_ONCE = (0x0F, "FlashReadOnce", "Flash Read Once") + FLASH_READ_RESOURCE = (0x10, "FlashReadResource", "Flash Read Resource") + CONFIGURE_MEMORY = (0x11, "ConfigureMemory", "Configure Quad-SPI Memory") + RELIABLE_UPDATE = (0x12, "ReliableUpdate", "Reliable Update") + GENERATE_KEY_BLOB = (0x13, "GenerateKeyBlob", "Generate Key Blob") + FUSE_PROGRAM = (0x14, "ProgramFuse", "Program Fuse") + KEY_PROVISIONING = (0x15, "KeyProvisioning", "Key Provisioning") + TRUST_PROVISIONING = (0x16, "TrustProvisioning", "Trust Provisioning") + FUSE_READ = (0x17, "ReadFuse", "Read Fuse") + UPDATE_LIFE_CYCLE = (0x18, "UpdateLifeCycle", "Update Life Cycle") + ELE_MESSAGE = (0x19, "EleMessage", "Send EdgeLock Enclave Message") + + # reserved commands + CONFIGURE_I2C = (0xC1, "ConfigureI2c", "Configure I2C") + CONFIGURE_SPI = (0xC2, "ConfigureSpi", "Configure SPI") + CONFIGURE_CAN = (0xC3, "ConfigureCan", "Configure CAN") + + + +class CommandFlag(SpsdkEnum): + """Flags for McuBoot commands.""" + + NONE = (0, "NoFlags", "No flags specified") + HAS_DATA_PHASE = (1, "DataPhase", "Command has a data phase") + + + +class ResponseTag(SpsdkEnum): + """McuBoot Responses to Commands.""" + + GENERIC = (0xA0, "GenericResponse", "Generic Response") + READ_MEMORY = (0xA3, "ReadMemoryResponse", "Read Memory Response") + GET_PROPERTY = (0xA7, "GetPropertyResponse", "Get Property Response") + FLASH_READ_ONCE = (0xAF, "FlashReadOnceResponse", "Flash Read Once Response") + FLASH_READ_RESOURCE = (0xB0, "FlashReadResourceResponse", "Flash Read Resource Response") + KEY_BLOB_RESPONSE = (0xB3, "CreateKeyBlobResponse", "Create Key Blob") + KEY_PROVISIONING_RESPONSE = (0xB5, "KeyProvisioningResponse", "Key Provisioning Response") + TRUST_PROVISIONING_RESPONSE = (0xB6, "TrustProvisioningResponse", "Trust Provisioning Response") + + +class KeyProvOperation(SpsdkEnum): + """Type of key provisioning operation.""" + + ENROLL = (0, "Enroll", "Enroll Operation") + SET_USER_KEY = (1, "SetUserKey", "Set User Key Operation") + SET_INTRINSIC_KEY = (2, "SetIntrinsicKey", "Set Intrinsic Key Operation") + WRITE_NON_VOLATILE = (3, "WriteNonVolatile", "Write Non Volatile Operation") + READ_NON_VOLATILE = (4, "ReadNonVolatile", "Read Non Volatile Operation") + WRITE_KEY_STORE = (5, "WriteKeyStore", "Write Key Store Operation") + READ_KEY_STORE = (6, "ReadKeyStore", "Read Key Store Operation") + + +class KeyProvUserKeyType(SpsdkEnum): + """Enumeration of supported user keys in PUF. Keys are SoC specific, not all will be supported for the processor.""" + + OTFADKEK = (2, "OTFADKEK", "Key for OTFAD encryption") + SBKEK = (3, "SBKEK", "Key for SB file encryption") + PRINCE_REGION_0 = (7, "PRINCE0", "Key for Prince region 0") + PRINCE_REGION_1 = (8, "PRINCE1", "Key for Prince region 1") + PRINCE_REGION_2 = (9, "PRINCE2", "Key for Prince region 2") + PRINCE_REGION_3 = (10, "PRINCE3", "Key for Prince region 3") + + USERKEK = (11, "USERKEK", "Encrypted boot image key") + UDS = (12, "UDS", "Universal Device Secret for DICE") + + +class GenerateKeyBlobSelect(SpsdkEnum): + """Key selector for the generate-key-blob function. + + For devices with SNVS, valid options of [key_sel] are + 0, 1 or OTPMK: OTPMK from FUSE or OTP(default), + 2 or ZMK: ZMK from SNVS, + 3 or CMK: CMK from SNVS, + For devices without SNVS, this option will be ignored. + """ + + OPTMK = (0, "OPTMK", "OTPMK from FUSE or OTP(default)") + ZMK = (2, "ZMK", "ZMK from SNVS") + CMK = (3, "CMK", "CMK from SNVS") + + +class TrustProvOperation(SpsdkEnum): + """Operations supported by Trust Provisioning flow.""" + + PROVE_GENUINITY = (0xF4, "ProveGenuinity", "Start the proving genuinity process") + ISP_SET_WRAPPED_DATA = (0xF0, "SetWrappedData", "Start processing Wrapped data") + """Type of trust provisioning operation.""" + + OEM_GEN_MASTER_SHARE = (0, "OemGenMasterShare", "Enroll Operation") + OEM_SET_MASTER_SHARE = (1, "SetUserKey", "Set User Key Operation") + OEM_GET_CUST_CERT_DICE_PUK = (2, "SetIntrinsicKey", "Set Intrinsic Key Operation") + HSM_GEN_KEY = (3, "HsmGenKey", "HSM gen key") + HSM_STORE_KEY = (4, "HsmStoreKey", "HSM store key") + HSM_ENC_BLOCK = (5, "HsmEncBlock", "HSM Enc block") + HSM_ENC_SIGN = (6, "HsnEncSign", "HSM enc sign") + + +class TrustProvOemKeyType(SpsdkEnum): + """Type of oem key type definition.""" + + MFWISK = (0xC3A5, "MFWISK", "ECDSA Manufacturing Firmware Signing Key") + MFWENCK = (0xA5C3, "MFWENCK", "CKDF Master Key for Manufacturing Firmware Encryption Key") + GENSIGNK = (0x5A3C, "GENSIGNK", "Generic ECDSA Signing Key") + GETCUSTMKSK = (0x3C5A, "GETCUSTMKSK", "CKDF Master Key for Production Firmware Encryption Key") + + +class TrustProvKeyType(SpsdkEnum): + """Type of key type definition.""" + + CKDFK = (1, "CKDFK", "CKDF Master Key") + HKDFK = (2, "HKDFK", "HKDF Master Key") + HMACK = (3, "HMACK", "HMAC Key") + CMACK = (4, "CMACK", "CMAC Key") + AESK = (5, "AESK", "AES Key") + KUOK = (6, "KUOK", "Key Unwrap Only Key") + + +class TrustProvWrappingKeyType(SpsdkEnum): + """Type of wrapping key type definition.""" + + INT_SK = (0x10, "INT_SK", "The wrapping key for wrapping of MFG_CUST_MK_SK0_BLOB") + EXT_SK = (0x11, "EXT_SK", "The wrapping key for wrapping of MFG_CUST_MK_SK0_BLOB") + +class TrustProvWpc(SpsdkEnum): + """Type of WPC trusted facility commands for DSC.""" + + WPC_GET_ID = (0x5000000, "wpc_get_id", "WPC get ID") + NXP_GET_ID = (0x5000001, "nxp_get_id", "NXP get ID") + WPC_INSERT_CERT = (0x5000002, "wpc_insert_cert", "WPC insert certificate") + WPC_SIGN_CSR = (0x5000003, "wpc_sign_csr", "WPC sign CSR") + +class TrustProvDevHsmDsc(SpsdkEnum): + """Type of DSC Device HSM.""" + + DSC_HSM_CREATE_SESSION = (0x6000000, "dsc_hsm_create_session", "DSC HSM create session") + DSC_HSM_ENC_BLK = (0x6000001, "dsc_hsm_enc_blk", "DSC HSM encrypt bulk") + DSC_HSM_ENC_SIGN = (0x6000002, "dsc_hsm_enc_sign", "DSC HSM sign") + +# fmt: on + +######################################################################################################################## +# McuBoot Command and Response packet classes +######################################################################################################################## + + +class CmdHeader: + """McuBoot command/response header.""" + + SIZE = 4 + + def __init__(self, tag: int, flags: int, reserved: int, params_count: int) -> None: + """Initialize the Command Header. + + :param tag: Tag indicating the command, see: `CommandTag` class + :param flags: Flags for the command + :param reserved: Reserved? + :param params_count: Number of parameter for the command + """ + self.tag = tag + self.flags = flags + self.reserved = reserved + self.params_count = params_count + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdHeader) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return ( + f"CmdHeader(tag=0x{self.tag:02X}, flags=0x{self.flags:02X}, " + f"reserved={self.reserved}, params_count={self.params_count})" + ) + + def to_bytes(self) -> bytes: + """Serialize header into bytes.""" + return pack("4B", self.tag, self.flags, self.reserved, self.params_count) + + @classmethod + def from_bytes(cls, data: bytes, offset: int = 0) -> "CmdHeader": + """Deserialize header from bytes. + + :param data: Input data in bytes + :param offset: The offset of input data + :return: De-serialized CmdHeader object + :raises McuBootError: Invalid data format + """ + if len(data) < 4: + raise McuBootError(f"Invalid format of RX packet (data length is {len(data)} bytes)") + return cls(*unpack_from("4B", data, offset)) + + +class CmdPacket(CmdPacketBase): + """McuBoot command packet format class.""" + + SIZE = 32 + EMPTY_VALUE = 0x00 + + def __init__( + self, tag: CommandTag, flags: int, *args: int, data: Optional[bytes] = None + ) -> None: + """Initialize the Command Packet object. + + :param tag: Tag identifying the command + :param flags: Flags used by the command + :param args: Arguments used by the command + :param data: Additional data, defaults to None + """ + self.header = CmdHeader(tag.tag, flags, 0, len(args)) + self.params = list(args) + if data is not None: + if len(data) % 4: + data += b"\0" * (4 - len(data) % 4) + self.params.extend(unpack_from(f"<{len(data) // 4}I", data)) + self.header.params_count = len(self.params) + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdPacket) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __str__(self) -> str: + """Get object info.""" + tag = ( + CommandTag.get_label(self.header.tag) + if self.header.tag in CommandTag.tags() + else f"0x{self.header.tag:02X}" + ) + return f"Tag={tag}, Flags=0x{self.header.flags:02X}" + "".join( + f", P[{n}]=0x{param:08X}" for n, param in enumerate(self.params) + ) + + def to_bytes(self, padding: bool = True) -> bytes: + """Serialize CmdPacket into bytes. + + :param padding: If True, add padding to specific size + :return: Serialized object into bytes + """ + self.header.params_count = len(self.params) + data = self.header.to_bytes() + data += pack(f"<{self.header.params_count}I", *self.params) + if padding and len(data) < self.SIZE: + data += bytes([self.EMPTY_VALUE] * (self.SIZE - len(data))) + return data + + +class CmdResponse(CmdResponseBase): + """McuBoot response base format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Command Response object. + + :param header: Header for the response + :param raw_data: Response data + """ + assert isinstance(header, CmdHeader) + assert isinstance(raw_data, (bytes, bytearray)) + self.header = header + self.raw_data = raw_data + (status,) = unpack_from(" int: + """Return a integer representation of the response.""" + return unpack_from(">I", self.raw_data)[0] + + def _get_status_label(self) -> str: + return ( + StatusCode.get_label(self.status) + if self.status in StatusCode.tags() + else f"Unknown[0x{self.status:08X}]" + ) + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdResponse) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __str__(self) -> str: + """Get object info.""" + return ( + f"Tag=0x{self.header.tag:02X}, Flags=0x{self.header.flags:02X}" + + " [" + + ", ".join(f"{b:02X}" for b in self.raw_data) + + "]" + ) + + +class GenericResponse(CmdResponse): + """McuBoot generic response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Generic response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, tag = unpack_from("<2I", raw_data) + self.cmd_tag: int = tag + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + cmd = ( + CommandTag.get_label(self.cmd_tag) + if self.cmd_tag in CommandTag.tags() + else f"Unknown[0x{self.cmd_tag:02X}]" + ) + return f"Tag={tag}, Status={status}, Cmd={cmd}" + + +class GetPropertyResponse(CmdResponse): + """McuBoot get property response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Get-Property response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, *values = unpack_from(f"<{self.header.params_count}I", raw_data) + self.values: List[int] = list(values) + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}" + "".join( + f", v{n}=0x{value:08X}" for n, value in enumerate(self.values) + ) + + +class ReadMemoryResponse(CmdResponse): + """McuBoot read memory response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Read-Memory response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class FlashReadOnceResponse(CmdResponse): + """McuBoot flash read once response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Flash-Read-Once response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length, *values = unpack_from(f"<{self.header.params_count}I", raw_data) + self.length: int = length + self.values: List[int] = list(values) + self.data = raw_data[8 : 8 + self.length] if self.length > 0 else b"" + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class FlashReadResourceResponse(CmdResponse): + """McuBoot flash read resource response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Flash-Read-Resource response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class KeyProvisioningResponse(CmdResponse): + """McuBoot Key Provisioning response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Key-Provisioning response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class TrustProvisioningResponse(CmdResponse): + """McuBoot Trust Provisioning response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Trust-Provisioning response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, *values = unpack(f"<{self.header.params_count}I", raw_data) + self.values: List[int] = list(values) + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}" + + +class NoResponse(CmdResponse): + """Special internal case when no response is provided by the target.""" + + def __init__(self, cmd_tag: int) -> None: + """Create a NoResponse to an command that was issued, indicated by its tag. + + :param cmd_tag: Tag of the command that preceded the no-response from target + """ + header = CmdHeader(tag=cmd_tag, flags=0, reserved=0, params_count=0) + raw_data = pack(" CmdResponse: + """Parse command response. + + :param data: Input data in bytes + :param offset: The offset of input data + :return: De-serialized object from data + """ + known_response: Dict[int, Type[CmdResponse]] = { + ResponseTag.GENERIC.tag: GenericResponse, + ResponseTag.GET_PROPERTY.tag: GetPropertyResponse, + ResponseTag.READ_MEMORY.tag: ReadMemoryResponse, + ResponseTag.FLASH_READ_RESOURCE.tag: FlashReadResourceResponse, + ResponseTag.FLASH_READ_ONCE.tag: FlashReadOnceResponse, + ResponseTag.KEY_BLOB_RESPONSE.tag: ReadMemoryResponse, + ResponseTag.KEY_PROVISIONING_RESPONSE.tag: KeyProvisioningResponse, + ResponseTag.TRUST_PROVISIONING_RESPONSE.tag: TrustProvisioningResponse, + } + header = CmdHeader.from_bytes(data, offset) + if header.tag in known_response: + return known_response[header.tag](header, data[CmdHeader.SIZE :]) + + return CmdResponse(header, data[CmdHeader.SIZE :]) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py new file mode 100644 index 00000000..278fa96a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Status and error codes used by the MBoot protocol.""" + +from spsdk.utils.spsdk_enum import SpsdkEnum + +######################################################################################################################## +# McuBoot Status Codes (Errors) +######################################################################################################################## + +# pylint: disable=line-too-long +# fmt: off +class StatusCode(SpsdkEnum): + """McuBoot status codes.""" + + SUCCESS = (0, "Success", "Success") + FAIL = (1, "Fail", "Fail") + READ_ONLY = (2, "ReadOnly", "Read Only Error") + OUT_OF_RANGE = (3, "OutOfRange", "Out Of Range Error") + INVALID_ARGUMENT = (4, "InvalidArgument", "Invalid Argument Error") + TIMEOUT = (5, "TimeoutError", "Timeout Error") + NO_TRANSFER_IN_PROGRESS = (6, "NoTransferInProgress", "No Transfer In Progress Error") + + # Flash driver errors. + FLASH_SIZE_ERROR = (100, "FlashSizeError", "FLASH Driver: Size Error") + FLASH_ALIGNMENT_ERROR = (101, "FlashAlignmentError", "FLASH Driver: Alignment Error") + FLASH_ADDRESS_ERROR = (102, "FlashAddressError", "FLASH Driver: Address Error") + FLASH_ACCESS_ERROR = (103, "FlashAccessError", "FLASH Driver: Access Error") + FLASH_PROTECTION_VIOLATION = (104, "FlashProtectionViolation", "FLASH Driver: Protection Violation") + FLASH_COMMAND_FAILURE = (105, "FlashCommandFailure", "FLASH Driver: Command Failure") + FLASH_UNKNOWN_PROPERTY = (106, "FlashUnknownProperty", "FLASH Driver: Unknown Property") + FLASH_ERASE_KEY_ERROR = (107, "FlashEraseKeyError", "FLASH Driver: Provided Key Does Not Match Programmed Flash Memory Key") + FLASH_REGION_EXECUTE_ONLY = (108, "FlashRegionExecuteOnly", "FLASH Driver: Region Execute Only") + FLASH_EXEC_IN_RAM_NOT_READY = (109, "FlashExecuteInRamFunctionNotReady", "FLASH Driver: Execute In RAM Function Not Ready") + FLASH_COMMAND_NOT_SUPPORTED = (111, "FlashCommandNotSupported", "FLASH Driver: Command Not Supported") + FLASH_READ_ONLY_PROPERTY = (112, "FlashReadOnlyProperty", "FLASH Driver: Flash Memory Property Is Read-Only") + FLASH_INVALID_PROPERTY_VALUE = (113, "FlashInvalidPropertyValue", "FLASH Driver: Flash Memory Property Value Out Of Range") + FLASH_INVALID_SPECULATION_OPTION = (114, "FlashInvalidSpeculationOption", "FLASH Driver: Flash Memory Prefetch Speculation Option Is Invalid") + FLASH_ECC_ERROR = (116, "FlashEccError", "FLASH Driver: ECC Error") + FLASH_COMPARE_ERROR = (117, "FlashCompareError", "FLASH Driver: Destination And Source Memory Contents Do Not Match") + FLASH_REGULATION_LOSS = (118, "FlashRegulationLoss", "FLASH Driver: Loss Of Regulation During Read") + FLASH_INVALID_WAIT_STATE_CYCLES = (119, "FlashInvalidWaitStateCycles", "FLASH Driver: Wait State Cycle Set To Read/Write Mode Is Invalid") + FLASH_OUT_OF_DATE_CFPA_PAGE = (132, "FlashOutOfDateCfpaPage", "FLASH Driver: Out Of Date CFPA Page") + FLASH_BLANK_IFR_PAGE_DATA = (133, "FlashBlankIfrPageData", "FLASH Driver: Blank IFR Page Data") + FLASH_ENCRYPTED_REGIONS_ERASE_NOT_DONE_AT_ONCE = (134, "FlashEncryptedRegionsEraseNotDoneAtOnce", "FLASH Driver: Encrypted Regions Erase Not Done At Once") + FLASH_PROGRAM_VERIFICATION_NOT_ALLOWED = (135, "FlashProgramVerificationNotAllowed", "FLASH Driver: Program Verification Not Allowed") + FLASH_HASH_CHECK_ERROR = (136, "FlashHashCheckError", "FLASH Driver: Hash Check Error") + FLASH_SEALED_PFR_REGION = (137, "FlashSealedPfrRegion", "FLASH Driver: Sealed PFR Region") + FLASH_PFR_REGION_WRITE_BROKEN = (138, "FlashPfrRegionWriteBroken", "FLASH Driver: PFR Region Write Broken") + FLASH_NMPA_UPDATE_NOT_ALLOWED = (139, "FlashNmpaUpdateNotAllowed", "FLASH Driver: NMPA Update Not Allowed") + FLASH_CMPA_CFG_DIRECT_ERASE_NOT_ALLOWED = (140, "FlashCmpaCfgDirectEraseNotAllowed", "FLASH Driver: CMPA Cfg Direct Erase Not Allowed") + FLASH_PFR_BANK_IS_LOCKED = (141, "FlashPfrBankIsLocked", "FLASH Driver: PFR Bank Is Locked") + FLASH_CFPA_SCRATCH_PAGE_INVALID = (148, "FlashCfpaScratchPageInvalid", "FLASH Driver: CFPA Scratch Page Invalid") + FLASH_CFPA_VERSION_ROLLBACK_DISALLOWED = (149, "FlashCfpaVersionRollbackDisallowed", "FLASH Driver: CFPA Version Rollback Disallowed") + FLASH_READ_HIDING_AREA_DISALLOWED = (150, "FlashReadHidingAreaDisallowed", "FLASH Driver: Flash Memory Hiding Read Not Allowed") + FLASH_MODIFY_PROTECTED_AREA_DISALLOWED = (151, "FlashModifyProtectedAreaDisallowed", "FLASH Driver: Flash Firewall Page Locked Erase And Program Are Not Allowed") + FLASH_COMMAND_OPERATION_IN_PROGRESS = (152, "FlashCommandOperationInProgress", "FLASH Driver: Flash Memory State Busy Flash Memory Command Is In Progress") + + # I2C driver errors. + I2C_SLAVE_TX_UNDERRUN = (200, "I2cSlaveTxUnderrun", "I2C Driver: Slave Tx Underrun") + I2C_SLAVE_RX_OVERRUN = (201, "I2cSlaveRxOverrun", "I2C Driver: Slave Rx Overrun") + I2C_ARBITRATION_LOST = (202, "I2cArbitrationLost", "I2C Driver: Arbitration Lost") + + # SPI driver errors. + SPI_SLAVE_TX_UNDERRUN = (300, "SpiSlaveTxUnderrun", "SPI Driver: Slave Tx Underrun") + SPI_SLAVE_RX_OVERRUN = (301, "SpiSlaveRxOverrun", "SPI Driver: Slave Rx Overrun") + + # QuadSPI driver errors. + QSPI_FLASH_SIZE_ERROR = (400, "QspiFlashSizeError", "QSPI Driver: Flash Size Error") + QSPI_FLASH_ALIGNMENT_ERROR = (401, "QspiFlashAlignmentError", "QSPI Driver: Flash Alignment Error") + QSPI_FLASH_ADDRESS_ERROR = (402, "QspiFlashAddressError", "QSPI Driver: Flash Address Error") + QSPI_FLASH_COMMAND_FAILURE = (403, "QspiFlashCommandFailure", "QSPI Driver: Flash Command Failure") + QSPI_FLASH_UNKNOWN_PROPERTY = (404, "QspiFlashUnknownProperty", "QSPI Driver: Flash Unknown Property") + QSPI_NOT_CONFIGURED = (405, "QspiNotConfigured", "QSPI Driver: Not Configured") + QSPI_COMMAND_NOT_SUPPORTED = (406, "QspiCommandNotSupported", "QSPI Driver: Command Not Supported") + QSPI_COMMAND_TIMEOUT = (407, "QspiCommandTimeout", "QSPI Driver: Command Timeout") + QSPI_WRITE_FAILURE = (408, "QspiWriteFailure", "QSPI Driver: Write Failure") + + # OTFAD driver errors. + OTFAD_SECURITY_VIOLATION = (500, "OtfadSecurityViolation", "OTFAD Driver: Security Violation") + OTFAD_LOGICALLY_DISABLED = (501, "OtfadLogicallyDisabled", "OTFAD Driver: Logically Disabled") + OTFAD_INVALID_KEY = (502, "OtfadInvalidKey", "OTFAD Driver: Invalid Key") + OTFAD_INVALID_KEY_BLOB = (503, "OtfadInvalidKeyBlob", "OTFAD Driver: Invalid Key Blob") + + # Sending errors. + SENDING_OPERATION_CONDITION_ERROR = (1812, "SendOperationConditionError", "Send Operation Condition failed") + + # SDMMC driver errors. + + # FlexSPI statuses. + FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT_RT5xx = (6000, "FLEXSPI_SequenceExecutionTimeout_RT5xx", "FLEXSPI: Sequence Execution Timeout") + FLEXSPI_INVALID_SEQUENCE_RT5xx = (6001, "FLEXSPI_InvalidSequence_RT5xx", "FLEXSPI: Invalid Sequence") + FLEXSPI_DEVICE_TIMEOUT_RT5xx = (6002, "FLEXSPI_DeviceTimeout_RT5xx", "FLEXSPI: Device Timeout") + FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT = (7000, "FLEXSPI_SequenceExecutionTimeout", "FLEXSPI: Sequence Execution Timeout") + FLEXSPI_INVALID_SEQUENCE = (7001, "FLEXSPI_InvalidSequence", "FLEXSPI: Invalid Sequence") + FLEXSPI_DEVICE_TIMEOUT = (7002, "FLEXSPI_DeviceTimeout", "FLEXSPI: Device Timeout") + + # Bootloader errors. + UNKNOWN_COMMAND = (10000, "UnknownCommand", "Unknown Command") + SECURITY_VIOLATION = (10001, "SecurityViolation", "Security Violation") + ABORT_DATA_PHASE = (10002, "AbortDataPhase", "Abort Data Phase") + PING_ERROR = (10003, "PingError", "Ping Error") + NO_RESPONSE = (10004, "NoResponse", "No response packet from target device") + NO_RESPONSE_EXPECTED = (10005, "NoResponseExpected", "No Response Expected") + UNSUPPORTED_COMMAND = (10006, "UnsupportedCommand", "Unsupported Command") + + # SB loader errors. + ROMLDR_SECTION_OVERRUN = (10100, "RomLdrSectionOverrun", "ROM Loader: Section Overrun") + ROMLDR_SIGNATURE = (10101, "RomLdrSignature", "ROM Loader: Signature Error") + ROMLDR_SECTION_LENGTH = (10102, "RomLdrSectionLength", "ROM Loader: Section Length Error") + ROMLDR_UNENCRYPTED_ONLY = (10103, "RomLdrUnencryptedOnly", "ROM Loader: Unencrypted Only") + ROMLDR_EOF_REACHED = (10104, "RomLdrEOFReached", "ROM Loader: EOF Reached") + ROMLDR_CHECKSUM = (10105, "RomLdrChecksum", "ROM Loader: Checksum Error") + ROMLDR_CRC32_ERROR = (10106, "RomLdrCrc32Error", "ROM Loader: CRC32 Error") + ROMLDR_UNKNOWN_COMMAND = (10107, "RomLdrUnknownCommand", "ROM Loader: Unknown Command") + ROMLDR_ID_NOT_FOUND = (10108, "RomLdrIdNotFound", "ROM Loader: ID Not Found") + ROMLDR_DATA_UNDERRUN = (10109, "RomLdrDataUnderrun", "ROM Loader: Data Underrun") + ROMLDR_JUMP_RETURNED = (10110, "RomLdrJumpReturned", "ROM Loader: Jump Returned") + ROMLDR_CALL_FAILED = (10111, "RomLdrCallFailed", "ROM Loader: Call Failed") + ROMLDR_KEY_NOT_FOUND = (10112, "RomLdrKeyNotFound", "ROM Loader: Key Not Found") + ROMLDR_SECURE_ONLY = (10113, "RomLdrSecureOnly", "ROM Loader: Secure Only") + ROMLDR_RESET_RETURNED = (10114, "RomLdrResetReturned", "ROM Loader: Reset Returned") + ROMLDR_ROLLBACK_BLOCKED = (10115, "RomLdrRollbackBlocked", "ROM Loader: Rollback Blocked") + ROMLDR_INVALID_SECTION_MAC_COUNT = (10116, "RomLdrInvalidSectionMacCount", "ROM Loader: Invalid Section Mac Count") + ROMLDR_UNEXPECTED_COMMAND = (10117, "RomLdrUnexpectedCommand", "ROM Loader: Unexpected Command") + ROMLDR_BAD_SBKEK = (10118, "RomLdrBadSBKEK", "ROM Loader: Bad SBKEK Detected") + ROMLDR_PENDING_JUMP_COMMAND = (10119, "RomLdrPendingJumpCommand", "ROM Loader: Pending Jump Command") + + # Memory interface errors. + MEMORY_RANGE_INVALID = (10200, "MemoryRangeInvalid", "Memory Range Invalid") + MEMORY_READ_FAILED = (10201, "MemoryReadFailed", "Memory Read Failed") + MEMORY_WRITE_FAILED = (10202, "MemoryWriteFailed", "Memory Write Failed") + MEMORY_CUMULATIVE_WRITE = (10203, "MemoryCumulativeWrite", "Memory Cumulative Write") + MEMORY_APP_OVERLAP_WITH_EXECUTE_ONLY_REGION = (10204, "MemoryAppOverlapWithExecuteOnlyRegion", "Memory App Overlap with exec region") + MEMORY_NOT_CONFIGURED = (10205, "MemoryNotConfigured", "Memory Not Configured") + MEMORY_ALIGNMENT_ERROR = (10206, "MemoryAlignmentError", "Memory Alignment Error") + MEMORY_VERIFY_FAILED = (10207, "MemoryVerifyFailed", "Memory Verify Failed") + MEMORY_WRITE_PROTECTED = (10208, "MemoryWriteProtected", "Memory Write Protected") + MEMORY_ADDRESS_ERROR = (10209, "MemoryAddressError", "Memory Address Error") + MEMORY_BLANK_CHECK_FAILED = (10210, "MemoryBlankCheckFailed", "Memory Black Check Failed") + MEMORY_BLANK_PAGE_READ_DISALLOWED = (10211, "MemoryBlankPageReadDisallowed", "Memory Blank Page Read Disallowed") + MEMORY_PROTECTED_PAGE_READ_DISALLOWED = (10212, "MemoryProtectedPageReadDisallowed", "Memory Protected Page Read Disallowed") + MEMORY_PFR_SPEC_REGION_WRITE_BROKEN = (10213, "MemoryPfrSpecRegionWriteBroken", "Memory PFR Spec Region Write Broken") + MEMORY_UNSUPPORTED_COMMAND = (10214, "MemoryUnsupportedCommand", "Memory Unsupported Command") + + # Property store errors. + UNKNOWN_PROPERTY = (10300, "UnknownProperty", "Unknown Property") + READ_ONLY_PROPERTY = (10301, "ReadOnlyProperty", "Read Only Property") + INVALID_PROPERTY_VALUE = (10302, "InvalidPropertyValue", "Invalid Property Value") + + # Property store errors. + APP_CRC_CHECK_PASSED = (10400, "AppCrcCheckPassed", "Application CRC Check: Passed") + APP_CRC_CHECK_FAILED = (10401, "AppCrcCheckFailed", "Application: CRC Check: Failed") + APP_CRC_CHECK_INACTIVE = (10402, "AppCrcCheckInactive", "Application CRC Check: Inactive") + APP_CRC_CHECK_INVALID = (10403, "AppCrcCheckInvalid", "Application CRC Check: Invalid") + APP_CRC_CHECK_OUT_OF_RANGE = (10404, "AppCrcCheckOutOfRange", "Application CRC Check: Out Of Range") + + # Packetizer errors. + PACKETIZER_NO_PING_RESPONSE = (10500, "NoPingResponse", "Packetizer Error: No Ping Response") + PACKETIZER_INVALID_PACKET_TYPE = (10501, "InvalidPacketType", "Packetizer Error: No response received for ping command") + PACKETIZER_INVALID_CRC = (10502, "InvalidCRC", "Packetizer Error: Invalid packet type") + PACKETIZER_NO_COMMAND_RESPONSE = (10503, "NoCommandResponse", "Packetizer Error: No response received for command") + + # Reliable Update statuses. + RELIABLE_UPDATE_SUCCESS = (10600, "ReliableUpdateSuccess", "Reliable Update: Success") + RELIABLE_UPDATE_FAIL = (10601, "ReliableUpdateFail", "Reliable Update: Fail") + RELIABLE_UPDATE_INACTIVE = (10602, "ReliableUpdateInactive", "Reliable Update: Inactive") + RELIABLE_UPDATE_BACKUPAPPLICATIONINVALID = (10603, "ReliableUpdateBackupApplicationInvalid", "Reliable Update: Backup Application Invalid") + RELIABLE_UPDATE_STILLINMAINAPPLICATION = (10604, "ReliableUpdateStillInMainApplication", "Reliable Update: Still In Main Application") + RELIABLE_UPDATE_SWAPSYSTEMNOTREADY = (10605, "ReliableUpdateSwapSystemNotReady", "Reliable Update: Swap System Not Ready") + RELIABLE_UPDATE_BACKUPBOOTLOADERNOTREADY = (10606, "ReliableUpdateBackupBootloaderNotReady", "Reliable Update: Backup Bootloader Not Ready") + RELIABLE_UPDATE_SWAPINDICATORADDRESSINVALID = (10607, "ReliableUpdateSwapIndicatorAddressInvalid", "Reliable Update: Swap Indicator Address Invalid") + RELIABLE_UPDATE_SWAPSYSTEMNOTAVAILABLE = (10608, "ReliableUpdateSwapSystemNotAvailable", "Reliable Update: Swap System Not Available") + RELIABLE_UPDATE_SWAPTEST = (10609, "ReliableUpdateSwapTest", "Reliable Update: Swap Test") + + # Serial NOR/EEPROM statuses. + SERIAL_NOR_EEPROM_ADDRESS_INVALID = (10700, "SerialNorEepromAddressInvalid", "SerialNorEeprom: Address Invalid") + SERIAL_NOR_EEPROM_TRANSFER_ERROR = (10701, "SerialNorEepromTransferError", "SerialNorEeprom: Transfer Error") + SERIAL_NOR_EEPROM_TYPE_INVALID = (10702, "SerialNorEepromTypeInvalid", "SerialNorEeprom: Type Invalid") + SERIAL_NOR_EEPROM_SIZE_INVALID = (10703, "SerialNorEepromSizeInvalid", "SerialNorEeprom: Size Invalid") + SERIAL_NOR_EEPROM_COMMAND_INVALID = (10704, "SerialNorEepromCommandInvalid", "SerialNorEeprom: Command Invalid") + + # ROM API statuses. + ROM_API_NEED_MORE_DATA = (10800, "RomApiNeedMoreData", "RomApi: Need More Data") + ROM_API_BUFFER_SIZE_NOT_ENOUGH = (10801, "RomApiBufferSizeNotEnough", "RomApi: Buffer Size Not Enough") + ROM_API_INVALID_BUFFER = (10802, "RomApiInvalidBuffer", "RomApi: Invalid Buffer") + + # FlexSPI NAND statuses. + FLEXSPINAND_READ_PAGE_FAIL = (20000, "FlexSPINANDReadPageFail", "FlexSPINAND: Read Page Fail") + FLEXSPINAND_READ_CACHE_FAIL = (20001, "FlexSPINANDReadCacheFail", "FlexSPINAND: Read Cache Fail") + FLEXSPINAND_ECC_CHECK_FAIL = (20002, "FlexSPINANDEccCheckFail", "FlexSPINAND: Ecc Check Fail") + FLEXSPINAND_PAGE_LOAD_FAIL = (20003, "FlexSPINANDPageLoadFail", "FlexSPINAND: Page Load Fail") + FLEXSPINAND_PAGE_EXECUTE_FAIL = (20004, "FlexSPINANDPageExecuteFail", "FlexSPINAND: Page Execute Fail") + FLEXSPINAND_ERASE_BLOCK_FAIL = (20005, "FlexSPINANDEraseBlockFail", "FlexSPINAND: Erase Block Fail") + FLEXSPINAND_WAIT_TIMEOUT = (20006, "FlexSPINANDWaitTimeout", "FlexSPINAND: Wait Timeout") + FlexSPINAND_NOT_SUPPORTED = (20007, "SPINANDPageSizeOverTheMaxSupportedSize", "SPI NAND: PageSize over the max supported size") + FlexSPINAND_FCB_UPDATE_FAIL = (20008, "FailedToUpdateFlashConfigBlockToSPINAND", "SPI NAND: Failed to update Flash config block to SPI NAND") + FlexSPINAND_DBBT_UPDATE_FAIL = (20009, "Failed to update discovered bad block table to SPI NAND", "SPI NAND: Failed to update discovered bad block table to SPI NAND") + FLEXSPINAND_WRITEALIGNMENTERROR = (20010, "FlexSPINANDWriteAlignmentError", "FlexSPINAND: Write Alignment Error") + FLEXSPINAND_NOT_FOUND = (20011, "FlexSPINANDNotFound", "FlexSPINAND: Not Found") + + # FlexSPI NOR statuses. + FLEXSPINOR_PROGRAM_FAIL = (20100, "FLEXSPINORProgramFail", "FLEXSPINOR: Program Fail") + FLEXSPINOR_ERASE_SECTOR_FAIL = (20101, "FLEXSPINOREraseSectorFail", "FLEXSPINOR: Erase Sector Fail") + FLEXSPINOR_ERASE_ALL_FAIL = (20102, "FLEXSPINOREraseAllFail", "FLEXSPINOR: Erase All Fail") + FLEXSPINOR_WAIT_TIMEOUT = (20103, "FLEXSPINORWaitTimeout", "FLEXSPINOR:Wait Timeout") + FLEXSPINOR_NOT_SUPPORTED = (20104, "FLEXSPINORPageSizeOverTheMaxSupportedSize", "FlexSPINOR: PageSize over the max supported size") + FLEXSPINOR_WRITE_ALIGNMENT_ERROR = (20105, "FlexSPINORWriteAlignmentError", "FlexSPINOR:Write Alignment Error") + FLEXSPINOR_COMMANDFAILURE = (20106, "FlexSPINORCommandFailure", "FlexSPINOR: Command Failure") + FLEXSPINOR_SFDP_NOTFOUND = (20107, "FlexSPINORSFDPNotFound", "FlexSPINOR: SFDP Not Found") + FLEXSPINOR_UNSUPPORTED_SFDP_VERSION = (20108, "FLEXSPINORUnsupportedSFDPVersion", "FLEXSPINOR: Unsupported SFDP Version") + FLEXSPINOR_FLASH_NOTFOUND = (20109, "FLEXSPINORFlashNotFound", "FLEXSPINOR Flash Not Found") + FLEXSPINOR_DTR_READ_DUMMYPROBEFAILED = (20110, "FLEXSPINORDTRReadDummyProbeFailed", "FLEXSPINOR: DTR Read Dummy Probe Failed") + + # OCOTP statuses. + OCOTP_READ_FAILURE = (20200, "OCOTPReadFailure", "OCOTP: Read Failure") + OCOTP_PROGRAM_FAILURE = (20201, "OCOTPProgramFailure", "OCOTP: Program Failure") + OCOTP_RELOAD_FAILURE = (20202, "OCOTPReloadFailure", "OCOTP: Reload Failure") + OCOTP_WAIT_TIMEOUT = (20203, "OCOTPWaitTimeout", "OCOTP: Wait Timeout") + + # SEMC NOR statuses. + SEMCNOR_DEVICE_TIMEOUT = (21100, "SemcNOR_DeviceTimeout", "SemcNOR: Device Timeout") + SEMCNOR_INVALID_MEMORY_ADDRESS = (21101, "SemcNOR_InvalidMemoryAddress", "SemcNOR: Invalid Memory Address") + SEMCNOR_UNMATCHED_COMMAND_SET = (21102, "SemcNOR_unmatchedCommandSet", "SemcNOR: unmatched Command Set") + SEMCNOR_ADDRESS_ALIGNMENT_ERROR = (21103, "SemcNOR_AddressAlignmentError", "SemcNOR: Address Alignment Error") + SEMCNOR_INVALID_CFI_SIGNATURE = (21104, "SemcNOR_InvalidCfiSignature", "SemcNOR: Invalid Cfi Signature") + SEMCNOR_COMMAND_ERROR_NO_OP_TO_SUSPEND = (21105, "SemcNOR_CommandErrorNoOpToSuspend", "SemcNOR: Command Error No Op To Suspend") + SEMCNOR_COMMAND_ERROR_NO_INFO_AVAILABLE = (21106, "SemcNOR_CommandErrorNoInfoAvailable", "SemcNOR: Command Error No Info Available") + SEMCNOR_BLOCK_ERASE_COMMAND_FAILURE = (21107, "SemcNOR_BlockEraseCommandFailure", "SemcNOR: Block Erase Command Failure") + SEMCNOR_BUFFER_PROGRAM_COMMAND_FAILURE = (21108, "SemcNOR_BufferProgramCommandFailure", "SemcNOR: Buffer Program Command Failure") + SEMCNOR_PROGRAM_VERIFY_FAILURE = (21109, "SemcNOR_ProgramVerifyFailure", "SemcNOR: Program Verify Failure") + SEMCNOR_ERASE_VERIFY_FAILURE = (21110, "SemcNOR_EraseVerifyFailure", "SemcNOR: Erase Verify Failure") + SEMCNOR_INVALID_CFG_TAG = (21116, "SemcNOR_InvalidCfgTag", "SemcNOR: Invalid Cfg Tag") + + # SEMC NAND statuses. + SEMCNAND_DEVICE_TIMEOUT = (21200, "SemcNAND_DeviceTimeout", "SemcNAND: Device Timeout") + SEMCNAND_INVALID_MEMORY_ADDRESS = (21201, "SemcNAND_InvalidMemoryAddress", "SemcNAND: Invalid Memory Address") + SEMCNAND_NOT_EQUAL_TO_ONE_PAGE_SIZE = (21202, "SemcNAND_NotEqualToOnePageSize", "SemcNAND: Not Equal To One Page Size") + SEMCNAND_MORE_THAN_ONE_PAGE_SIZE = (21203, "SemcNAND_MoreThanOnePageSize", "SemcNAND: More Than One Page Size") + SEMCNAND_ECC_CHECK_FAIL = (21204, "SemcNAND_EccCheckFail", "SemcNAND: Ecc Check Fail") + SEMCNAND_INVALID_ONFI_PARAMETER = (21205, "SemcNAND_InvalidOnfiParameter", "SemcNAND: Invalid Onfi Parameter") + SEMCNAND_CANNOT_ENABLE_DEVICE_ECC = (21206, "SemcNAND_CannotEnableDeviceEcc", "SemcNAND: Cannot Enable Device Ecc") + SEMCNAND_SWITCH_TIMING_MODE_FAILURE = (21207, "SemcNAND_SwitchTimingModeFailure", "SemcNAND: Switch Timing Mode Failure") + SEMCNAND_PROGRAM_VERIFY_FAILURE = (21208, "SemcNAND_ProgramVerifyFailure", "SemcNAND: Program Verify Failure") + SEMCNAND_ERASE_VERIFY_FAILURE = (21209, "SemcNAND_EraseVerifyFailure", "SemcNAND: Erase Verify Failure") + SEMCNAND_INVALID_READBACK_BUFFER = (21210, "SemcNAND_InvalidReadbackBuffer", "SemcNAND: Invalid Readback Buffer") + SEMCNAND_INVALID_CFG_TAG = (21216, "SemcNAND_InvalidCfgTag", "SemcNAND: Invalid Cfg Tag") + SEMCNAND_FAIL_TO_UPDATE_FCB = (21217, "SemcNAND_FailToUpdateFcb", "SemcNAND: Fail To Update Fcb") + SEMCNAND_FAIL_TO_UPDATE_DBBT = (21218, "SemcNAND_FailToUpdateDbbt", "SemcNAND: Fail To Update Dbbt") + SEMCNAND_DISALLOW_OVERWRITE_BCB = (21219, "SemcNAND_DisallowOverwriteBcb", "SemcNAND: Disallow Overwrite Bcb") + SEMCNAND_ONLY_SUPPORT_ONFI_DEVICE = (21220, "SemcNAND_OnlySupportOnfiDevice", "SemcNAND: Only Support Onfi Device") + SEMCNAND_MORE_THAN_MAX_IMAGE_COPY = (21221, "SemcNAND_MoreThanMaxImageCopy", "SemcNAND: More Than Max Image Copy") + SEMCNAND_DISORDERED_IMAGE_COPIES = (21222, "SemcNAND_DisorderedImageCopies", "SemcNAND: Disordered Image Copies") + + # SPIFI NOR statuses. + SPIFINOR_PROGRAM_FAIL = (22000, "SPIFINOR_ProgramFail", "SPIFINOR: Program Fail") + SPIFINOR_ERASE_SECTORFAIL = (22001, "SPIFINOR_EraseSectorFail", "SPIFINOR: Erase Sector Fail") + SPIFINOR_ERASE_ALL_FAIL = (22002, "SPIFINOR_EraseAllFail", "SPIFINOR: Erase All Fail") + SPIFINOR_WAIT_TIMEOUT = (22003, "SPIFINOR_WaitTimeout", "SPIFINOR: Wait Timeout") + SPIFINOR_NOT_SUPPORTED = (22004, "SPIFINOR_NotSupported", "SPIFINOR: Not Supported") + SPIFINOR_WRITE_ALIGNMENTERROR = (22005, "SPIFINOR_WriteAlignmentError", "SPIFINOR: Write Alignment Error") + SPIFINOR_COMMAND_FAILURE = (22006, "SPIFINOR_CommandFailure", "SPIFINOR: Command Failure") + SPIFINOR_SFDP_NOT_FOUND = (22007, "SPIFINOR_SFDP_NotFound", "SPIFINOR: SFDP Not Found") + + # EDGELOCK ENCLAVE statuses. + EDGELOCK_INVALID_RESPONSE = (30000, "EDGELOCK_InvalidResponse", "EDGELOCK: Invalid Response") + EDGELOCK_RESPONSE_ERROR = (30001, "EDGELOCK_ResponseError", "EDGELOCK: Response Error") + EDGELOCK_ABORT = (30002, "EDGELOCK_Abort", "EDGELOCK: Abort") + EDGELOCK_OPERATION_FAILED = (30003, "EDGELOCK_OperationFailed", "EDGELOCK: Operation Failed") + EDGELOCK_OTP_PROGRAM_FAILURE = (30004, "EDGELOCK_OTPProgramFailure", "EDGELOCK: OTP Program Failure") + EDGELOCK_OTP_LOCKED = (30005, "EDGELOCK_OTPLocked", "EDGELOCK: OTP Locked") + EDGELOCK_OTP_INVALID_IDX = (30006, "EDGELOCK_OTPInvalidIDX", "EDGELOCK: OTP Invalid IDX") + EDGELOCK_INVALID_LIFECYCLE = (30007, "EDGELOCK_InvalidLifecycle", "EDGELOCK: Invalid Lifecycle") + + # OTP statuses. + OTP_INVALID_ADDRESS = (52801, "OTP_InvalidAddress", "OTD: Invalid OTP address") + OTP_PROGRAM_FAIL = (52802, "OTP_ProgrammingFail", "OTD: Programming failed") + OTP_CRC_FAIL = (52803, "OTP_CRCFail", "OTP: CRC check failed") + OTP_ERROR = (52804, "OTP_Error", "OTP: Error happened during OTP operation") + OTP_ECC_CRC_FAIL = (52805, "OTP_EccCheckFail", "OTP: ECC check failed during OTP operation") + OTP_LOCKED = (52806, "OTP_FieldLocked", "OTP: Field is locked when programming") + OTP_TIMEOUT = (52807, "OTP_Timeout", "OTP: Operation timed out") + OTP_CRC_CHECK_PASS = (52808, "OTP_CRCCheckPass", "OTP: CRC check passed") + OTP_VERIFY_FAIL = (52009, "OPT_VerifyFail", "OTP: Failed to verify OTP write") + + # Security subsystem statuses. + SECURITY_SUBSYSTEM_ERROR = (1515890085, "SecuritySubSystemError", "Security SubSystem Error") + + # TrustProvisioning statuses. + TP_SUCCESS = (0, "TP_SUCCESS", "TP: SUCCESS") + TP_GENERAL_ERROR = (80000, "TP_GENERAL_ERROR", "TP: General error") + TP_CRYPTO_ERROR = (80001, "TP_CRYPTO_ERROR", "TP: Error during cryptographic operation") + TP_NULLPTR_ERROR = (80002, "TP_NULLPTR_ERROR", "TP: NULL pointer dereference or when buffer could not be allocated") + TP_ALREADYINITIALIZED = (80003, "TP_ALREADYINITIALIZED", "TP: Already initialized") + TP_BUFFERSMALL = (80004, "TP_BUFFERSMALL", "TP: Buffer is too small") + TP_ADDRESS_ERROR = (80005, "TP_ADDRESS_ERROR", "TP: Address out of allowed range or buffer could not be allocated") + TP_CONTAINERINVALID = (80006, "TP_CONTAINERINVALID", "TP: Container header or size is invalid") + TP_CONTAINERENTRYINVALID = (80007, "TP_CONTAINERENTRYINVALID", "TP: Container entry invalid") + TP_CONTAINERENTRYNOTFOUND = (80008, "TP_CONTAINERENTRYNOTFOUND", "TP: Container entry not found in container") + TP_INVALIDSTATEOPERATION = (80009, "TP_INVALIDSTATEOPERATION", "TP: Attempt to process command in disallowed state") + TP_COMMAND_ERROR = (80010, "TP_COMMAND_ERROR", "TP: ISP command arguments are invalid") + TP_PUF_ERROR = (80011, "TP_PUF_ERROR", "TP: PUF operation error") + TP_FLASH_ERROR = (80012, "TP_FLASH_ERROR", "TP: Flash erase/program/verify_erase failed") + TP_SECRETBOX_ERROR = (80013, "TP_SECRETBOX_ERROR", "TP: SBKEK or USER KEK cannot be stored in secret box") + TP_PFR_ERROR = (80014, "TP_PFR_ERROR", "TP: Protected Flash Region operation failed") + TP_VERIFICATION_ERROR = (80015, "TP_VERIFICATION_ERROR", "TP: Container signature verification failed") + TP_CFPA_ERROR = (80016, "TP_CFPA_ERROR", "TP: CFPA page cannot be stored") + TP_CMPA_ERROR = (80017, "TP_CMPA_ERROR", "TP: CMPA page cannot be stored or ROTKH or SECU registers are invalid") + TP_ADDR_OUT_OF_RANGE = (80018, "TP_ADDR_OUT_OF_RANGE", "TP: Address is out of range") + TP_CONTAINER_ADDR_ERROR = (80019, "TP_CONTAINER_ADDR_ERROR", "TP: Container address in write context is invalid or there is no memory for entry storage") + TP_CONTAINER_ADDR_UNALIGNED = (80020, "TP_CONTAINER_ADDR_UNALIGNED", "TP: Container address in read context is unaligned") + TP_CONTAINER_BUFF_SMALL = (80021, "TP_CONTAINER_BUFF_SMALL", "TP: There is not enough memory to store the container") + TP_CONTAINER_NO_ENTRY = (80022, "TP_CONTAINER_NO_ENTRY", "TP: Attempt to sign an empty container") + TP_CERT_ADDR_ERROR = (80023, "TP_CERT_ADDR_ERROR", "TP: Destination address of OEM certificate is invalid") + TP_CERT_ADDR_UNALIGNED = (80024, "TP_CERT_ADDR_UNALIGNED", "TP: Destination address of certificate is unaligned") + TP_CERT_OVERLAPPING = (80025, "TP_CERT_OVERLAPPING", "TP: OEM certificates are overlapping due to wrong destination addresses") + TP_PACKET_ERROR = (80026, "TP_PACKET_ERROR", "TP: Error during packet sending/receiving") + TP_PACKET_DATA_ERROR = (80027, "TP_PACKET_DATA_ERROR", "TP: Data in packet handle are invalid") + TP_UNKNOWN_COMMAND = (80028, "TP_UNKNOWN_COMMAND", "TP: Unknown command was received") + TP_SB3_FILE_ERROR = (80029, "TP_SB3_FILE_ERROR", "TP: Error during processing SB3 file") + # TP_CRITICAL_ERROR_START (80100) + TP_GENERAL_CRITICAL_ERROR = (80101, "TP_GENERAL_CRITICAL_ERROR", "TP: Critical error") + TP_CRYPTO_CRITICAL_ERROR = (80102, "TP_CRYPTO_CRITICAL_ERROR", "TP: Error of crypto module which prevents proper functionality") + TP_PUF_CRITICAL_ERROR = (80103, "TP_PUF_CRITICAL_ERROR", "TP: Initialization or start of the PUF periphery failed") + TP_PFR_CRITICAL_ERROR = (80104, "TP_PFR_CRITICAL_ERROR", "TP: Initialization of PFR or reading of activation code failed") + TP_PERIPHERAL_CRITICAL_ERROR = (80105, "TP_PERIPHERAL_CRITICAL_ERROR", "TP: Peripheral failure") + TP_PRINCE_CRITICAL_ERROR = (80106, "TP_PRINCE_CRITICAL_ERROR", "TP: Error during PRINCE encryption/decryption") + TP_SHA_CHECK_CRITICAL_ERROR = (80107, "TP_SHA_CHECK_CRITICAL_ERROR", "TP: SHA check verification failed") + + # IAP statuses. + IAP_INVALID_ARGUMENT = (100001, "IAP_InvalidArgument", "IAP: Invalid Argument Detected During API Execution") + IAP_OUT_OF_MEMORY = (100002, "IAP_OutOfMemory", "IAP: Heap Size Not Large Enough During API Execution") + IAP_READ_DISALLOWED = (100003, "IAP_ReadDisallowed ", "IAP: Read Memory Operation Disallowed During API Execution") + IAP_CUMULATIVE_WRITE = (100004, "IAP_CumulativeWrite", "IAP: Flash Memory Region To Be Programmed Is Not Empty") + IAP_ERASE_FAILUIRE = (100005, "IAP_EraseFailuire", "IAP: Erase Operation Failed") + IAP_COMMAND_NOT_SUPPORTED = (100006, "IAP_CommandNotSupported", "IAP: Specific Command Not Supported") + IAP_MEMORY_ACCESS_DISABLED = (100007, "IAP_MemoryAccessDisabled", "IAP: Memory Access Disabled") +# fmt: on + + +def stringify_status_code(status_code: int) -> str: + """Stringifies the MBoot status code.""" + return ( + f"{status_code} ({status_code:#x}) " + f"{StatusCode.get_description(status_code) if status_code in StatusCode.tags() else f'Unknown error code ({status_code})'}." + ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py new file mode 100644 index 00000000..91ef89c0 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Exceptions used in the MBoot module.""" + +from spsdk.exceptions import SPSDKError + +from .error_codes import StatusCode + +######################################################################################################################## +# McuBoot Exceptions +######################################################################################################################## + + +class McuBootError(SPSDKError): + """MBoot Module: Base Exception.""" + + fmt = "MBoot: {description}" + + +class McuBootCommandError(McuBootError): + """MBoot Module: Command Exception.""" + + fmt = "MBoot: {cmd_name} interrupted -> {description}" + + def __init__(self, cmd: str, value: int) -> None: + """Initialize the Command Error exception. + + :param cmd: Name of the command causing the exception + :param value: Response value causing the exception + """ + super().__init__() + self.cmd_name = cmd + self.error_value = value + self.description = ( + StatusCode.get_description(value) + if value in StatusCode.tags() + else f"Unknown Error 0x{value:08X}" + ) + + def __str__(self) -> str: + return self.fmt.format(cmd_name=self.cmd_name, description=self.description) + + +class McuBootDataAbortError(McuBootError): + """MBoot Module: Data phase aborted by sender.""" + + fmt = "Mboot: Data aborted by sender" + + +class McuBootConnectionError(McuBootError): + """MBoot Module: Connection Exception.""" + + fmt = "MBoot: Connection issue -> {description}" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/__init__.py new file mode 100644 index 00000000..eff4a84e --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright (c) 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing the Mboot communication protocol.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py new file mode 100644 index 00000000..e0add394 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Buspal Mboot device implementation.""" +import datetime +import logging +import struct +import time +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple + +from serial import SerialException +from serial.tools.list_ports import comports +from typing_extensions import Self + +from spsdk.exceptions import SPSDKError +from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from spsdk.mboot.protocol.serial_protocol import FPType, MbootSerialProtocol, to_int +from spsdk.utils.interfaces.device.serial_device import SerialDevice + +logger = logging.getLogger(__name__) + + +@dataclass +class ScanArgs: + """Scan arguments dataclass.""" + + port: Optional[str] + props: Optional[List[str]] + + @classmethod + def parse(cls, params: str, extra_params: Optional[str] = None) -> Self: + """Parse given scanning parameters and extra parameters into ScanArgs class. + + :param params: Parameters as a string + :param extra_params: Optional extra parameters as a string + """ + props = [] + if extra_params: + props = extra_params.split(",") + target = props.pop(0) + if target not in ["spi", "i2c"]: + raise SPSDKError(f"Target must be either 'spi' or 'ic2', not {target}") + port_parts = params.split(",") + return cls(port=port_parts.pop(0), props=props) + + +class SpiModeCommand(Enum): + """Spi mode commands.""" + + exit = 0x00 # 00000000 - Exit to bit bang mode + version = 0x01 # 00000001 - Enter raw SPI mode, display version string + chip_select = 0x02 # 0000001x - CS high (1) or low (0) + sniff = 0x0C # 000011XX - Sniff SPI traffic when CS low(10)/all(01) + bulk_transfer = 0x10 # 0001xxxx - Bulk SPI transfer, send/read 1-16 bytes (0=1byte!) + config_periph = 0x40 # 0100wxyz - Configure peripherals w=power, x=pull-ups, y=AUX, z=CS + set_speed = 0x60 # 01100xxx - SPI speed + config_spi = 0x80 # 1000wxyz - SPI config, w=HiZ/3.3v, x=CKP idle, y=CKE edge, z=SMP sample + write_then_read = 0x04 # 00000100 - Write then read extended command + + +# pylint: disable=invalid-name +class SpiConfigShift(Enum): + """Spi configuration shifts for the mask.""" + + direction = 0 + phase = 1 + polarity = 2 + + +# pylint: disable=invalid-name +class SpiClockPolarity(Enum): + """SPI clock polarity configuration.""" + + active_high = 0 # Active-high SPI clock (idles low). + active_low = 1 # Active-low SPI clock (idles high). + + +# pylint: disable=invalid-name +class SpiClockPhase(Enum): + """SPI clock phase configuration.""" + + # First edge on SPSCK occurs at the middle of the first cycle of a data transfer. + first_edge = 0 + # First edge on SPSCK occurs at the start of the first cycle of a data transfer. + second_edge = 1 + + +# pylint: disable=invalid-name +class SpiShiftDirection(Enum): + """SPI clock phase configuration.""" + + msb_first = 0 # Data transfers start with most significant bit. + lsb_first = 1 # Data transfers start with least significant bit. + + +class SpiConfiguration: + """Dataclass to store SPI configuration.""" + + speed: int + polarity: SpiClockPolarity + phase: SpiClockPhase + direction: SpiShiftDirection + + +# pylint: disable=invalid-name +class BBConstants(Enum): + """Constants.""" + + reset_count = 20 # Max number of nulls to send to enter BBIO mode + response_ok = 0x01 # Successful command response + bulk_transfer_max = 4096 # Max number of bytes per bulk transfer + packet_timeout_ms = 10 # Packet timeout in milliseconds + + +class Response(str, Enum): + """Response to enter bit bang mode.""" + + BITBANG = "BBIO1" + SPI = "SPI1" + I2C = "I2C1" + + +class BuspalMode(Enum): + """Bit Bang mode command.""" + + RESET = 0x00 # Reset, responds "BBIO1" + SPI = 0x01 # Enter binary SPI mode, responds "SPI1" + I2C = 0x02 # Enter binary I2C mode, responds "I2C1" + + +MODE_COMMANDS_RESPONSES = { + BuspalMode.RESET: Response.BITBANG, + BuspalMode.SPI: Response.SPI, + BuspalMode.I2C: Response.I2C, +} + + +class MbootBuspalProtocol(MbootSerialProtocol): + """Mboot Serial protocol.""" + + default_baudrate = 57600 + default_timeout = 5000 + device: SerialDevice + mode: BuspalMode + + def __init__(self, device: SerialDevice) -> None: + """Initialize the MbootBuspalProtocol object. + + :param device: The device instance + """ + super().__init__(device) + + def open(self) -> None: + """Open the interface.""" + self.device.open() + # reset first, send bit-bang command + self._enter_mode(BuspalMode.RESET) + logger.debug("Entered BB mode") + self._enter_mode(self.mode) + + @classmethod + def scan( + cls, + port: Optional[str] = None, + props: Optional[List[str]] = None, + timeout: Optional[int] = None, + ) -> List[SerialDevice]: + """Scan connected serial ports and set BUSPAL properties. + + Returns list of serial ports with devices that respond to BUSPAL communication protocol. + If 'port' is specified, only that serial port is checked + If no devices are found, return an empty list. + + :param port: name of preferred serial port, defaults to None + :param timeout: timeout in milliseconds + :param props: buspal target properties + :return: list of available interfaces + """ + timeout = timeout or cls.default_timeout + if port: + device = cls._check_port_buspal(port, timeout, props) + devices = [device] if device else [] + else: + all_ports = [ + cls._check_port_buspal(comport.device, timeout, props) + for comport in comports(include_links=True) + ] + devices = list(filter(None, all_ports)) + return devices + + @classmethod + def _check_port_buspal( + cls, port: str, timeout: int, props: Optional[List[str]] = None + ) -> Optional[SerialDevice]: + """Check if device on comport 'port' can connect using BUSPAL communication protocol. + + :param port: name of port to check + :param timeout: timeout in milliseconds + :param props: buspal settings + :return: None if device doesn't respond to PING, instance of Interface if it does + """ + props = props if props is not None else [] + try: + device = SerialDevice(port=port, timeout=timeout, baudrate=cls.default_baudrate) + interface = cls(device) + interface.open() + interface._configure(props) + interface._ping() + return device + except (AssertionError, SerialException, McuBootConnectionError) as e: + logger.error(str(e)) + return None + + def _send_frame(self, frame: bytes, wait_for_ack: bool = True) -> None: + """Send frame method to be implemented by child class.""" + raise NotImplementedError() + + def _read(self, size: int, timeout: Optional[int] = None) -> bytes: + """Implementation done by child class.""" + raise NotImplementedError() + + def _configure(self, props: List[str]) -> None: + """Configure the BUSPAL interface. + + :param props: buspal settings + """ + raise NotImplementedError() + + def _enter_mode(self, mode: BuspalMode) -> None: + """Enter BUSPAL mode. + + :param mode: buspal mode + """ + response = MODE_COMMANDS_RESPONSES[mode] + self._send_command_check_response( + bytes([mode.value]), bytes(response.value.encode("utf-8")) + ) + + def _send_command_check_response(self, command: bytes, response: bytes) -> None: + """Send a command and check if expected response is received. + + :param command: command to send + :param response: expected response + """ + self.device.write(command) + data_recvd = self.device.read(len(response)) + format_received = " ".join(hex(x) for x in data_recvd) + format_expected = " ".join(hex(x) for x in response) + assert ( + format_received == format_expected + ), f"Received data '{format_received}' but expected '{format_expected}'" + + def _read_frame_header(self, expected_frame_type: Optional[FPType] = None) -> Tuple[int, int]: + """Read frame header and frame type. Return them as tuple of integers. + + :param expected_frame_type: Check if the frame_type is exactly as expected + :return: Tuple of integers representing frame header and frame type + :raises AssertionError: Unexpected frame header or frame type (if specified) + :raises McuBootDataAbortError: Abort frame received + """ + header = None + time_start = datetime.datetime.now() + time_end = time_start + datetime.timedelta(milliseconds=self.device.timeout) + + # read uart until start byte is equal to FRAME_START_BYTE, max. 'retry_count' times + while header != self.FRAME_START_BYTE and datetime.datetime.now() < time_end: + header = to_int(self._read(1)) + if header == FPType.ABORT: + raise McuBootDataAbortError() + if header != self.FRAME_START_BYTE: + time.sleep(BBConstants.packet_timeout_ms.value / 1000) + assert ( + header == self.FRAME_START_BYTE + ), f"Received invalid frame header '{header:#X}' expected '{self.FRAME_START_BYTE:#X}'" + + frame_type = to_int(self._read(1)) + + if frame_type == FPType.ABORT: + raise McuBootDataAbortError() + return header, frame_type + + +class MbootBuspalSPIInterface(MbootBuspalProtocol): + """BUSPAL SPI interface.""" + + TARGET_SETTINGS = ["speed", "polarity", "phase", "direction"] + + HDR_FRAME_RETRY_CNT = 3 + ACK_WAIT_DELAY = 0.01 # in seconds + device: SerialDevice + identifier = "buspal_spi" + + def __init__(self, device: SerialDevice): + """Initialize the BUSPAL SPI interface. + + :param port: name of the serial port, defaults to None + :param timeout: read/write timeout in milliseconds + """ + self.mode = BuspalMode.SPI + super().__init__(device) + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected Buspal devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params, extra_params) + devices = cls.scan(port=scan_args.port, props=scan_args.props, timeout=timeout) + interfaces = [] + for device in devices: + interfaces.append(cls(device)) + return interfaces + + def _configure(self, props: List[str]) -> None: + """Configure the BUSPAL SPI interface. + + :param props: buspal settings + """ + spi_props: Dict[str, Any] = dict(zip(self.TARGET_SETTINGS, props)) + + speed = int(spi_props.get("speed", 100)) + polarity = SpiClockPolarity(spi_props.get("polarity", SpiClockPolarity.active_low)) + phase = SpiClockPhase(spi_props.get("phase", SpiClockPhase.second_edge)) + direction = SpiShiftDirection(spi_props.get("direction", SpiShiftDirection.msb_first)) + + # set SPI config + logger.debug("Set SPI config") + spi_data = polarity.value << SpiConfigShift.polarity.value + spi_data |= phase.value << SpiConfigShift.phase.value + spi_data |= direction.value << SpiConfigShift.direction.value + spi_data |= SpiModeCommand.config_spi.value + self._send_command_check_response(bytes([spi_data]), bytes([BBConstants.response_ok.value])) + + # set SPI speed + logger.debug(f"Set SPI speed to {speed}bps") + spi_speed = struct.pack(" None: + """Send data to BUSPAL I2C device. + + :param data: Data to send + """ + self._send_frame_retry(data, wait_for_ack, self.HDR_FRAME_RETRY_CNT) + + def _send_frame_retry( + self, data: bytes, wait_for_ack: bool = True, retry_cnt: int = HDR_FRAME_RETRY_CNT + ) -> None: + """Send a frame to BUSPAL SPI device. + + :param data: Data to send + :param wait_for_ack: Wait for ACK frame from device, defaults to True + :param retry_cnt: Number of retry in case the header frame is incorrect + :raises AssertionError: Unexpected frame header or frame type (if specified) + """ + size = min(len(data), BBConstants.bulk_transfer_max.value) + command = struct.pack(" 0: + logger.error( + f"{error} (retry {self.HDR_FRAME_RETRY_CNT-retry_cnt+1}/{self.HDR_FRAME_RETRY_CNT})" + ) + retry_cnt -= 1 + self._send_frame_retry(data, wait_for_ack, retry_cnt) + else: + raise SPSDKError("Failed retrying reading the SPI header frame") from error + + def _read(self, size: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount of bytes from BUSPAL SPI device. + + :return: Data read from the device + """ + size = min(size, BBConstants.bulk_transfer_max.value) + command = struct.pack(" List[Self]: + """Scan connected Buspal devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params, extra_params) + devices = cls.scan(port=scan_args.port, props=scan_args.props, timeout=timeout) + interfaces = [] + for device in devices: + interfaces.append(cls(device)) + return interfaces + + def _configure(self, props: List[str]) -> None: + """Initialize the BUSPAL I2C interface. + + :param props: buspal settings + """ + i2c_props: Dict[str, Any] = dict(zip(self.TARGET_SETTINGS, props)) + + # get I2C configuration values, use default values if settings are not defined in input string) + speed = int(i2c_props.get("speed", 100)) + address = int(i2c_props.get("address", 0x10)) + + # set I2C address + logger.debug(f"Set I2C address to {address}") + i2c_data = struct.pack(" None: + """Send data to BUSPAL I2C device. + + :param data: Data to send + """ + self._send_frame_retry(data, wait_for_ack, self.HDR_FRAME_RETRY_CNT) + + def _send_frame_retry( + self, data: bytes, wait_for_ack: bool = True, retry_cnt: int = HDR_FRAME_RETRY_CNT + ) -> None: + """Send data to BUSPAL I2C device. + + :param data: Data to send + :param wait_for_ack: Wait for ACK frame from device, defaults to True + :param retry_cnt: Number of retry in case the header frame is incorrect + :raises AssertionError: Unexpected frame header or frame type (if specified) + """ + retry_cnt = self.HDR_FRAME_RETRY_CNT + size = min(len(data), BBConstants.bulk_transfer_max.value) + command = struct.pack(" 0: + logger.error( + f"{error} (retry {self.HDR_FRAME_RETRY_CNT-retry_cnt+1}/{self.HDR_FRAME_RETRY_CNT})" + ) + retry_cnt -= 1 + self._send_frame_retry(data, wait_for_ack, retry_cnt) + else: + raise SPSDKError("Failed retrying reading the I2C header frame") from error + + def _read(self, size: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount of bytes from BUSPAL I2C device. + + :return: Data read from the device + """ + size = min(size, BBConstants.bulk_transfer_max.value) + command = struct.pack(" Self: + """Parse given scanning parameters into ScanArgs class. + + :param params: Parameters as a string + """ + return cls(device_path=params) + + +class MbootSdioInterface(MbootSerialProtocol): + """Sdio interface.""" + + identifier = "sdio" + device: SdioDevice + sdio_devices = SDIO_DEVICES + + def __init__(self, device: SdioDevice) -> None: + """Initialize the MbootSdioInterface object. + + :param device: The device instance + """ + super().__init__(device=device) + + @property + def name(self) -> str: + """Get the name of the device. + + :return: Name of the device. + """ + assert isinstance(self.device, SdioDevice) + for name, value in self.sdio_devices.items(): + if value[0] == self.device.vid and value[1] == self.device.pid: + return name + return "Unknown" + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected USB devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Interface timeout + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params) + interfaces = cls.scan(device_path=scan_args.device_path, timeout=timeout) + return interfaces + + @classmethod + def scan( + cls, + device_path: str, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected SDIO devices. + + :param device_path: device path string + :param timeout: Interface timeout + :return: matched SDIO device + """ + devices = SdioDevice.scan(device_path=device_path, timeout=timeout) + return [cls(device) for device in devices] + + def open(self) -> None: + """Open the interface.""" + self.device.open() + + def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: + """Read data on the IN endpoint associated to the HID interface. + + :return: Return CmdResponse object. + :raises McuBootConnectionError: Raises an error if device is not opened for reading + :raises McuBootConnectionError: Raises if device is not available + :raises McuBootDataAbortError: Raises if reading fails + :raises TimeoutError: When timeout occurs + """ + raw_data = self._read(1024) + if not raw_data: + logger.error("Cannot read from SDIO device") + raise TimeoutError() + + _, frame_type = self._parse_frame_header(raw_data) + _length, crc = struct.unpack_from(" Tuple[int, int]: + """Read frame header and frame type. Return them as tuple of integers. + + :param expected_frame_type: Check if the frame_type is exactly as expected + :return: Tuple of integers representing frame header and frame type + :raises McuBootDataAbortError: Target sens Data Abort frame + :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) + :raises McuBootConnectionError: When received invalid ACK + """ + data = self._read(2) + return self._parse_frame_header(data, FPType.ACK) + + def _parse_frame_header( + self, frame: bytes, expected_frame_type: Optional[FPType] = None + ) -> Tuple[int, int]: + """Read frame header and frame type. Return them as tuple of integers. + + :param expected_frame_type: Check if the frame_type is exactly as expected + :return: Tuple of integers representing frame header and frame type + :raises McuBootDataAbortError: Target sens Data Abort frame + :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) + :raises McuBootConnectionError: When received invalid ACK + """ + header, frame_type = struct.unpack_from(" Self: + """Parse given scanning parameters into ScanArgs class. + + :param params: Parameters as a string + """ + port_parts = params.split(",") + return cls( + port=port_parts.pop(0), baudrate=int(port_parts.pop(), 0) if port_parts else None + ) + + +class MbootUARTInterface(MbootSerialProtocol): + """UART interface.""" + + default_baudrate = 57600 + device: SerialDevice + identifier = "uart" + + def __init__(self, device: SerialDevice): + """Initialize the MbootUARTInterface object. + + :param device: The device instance + """ + assert isinstance(device, SerialDevice) + super().__init__(device=device) + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected UART devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params=params) + interfaces = cls.scan( + port=scan_args.port, + baudrate=scan_args.baudrate or cls.default_baudrate, + timeout=timeout, + ) + return interfaces + + @classmethod + def scan( + cls, + port: Optional[str] = None, + baudrate: Optional[int] = None, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected UART devices. + + Returns list of serial ports with devices that respond to PING command. + If 'port' is specified, only that serial port is checked + If no devices are found, return an empty list. + + :param port: name of preferred serial port, defaults to None + :param baudrate: speed of the UART interface, defaults to 56700 + :param timeout: timeout in milliseconds, defaults to 5000 + :return: list of interfaces responding to the PING command + """ + devices = SerialDevice.scan( + port=port, baudrate=baudrate or cls.default_baudrate, timeout=timeout + ) + interfaces = [] + for device in devices: + try: + interface = cls(device) + interface.open() + interface._ping() + interface.close() + interfaces.append(interface) + except Exception: + interface.close() + return interfaces diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py new file mode 100644 index 00000000..687b8e4b --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""USB Mboot interface implementation.""" + + +from dataclasses import dataclass +from typing import List, Optional + +from typing_extensions import Self + +from spsdk.mboot.protocol.bulk_protocol import MbootBulkProtocol +from spsdk.utils.interfaces.device.usb_device import UsbDevice + + +@dataclass +class ScanArgs: + """Scan arguments dataclass.""" + + device_id: str + + @classmethod + def parse(cls, params: str) -> Self: + """Parse given scanning parameters into ScanArgs class. + + :param params: Parameters as a string + """ + return cls(device_id=params.replace(",", ":")) + + +USB_DEVICES = { + # NAME | VID | PID + "MKL27": (0x15A2, 0x0073), + "LPC55": (0x1FC9, 0x0021), + "IMXRT": (0x1FC9, 0x0135), + "MXRT10": (0x15A2, 0x0073), # this is ID of flash-loader for RT101x + "MXRT20": (0x15A2, 0x0073), # this is ID of flash-loader for RT102x + "MXRT50": (0x15A2, 0x0073), # this is ID of flash-loader for RT105x + "MXRT60": (0x15A2, 0x0073), # this is ID of flash-loader for RT106x + "LPC55xx": (0x1FC9, 0x0020), + "LPC551x": (0x1FC9, 0x0022), + "RT6xx": (0x1FC9, 0x0021), + "RT5xx_A": (0x1FC9, 0x0020), + "RT5xx_B": (0x1FC9, 0x0023), + "RT5xx_C": (0x1FC9, 0x0023), + "RT5xx": (0x1FC9, 0x0023), + "RT6xxM": (0x1FC9, 0x0024), + "LPC553x": (0x1FC9, 0x0025), + "MCXN9xx": (0x1FC9, 0x014F), + "MCXA1xx": (0x1FC9, 0x0155), + "MCXN23x": (0x1FC9, 0x0158), +} + + +class MbootUSBInterface(MbootBulkProtocol): + """USB interface.""" + + identifier = "usb" + device: UsbDevice + usb_devices = USB_DEVICES + + def __init__(self, device: UsbDevice) -> None: + """Initialize the MbootUSBInterface object. + + :param device: The device instance + """ + assert isinstance(device, UsbDevice) + super().__init__(device=device) + + @property + def name(self) -> str: + """Get the name of the device.""" + assert isinstance(self.device, UsbDevice) + for name, value in self.usb_devices.items(): + if value[0] == self.device.vid and value[1] == self.device.pid: + return name + return "Unknown" + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected USB devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params=params) + devices = cls.scan(device_id=scan_args.device_id, timeout=timeout) + return devices + + @classmethod + def scan( + cls, + device_id: Optional[str] = None, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected USB devices. + + :param device_id: Device identifier , , device/instance path, device name are supported + :param timeout: Read/write timeout + :return: list of matching RawHid devices + """ + devices = UsbDevice.scan( + device_id=device_id, usb_devices_filter=cls.usb_devices, timeout=timeout + ) + return [cls(device) for device in devices] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py new file mode 100644 index 00000000..e87d6873 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright (c) 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""USBSIO Mboot interface implementation.""" +from typing import List, Optional + +from typing_extensions import Self + +from spsdk.mboot.protocol.serial_protocol import MbootSerialProtocol +from spsdk.utils.interfaces.device.usbsio_device import ScanArgs, UsbSioI2CDevice, UsbSioSPIDevice + + +class MbootUsbSioI2CInterface(MbootSerialProtocol): + """USBSIO I2C interface.""" + + device: UsbSioI2CDevice + identifier = "usbsio_i2c" + + def __init__(self, device: UsbSioI2CDevice): + """Initialize the UsbSioI2CDevice object. + + :param device: The device instance + """ + super().__init__(device=device) + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected USBSIO devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params=params) + interfaces = cls.scan(config=scan_args.config, timeout=timeout) + return interfaces + + @classmethod + def scan(cls, config: Optional[str] = None, timeout: int = 5000) -> List[Self]: + """Scan connected USB-SIO bridge devices. + + :param config: Configuration string identifying spi or i2c SIO interface + and could filter out USB devices + :param timeout: Read timeout in milliseconds, defaults to 5000 + :return: List of interfaces + """ + devices = UsbSioI2CDevice.scan(config, timeout) + spi_devices = [x for x in devices if isinstance(x, UsbSioI2CDevice)] + return [cls(device) for device in spi_devices] + + +class MbootUsbSioSPIInterface(MbootSerialProtocol): + """USBSIO I2C interface.""" + + # START_NOT_READY may be 0x00 or 0xFF depending on the implementation + FRAME_START_NOT_READY_LIST = [0x00, 0xFF] + device: UsbSioSPIDevice + identifier = "usbsio_spi" + + def __init__(self, device: UsbSioSPIDevice) -> None: + """Initialize the UsbSioSPIDevice object. + + :param device: The device instance + """ + super().__init__(device) + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan connected USBSIO devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params=params) + interfaces = cls.scan(config=scan_args.config, timeout=timeout) + return interfaces + + @classmethod + def scan(cls, config: Optional[str] = None, timeout: int = 5000) -> List[Self]: + """Scan connected USB-SIO bridge devices. + + :param config: Configuration string identifying spi or i2c SIO interface + and could filter out USB devices + :param timeout: Read timeout in milliseconds, defaults to 5000 + :return: List of interfaces + """ + devices = UsbSioSPIDevice.scan(config, timeout) + spi_devices = [x for x in devices if isinstance(x, UsbSioSPIDevice)] + return [cls(device) for device in spi_devices] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py new file mode 100644 index 00000000..145d580b --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py @@ -0,0 +1,1683 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for communication with the bootloader.""" + +import logging +import struct +import time +from types import TracebackType +from typing import Callable, Dict, List, Optional, Sequence, Type + +from spsdk.mboot.protocol.base import MbootProtocolBase +from spsdk.utils.interfaces.device.usb_device import UsbDevice + +from .commands import ( + CmdPacket, + CmdResponse, + CommandFlag, + CommandTag, + FlashReadOnceResponse, + FlashReadResourceResponse, + GenerateKeyBlobSelect, + GenericResponse, + GetPropertyResponse, + KeyProvisioningResponse, + KeyProvOperation, + NoResponse, + ReadMemoryResponse, + TrustProvDevHsmDsc, + TrustProvisioningResponse, + TrustProvOperation, + TrustProvWpc, +) +from .error_codes import StatusCode, stringify_status_code +from .exceptions import ( + McuBootCommandError, + McuBootConnectionError, + McuBootDataAbortError, + McuBootError, + SPSDKError, +) +from .memories import ExtMemId, ExtMemRegion, FlashRegion, MemoryRegion, RamRegion +from .properties import PropertyTag, PropertyValueBase, Version, parse_property_value + +logger = logging.getLogger(__name__) + + +######################################################################################################################## +# McuBoot Class +######################################################################################################################## +class McuBoot: # pylint: disable=too-many-public-methods + """Class for communication with the bootloader.""" + + DEFAULT_MAX_PACKET_SIZE = 32 + + @property + def status_code(self) -> int: + """Return status code of the last operation.""" + return self._status_code + + @property + def status_string(self) -> str: + """Return status string.""" + return stringify_status_code(self._status_code) + + @property + def is_opened(self) -> bool: + """Return True if the device is open.""" + return self._interface.is_opened + + def __init__(self, interface: MbootProtocolBase, cmd_exception: bool = False) -> None: + """Initialize the McuBoot object. + + :param interface: The instance of communication interface class + :param cmd_exception: True to throw McuBootCommandError on any error; + False to set status code only + Note: some operation might raise McuBootCommandError is all cases + + """ + self._cmd_exception = cmd_exception + self._status_code = StatusCode.SUCCESS.tag + self._interface = interface + self.reopen = False + self.enable_data_abort = False + self._pause_point: Optional[int] = None + + def __enter__(self) -> "McuBoot": + self.reopen = True + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + def _process_cmd(self, cmd_packet: CmdPacket) -> CmdResponse: + """Process Command. + + :param cmd_packet: Command Packet + :return: command response derived from the CmdResponse + :raises McuBootConnectionError: Timeout Error + :raises McuBootCommandError: Error during command execution on the target + """ + if not self.is_opened: + logger.info("TX: Device not opened") + raise McuBootConnectionError("Device not opened") + + logger.debug(f"TX-PACKET: {str(cmd_packet)}") + + try: + self._interface.write_command(cmd_packet) + response = self._interface.read() + except TimeoutError: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.debug("RX-PACKET: No Response, Timeout Error !") + response = NoResponse(cmd_tag=cmd_packet.header.tag) + + assert isinstance(response, CmdResponse) + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + + if self._cmd_exception and self._status_code != StatusCode.SUCCESS: + raise McuBootCommandError(CommandTag.get_label(cmd_packet.header.tag), response.status) + logger.info(f"CMD: Status: {self.status_string}") + return response + + def _read_data( + self, + cmd_tag: CommandTag, + length: int, + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bytes: + """Read data from device. + + :param cmd_tag: Tag indicating the read command. + :param length: Length of data to read + :param progress_callback: Callback for updating the caller about the progress + :raises McuBootConnectionError: Timeout error or a problem opening the interface + :raises McuBootCommandError: Error during command execution on the target + :return: Data read from the device + """ + data = b"" + + if not self.is_opened: + logger.error("RX: Device not opened") + raise McuBootConnectionError("Device not opened") + while True: + try: + response = self._interface.read() + except McuBootDataAbortError as e: + logger.error(f"RX: {e}") + logger.info("Try increasing the timeout value") + response = self._interface.read() + except TimeoutError: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.error("RX: No Response, Timeout Error !") + response = NoResponse(cmd_tag=cmd_tag.tag) + break + + if isinstance(response, bytes): + data += response + if progress_callback: + progress_callback(len(data), length) + + elif isinstance(response, GenericResponse): + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + if response.cmd_tag == cmd_tag: + break + + if len(data) < length or self.status_code != StatusCode.SUCCESS: + status_info = ( + StatusCode.get_label(self._status_code) + if self._status_code in StatusCode.tags() + else f"0x{self._status_code:08X}" + ) + logger.debug(f"CMD: Received {len(data)} from {length} Bytes, {status_info}") + if self._cmd_exception: + assert isinstance(response, CmdResponse) + raise McuBootCommandError(cmd_tag.label, response.status) + else: + logger.info(f"CMD: Successfully Received {len(data)} from {length} Bytes") + + return data[:length] if len(data) > length else data + + def _send_data( + self, + cmd_tag: CommandTag, + data: List[bytes], + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bool: + """Send Data part of specific command. + + :param cmd_tag: Tag indicating the command + :param data: List of data chunks to send + :param progress_callback: Callback for updating the caller about the progress + :raises McuBootConnectionError: Timeout error + :raises McuBootCommandError: Error during command execution on the target + :return: True if the operation is successful + """ + if not self.is_opened: + logger.info("TX: Device Disconnected") + raise McuBootConnectionError("Device Disconnected !") + + total_sent = 0 + total_to_send = sum(len(chunk) for chunk in data) + # this difference is applicable for load-image and program-aeskey commands + expect_response = cmd_tag != CommandTag.NO_COMMAND + self._interface.allow_abort = self.enable_data_abort + try: + for data_chunk in data: + self._interface.write_data(data_chunk) + total_sent += len(data_chunk) + if progress_callback: + progress_callback(total_sent, total_to_send) + if self._pause_point and total_sent > self._pause_point: + time.sleep(0.1) + self._pause_point = None + + if expect_response: + response = self._interface.read() + except TimeoutError as e: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.error("RX: No Response, Timeout Error !") + raise McuBootConnectionError("No Response from Device") from e + except SPSDKError as e: + logger.error(f"RX: {e}") + if expect_response: + response = self._interface.read() + else: + self._status_code = StatusCode.SENDING_OPERATION_CONDITION_ERROR.tag + + if expect_response: + assert isinstance(response, CmdResponse) + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + if response.status != StatusCode.SUCCESS: + status_info = ( + StatusCode.get_label(self._status_code) + if self._status_code in StatusCode.tags() + else f"0x{self._status_code:08X}" + ) + logger.debug(f"CMD: Send Error, {status_info}") + if self._cmd_exception: + raise McuBootCommandError(cmd_tag.label, response.status) + return False + + logger.info(f"CMD: Successfully Send {total_sent} out of {total_to_send} Bytes") + return total_sent == total_to_send + + def _get_max_packet_size(self) -> int: + """Get max packet size. + + :return int: max packet size in B + """ + packet_size_property = None + try: + packet_size_property = self.get_property(prop_tag=PropertyTag.MAX_PACKET_SIZE) + except McuBootError: + pass + if packet_size_property is None: + packet_size_property = [self.DEFAULT_MAX_PACKET_SIZE] + logger.warning( + f"CMD: Unable to get MAX PACKET SIZE, using: {self.DEFAULT_MAX_PACKET_SIZE}" + ) + return packet_size_property[0] + + def _split_data(self, data: bytes) -> List[bytes]: + """Split data to send if necessary. + + :param data: Data to send + :return: List of data splices + """ + if not self._interface.need_data_split: + return [data] + max_packet_size = self._get_max_packet_size() + logger.info(f"CMD: Max Packet Size = {max_packet_size}") + return [data[i : i + max_packet_size] for i in range(0, len(data), max_packet_size)] + + def open(self) -> None: + """Connect to the device.""" + logger.info(f"Connect: {str(self._interface)}") + self._interface.open() + + def close(self) -> None: + """Disconnect from the device.""" + logger.info(f"Closing: {str(self._interface)}") + self._interface.close() + + def get_property_list(self) -> List[PropertyValueBase]: + """Get a list of available properties. + + :return: List of available properties. + :raises McuBootCommandError: Failure to read properties list + """ + property_list: List[PropertyValueBase] = [] + for property_tag in PropertyTag: + try: + values = self.get_property(property_tag) + except McuBootCommandError: + continue + + if values: + prop = parse_property_value(property_tag.tag, values) + assert prop is not None, "Property values cannot be parsed" + property_list.append(prop) + + self._status_code = StatusCode.SUCCESS.tag + if not property_list: + self._status_code = StatusCode.FAIL.tag + if self._cmd_exception: + raise McuBootCommandError("GetPropertyList", self.status_code) + + return property_list + + def _get_internal_flash(self) -> List[FlashRegion]: + """Get information about the internal flash. + + :return: list of FlashRegion objects + """ + index = 0 + mdata: List[FlashRegion] = [] + start_address = 0 + while True: + try: + values = self.get_property(PropertyTag.FLASH_START_ADDRESS, index) + if not values: + break + if index == 0: + start_address = values[0] + elif start_address == values[0]: + break + region_start = values[0] + values = self.get_property(PropertyTag.FLASH_SIZE, index) + if not values: + break + region_size = values[0] + values = self.get_property(PropertyTag.FLASH_SECTOR_SIZE, index) + if not values: + break + region_sector_size = values[0] + mdata.append( + FlashRegion( + index=index, + start=region_start, + size=region_size, + sector_size=region_sector_size, + ) + ) + index += 1 + except McuBootCommandError: + break + + return mdata + + def _get_internal_ram(self) -> List[RamRegion]: + """Get information about the internal RAM. + + :return: list of RamRegion objects + """ + index = 0 + mdata: List[RamRegion] = [] + start_address = 0 + while True: + try: + values = self.get_property(PropertyTag.RAM_START_ADDRESS, index) + if not values: + break + if index == 0: + start_address = values[0] + elif start_address == values[0]: + break + start = values[0] + values = self.get_property(PropertyTag.RAM_SIZE, index) + if not values: + break + size = values[0] + mdata.append(RamRegion(index=index, start=start, size=size)) + index += 1 + except McuBootCommandError: + break + + return mdata + + def _get_ext_memories(self) -> List[ExtMemRegion]: + """Get information about the external memories. + + :return: list of ExtMemRegion objects supported by the device + :raises SPSDKError: If no response to get property command + :raises SPSDKError: Other Error + """ + ext_mem_list: List[ExtMemRegion] = [] + ext_mem_ids: Sequence[int] = ExtMemId.tags() + try: + values = self.get_property(PropertyTag.CURRENT_VERSION) + except McuBootCommandError: + values = None + + if not values and self._status_code == StatusCode.UNKNOWN_PROPERTY: + self._status_code = StatusCode.SUCCESS.tag + return ext_mem_list + + if not values: + raise SPSDKError("No response to get property command") + + if Version(values[0]) <= Version("2.0.0"): + # old versions mboot support only Quad SPI memory + ext_mem_ids = [ExtMemId.QUAD_SPI0.tag] + + for mem_id in ext_mem_ids: + try: + values = self.get_property(PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES, mem_id) + except McuBootCommandError: + values = None + + if not values: # pragma: no cover # corner-cases are currently untestable without HW + if self._status_code == StatusCode.UNKNOWN_PROPERTY: + break + + if self._status_code in [ + StatusCode.QSPI_NOT_CONFIGURED, + StatusCode.INVALID_ARGUMENT, + ]: + continue + + if self._status_code == StatusCode.MEMORY_NOT_CONFIGURED: + ext_mem_list.append(ExtMemRegion(mem_id=mem_id)) + + if self._status_code == StatusCode.SUCCESS: + raise SPSDKError("Other Error") + + else: + ext_mem_list.append(ExtMemRegion(mem_id=mem_id, raw_values=values)) + return ext_mem_list + + def get_memory_list(self) -> dict: + """Get list of embedded memories. + + :return: dict, with the following keys: internal_flash (optional) - list , + internal_ram (optional) - list, external_mems (optional) - list + :raises McuBootCommandError: Error reading the memory list + """ + memory_list: Dict[str, Sequence[MemoryRegion]] = {} + + # Internal FLASH + mdata = self._get_internal_flash() + if mdata: + memory_list["internal_flash"] = mdata + + # Internal RAM + ram_data = self._get_internal_ram() + if mdata: + memory_list["internal_ram"] = ram_data + + # External Memories + ext_mem_list = self._get_ext_memories() + if ext_mem_list: + memory_list["external_mems"] = ext_mem_list + + self._status_code = StatusCode.SUCCESS.tag + if not memory_list: + self._status_code = StatusCode.FAIL.tag + if self._cmd_exception: + raise McuBootCommandError("GetMemoryList", self.status_code) + + return memory_list + + def flash_erase_all(self, mem_id: int = 0) -> bool: + """Erase complete flash memory without recovering flash security section. + + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: FlashEraseAll(mem_id={mem_id})") + cmd_packet = CmdPacket(CommandTag.FLASH_ERASE_ALL, CommandFlag.NONE.tag, mem_id) + response = self._process_cmd(cmd_packet) + return response.status == StatusCode.SUCCESS + + def flash_erase_region(self, address: int, length: int, mem_id: int = 0) -> bool: + """Erase specified range of flash. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FlashEraseRegion(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.FLASH_ERASE_REGION, CommandFlag.NONE.tag, address, length, mem_id + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def read_memory( + self, + address: int, + length: int, + mem_id: int = 0, + progress_callback: Optional[Callable[[int, int], None]] = None, + fast_mode: bool = False, + ) -> Optional[bytes]: + """Read data from MCU memory. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :param fast_mode: Fast mode for USB-HID data transfer, not reliable !!! + :param progress_callback: Callback for updating the caller about the progress + :return: Data read from the memory; None in case of a failure + """ + logger.info(f"CMD: ReadMemory(address=0x{address:08X}, length={length}, mem_id={mem_id})") + mem_id = _clamp_down_memory_id(memory_id=mem_id) + + # workaround for better USB-HID reliability + if isinstance(self._interface.device, UsbDevice) and not fast_mode: + payload_size = self._get_max_packet_size() + packets = length // payload_size + remainder = length % payload_size + if remainder: + packets += 1 + + data = b"" + + for idx in range(packets): + if idx == packets - 1 and remainder: + data_len = remainder + else: + data_len = payload_size + + cmd_packet = CmdPacket( + CommandTag.READ_MEMORY, + CommandFlag.NONE.tag, + address + idx * payload_size, + data_len, + mem_id, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + data += self._read_data(CommandTag.READ_MEMORY, data_len) + if progress_callback: + progress_callback(len(data), length) + if self._status_code == StatusCode.NO_RESPONSE: + logger.warning(f"CMD: NO RESPONSE, received {len(data)}/{length} B") + return data + else: + return b"" + + return data + + cmd_packet = CmdPacket( + CommandTag.READ_MEMORY, CommandFlag.NONE.tag, address, length, mem_id + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data(CommandTag.READ_MEMORY, cmd_response.length, progress_callback) + return None + + def write_memory( + self, + address: int, + data: bytes, + mem_id: int = 0, + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bool: + """Write data into MCU memory. + + :param address: Start address + :param data: List of bytes + :param progress_callback: Callback for updating the caller about the progress + :param mem_id: Memory ID, see ExtMemId; additionally use `0` for internal memory + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: WriteMemory(address=0x{address:08X}, length={len(data)}, mem_id={mem_id})" + ) + data_chunks = self._split_data(data=data) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.WRITE_MEMORY, CommandFlag.HAS_DATA_PHASE.tag, address, len(data), mem_id + ) + if self._process_cmd(cmd_packet).status == StatusCode.SUCCESS: + return self._send_data(CommandTag.WRITE_MEMORY, data_chunks, progress_callback) + return False + + def fill_memory(self, address: int, length: int, pattern: int = 0xFFFFFFFF) -> bool: + """Fill MCU memory with specified pattern. + + :param address: Start address (must be word aligned) + :param length: Count of words (must be word aligned) + :param pattern: Count of wrote bytes + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FillMemory(address=0x{address:08X}, length={length}, pattern=0x{pattern:08X})" + ) + cmd_packet = CmdPacket( + CommandTag.FILL_MEMORY, CommandFlag.NONE.tag, address, length, pattern + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def flash_security_disable(self, backdoor_key: bytes) -> bool: + """Disable flash security by using of backdoor key. + + :param backdoor_key: The key value as array of 8 bytes + :return: False in case of any problem; True otherwise + :raises McuBootError: If the backdoor_key is not 8 bytes long + """ + if len(backdoor_key) != 8: + raise McuBootError("Backdoor key must by 8 bytes long") + logger.info(f"CMD: FlashSecurityDisable(backdoor_key={backdoor_key!r})") + key_high = backdoor_key[0:4][::-1] + key_low = backdoor_key[4:8][::-1] + cmd_packet = CmdPacket( + CommandTag.FLASH_SECURITY_DISABLE, CommandFlag.NONE.tag, data=key_high + key_low + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def get_property(self, prop_tag: PropertyTag, index: int = 0) -> Optional[List[int]]: + """Get specified property value. + + :param prop_tag: Property TAG (see Properties Enum) + :param index: External memory ID or internal memory region index (depends on property type) + :return: list integers representing the property; None in case no response from device + :raises McuBootError: If received invalid get-property response + """ + logger.info(f"CMD: GetProperty({prop_tag.label}, index={index!r})") + cmd_packet = CmdPacket(CommandTag.GET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, index) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + if isinstance(cmd_response, GetPropertyResponse): + return cmd_response.values + raise McuBootError(f"Received invalid get-property response: {str(cmd_response)}") + return None + + def set_property(self, prop_tag: PropertyTag, value: int) -> bool: + """Set value of specified property. + + :param prop_tag: Property TAG (see Property enumerator) + :param value: The value of selected property + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: SetProperty({prop_tag.label}, value=0x{value:08X})") + cmd_packet = CmdPacket(CommandTag.SET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, value) + cmd_response = self._process_cmd(cmd_packet) + return cmd_response.status == StatusCode.SUCCESS + + def receive_sb_file( + self, + data: bytes, + progress_callback: Optional[Callable[[int, int], None]] = None, + check_errors: bool = False, + ) -> bool: + """Receive SB file. + + :param data: SB file data + :param progress_callback: Callback for updating the caller about the progress + :param check_errors: Check for ABORT_FRAME (and related errors) on USB interface between data packets. + When this parameter is set to `False` significantly improves USB transfer speed (cca 20x) + However, the final status code might be misleading (original root cause may get overridden) + In case `receive-sb-file` fails, re-run the operation with this flag set to `True` + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ReceiveSBfile(data_length={len(data)})") + data_chunks = self._split_data(data=data) + cmd_packet = CmdPacket( + CommandTag.RECEIVE_SB_FILE, CommandFlag.HAS_DATA_PHASE.tag, len(data) + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + self.enable_data_abort = check_errors + if isinstance(self._interface.device, UsbDevice): + try: + # pylint: disable=import-outside-toplevel # import only if needed to save time + from spsdk.sbfile.sb2.images import ImageHeaderV2 + + sb2_header = ImageHeaderV2.parse(data=data) + self._pause_point = sb2_header.first_boot_tag_block * 16 + except SPSDKError: + pass + try: + # pylint: disable=import-outside-toplevel # import only if needed to save time + from spsdk.sbfile.sb31.images import SecureBinary31Header + + sb3_header = SecureBinary31Header.parse(data=data) + self._pause_point = sb3_header.image_total_length + except SPSDKError: + pass + result = self._send_data(CommandTag.RECEIVE_SB_FILE, data_chunks, progress_callback) + self.enable_data_abort = False + return result + return False + + def execute(self, address: int, argument: int, sp: int) -> bool: # pylint: disable=invalid-name + """Execute program on a given address using the stack pointer. + + :param address: Jump address (must be word aligned) + :param argument: Function arguments address + :param sp: Stack pointer address + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: Execute(address=0x{address:08X}, argument=0x{argument:08X}, SP=0x{sp:08X})" + ) + cmd_packet = CmdPacket(CommandTag.EXECUTE, CommandFlag.NONE.tag, address, argument, sp) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def call(self, address: int, argument: int) -> bool: + """Fill MCU memory with specified pattern. + + :param address: Call address (must be word aligned) + :param argument: Function arguments address + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: Call(address=0x{address:08X}, argument=0x{argument:08X})") + cmd_packet = CmdPacket(CommandTag.CALL, CommandFlag.NONE.tag, address, argument) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def reset(self, timeout: int = 2000, reopen: bool = True) -> bool: + """Reset MCU and reconnect if enabled. + + :param timeout: The maximal waiting time in [ms] for reopen connection + :param reopen: True for reopen connection after HW reset else False + :return: False in case of any problem; True otherwise + :raises McuBootError: if reopen is not supported + :raises McuBootConnectionError: Failure to reopen the device + """ + logger.info("CMD: Reset MCU") + cmd_packet = CmdPacket(CommandTag.RESET, CommandFlag.NONE.tag) + ret_val = False + status = self._process_cmd(cmd_packet).status + self.close() + ret_val = True + + if status not in [StatusCode.NO_RESPONSE, StatusCode.SUCCESS]: + ret_val = False + if self._cmd_exception: + raise McuBootConnectionError("Reset command failed") + + if status == StatusCode.NO_RESPONSE: + logger.warning("Did not receive response from reset command, ignoring it") + self._status_code = StatusCode.SUCCESS.tag + + if reopen: + if not self.reopen: + raise McuBootError("reopen is not supported") + time.sleep(timeout / 1000) + try: + self.open() + except SPSDKError as e: + ret_val = False + if self._cmd_exception: + raise McuBootConnectionError("reopen failed") from e + + return ret_val + + def flash_erase_all_unsecure(self) -> bool: + """Erase complete flash memory and recover flash security section. + + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: FlashEraseAllUnsecure") + cmd_packet = CmdPacket(CommandTag.FLASH_ERASE_ALL_UNSECURE, CommandFlag.NONE.tag) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def efuse_read_once(self, index: int) -> Optional[int]: + """Read from MCU flash program once region. + + :param index: Start index + :return: read value (32-bit int); None if operation failed + """ + logger.info(f"CMD: FlashReadOnce(index={index})") + cmd_packet = CmdPacket(CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, 4) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadOnceResponse) + return cmd_response.values[0] + return None + + def efuse_program_once(self, index: int, value: int, verify: bool = False) -> bool: + """Write into MCU once program region (OCOTP). + + :param index: Start index + :param value: Int value (4 bytes long) + :param verify: Verify that data were written (by comparing value as bitmask) + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FlashProgramOnce(index={index}, value=0x{value:X}) " + f"with{'' if verify else 'out'} verification." + ) + cmd_packet = CmdPacket(CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, 4, value) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status != StatusCode.SUCCESS: + return False + if verify: + read_value = self.efuse_read_once(index=index & ((1 << 24) - 1)) + if read_value is None: + return False + # We check only a bitmask, because OTP allows to burn individual bits separately + # Some other bits may have been already written + if read_value & value == value: + return True + # It may happen that ROM will not report error when attempting to write into locked OTP + # In such case we substitute the original SUCCESS code with custom-made OTP_VERIFY_FAIL + self._status_code = StatusCode.OTP_VERIFY_FAIL.tag + return False + return cmd_response.status == StatusCode.SUCCESS + + def flash_read_once(self, index: int, count: int = 4) -> Optional[bytes]: + """Read from MCU flash program once region (max 8 bytes). + + :param index: Start index + :param count: Count of bytes + :return: Data read; None in case of an failure + :raises SPSDKError: When invalid count of bytes. Must be 4 or 8 + """ + if count not in (4, 8): + raise SPSDKError("Invalid count of bytes. Must be 4 or 8") + logger.info(f"CMD: FlashReadOnce(index={index}, bytes={count})") + cmd_packet = CmdPacket(CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, count) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadOnceResponse) + return cmd_response.data + return None + + def flash_program_once(self, index: int, data: bytes) -> bool: + """Write into MCU flash program once region (max 8 bytes). + + :param index: Start index + :param data: Input data aligned to 4 or 8 bytes + :return: False in case of any problem; True otherwise + :raises SPSDKError: When invalid length of data. Must be aligned to 4 or 8 bytes + """ + if len(data) not in (4, 8): + raise SPSDKError("Invalid length of data. Must be aligned to 4 or 8 bytes") + logger.info(f"CMD: FlashProgramOnce(index={index!r}, data={data!r})") + cmd_packet = CmdPacket( + CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, len(data), data=data + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def flash_read_resource(self, address: int, length: int, option: int = 1) -> Optional[bytes]: + """Read resource of flash module. + + :param address: Start address + :param length: Number of bytes + :param option: Area to be read. 0 means Flash IFR, 1 means Flash Firmware ID + :raises McuBootError: when the length is not aligned to 4 bytes + :return: Data from the resource; None in case of an failure + """ + if length % 4: + raise McuBootError("The number of bytes to read is not aligned to the 4 bytes") + logger.info( + f"CMD: FlashReadResource(address=0x{address:08X}, length={length}, option={option})" + ) + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_RESOURCE, CommandFlag.NONE.tag, address, length, option + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadResourceResponse) + return self._read_data(CommandTag.FLASH_READ_RESOURCE, cmd_response.length) + return None + + def configure_memory(self, address: int, mem_id: int) -> bool: + """Configure memory. + + :param address: The address in memory where are locating configuration data + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ConfigureMemory({mem_id}, address=0x{address:08X})") + cmd_packet = CmdPacket(CommandTag.CONFIGURE_MEMORY, CommandFlag.NONE.tag, mem_id, address) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def reliable_update(self, address: int) -> bool: + """Reliable Update. + + :param address: Address where new the firmware is stored + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ReliableUpdate(address=0x{address:08X})") + cmd_packet = CmdPacket(CommandTag.RELIABLE_UPDATE, CommandFlag.NONE.tag, address) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def generate_key_blob( + self, + dek_data: bytes, + key_sel: int = GenerateKeyBlobSelect.OPTMK.tag, + count: int = 72, + ) -> Optional[bytes]: + """Generate Key Blob. + + :param dek_data: Data Encryption Key as bytes + :param key_sel: select the BKEK used to wrap the BK (default: OPTMK/FUSES) + :param count: Key blob count (default: 72 - AES128bit) + :return: Key blob; None in case of an failure + """ + logger.info( + f"CMD: GenerateKeyBlob(dek_len={len(dek_data)}, key_sel={key_sel}, count={count})" + ) + data_chunks = self._split_data(data=dek_data) + cmd_response = self._process_cmd( + CmdPacket( + CommandTag.GENERATE_KEY_BLOB, + CommandFlag.HAS_DATA_PHASE.tag, + key_sel, + len(dek_data), + 0, + ) + ) + if cmd_response.status != StatusCode.SUCCESS: + return None + if not self._send_data(CommandTag.GENERATE_KEY_BLOB, data_chunks): + return None + cmd_response = self._process_cmd( + CmdPacket(CommandTag.GENERATE_KEY_BLOB, CommandFlag.NONE.tag, key_sel, count, 1) + ) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data(CommandTag.GENERATE_KEY_BLOB, cmd_response.length) + return None + + def kp_enroll(self) -> bool: + """Key provisioning: Enroll Command (start PUF). + + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: [KeyProvisioning] Enroll") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, CommandFlag.NONE.tag, KeyProvOperation.ENROLL.tag + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_set_intrinsic_key(self, key_type: int, key_size: int) -> bool: + """Key provisioning: Generate Intrinsic Key. + + :param key_type: Type of the key + :param key_size: Size of the key + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: [KeyProvisioning] SetIntrinsicKey(type={key_type}, key_size={key_size})") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.SET_INTRINSIC_KEY.tag, + key_type, + key_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_write_nonvolatile(self, mem_id: int = 0) -> bool: + """Key provisioning: Write the key to a nonvolatile memory. + + :param mem_id: The memory ID (default: 0) + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: [KeyProvisioning] WriteNonVolatileMemory(mem_id={mem_id})") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.WRITE_NON_VOLATILE.tag, + mem_id, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_read_nonvolatile(self, mem_id: int = 0) -> bool: + """Key provisioning: Load the key from a nonvolatile memory to bootloader. + + :param mem_id: The memory ID (default: 0) + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: [KeyProvisioning] ReadNonVolatileMemory(mem_id={mem_id})") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.READ_NON_VOLATILE.tag, + mem_id, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_set_user_key(self, key_type: int, key_data: bytes) -> bool: + """Key provisioning: Send the user key specified by to bootloader. + + :param key_type: type of the user key, see enumeration for details + :param key_data: binary content of the user key + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: [KeyProvisioning] SetUserKey(key_type={key_type}, " f"key_len={len(key_data)})" + ) + data_chunks = self._split_data(data=key_data) + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.HAS_DATA_PHASE.tag, + KeyProvOperation.SET_USER_KEY.tag, + key_type, + len(key_data), + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return self._send_data(CommandTag.KEY_PROVISIONING, data_chunks) + return False + + def kp_write_key_store(self, key_data: bytes) -> bool: + """Key provisioning: Write key data into key store area. + + :param key_data: key store binary content to be written to processor + :return: result of the operation; True means success + """ + logger.info(f"CMD: [KeyProvisioning] WriteKeyStore(key_len={len(key_data)})") + data_chunks = self._split_data(data=key_data) + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.HAS_DATA_PHASE.tag, + KeyProvOperation.WRITE_KEY_STORE.tag, + 0, + len(key_data), + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return self._send_data(CommandTag.KEY_PROVISIONING, data_chunks) + return False + + def kp_read_key_store(self) -> Optional[bytes]: + """Key provisioning: Read key data from key store area.""" + logger.info("CMD: [KeyProvisioning] ReadKeyStore") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, CommandFlag.NONE.tag, KeyProvOperation.READ_KEY_STORE.tag + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, KeyProvisioningResponse) + return self._read_data(CommandTag.KEY_PROVISIONING, cmd_response.length) + return None + + def load_image( + self, data: bytes, progress_callback: Optional[Callable[[int, int], None]] = None + ) -> bool: + """Load a boot image to the device. + + :param data: boot image + :param progress_callback: Callback for updating the caller about the progress + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: LoadImage(length={len(data)})") + data_chunks = self._split_data(data) + # there's no command in this case + self._status_code = StatusCode.SUCCESS.tag + return self._send_data(CommandTag.NO_COMMAND, data_chunks, progress_callback) + + def tp_prove_genuinity(self, address: int, buffer_size: int) -> Optional[int]: + """Start the process of proving genuinity. + + :param address: Address where to prove genuinity request (challenge) container + :param buffer_size: Maximum size of the response package (limit 0xFFFF) + :raises McuBootError: Invalid input parameters + :return: True if prove_genuinity operation is successfully completed + """ + logger.info( + f"CMD: [TrustProvisioning] ProveGenuinity(address={hex(address)}, " + f"buffer_size={buffer_size})" + ) + if buffer_size > 0xFFFF: + raise McuBootError("buffer_size must be less than 0xFFFF") + address_msb = (address >> 32) & 0xFFFF_FFFF + address_lsb = address & 0xFFFF_FFFF + sentinel_cmd = _tp_sentinel_frame( + TrustProvOperation.PROVE_GENUINITY.tag, args=[address_msb, address_lsb, buffer_size] + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, data=sentinel_cmd + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, TrustProvisioningResponse) + return cmd_response.values[0] + return None + + def tp_set_wrapped_data(self, address: int, stage: int = 0x4B, control: int = 1) -> bool: + """Start the process of setting OEM data. + + :param address: Address where the wrapped data container on target + :param control: 1 - use the address, 2 - use container within the firmware, defaults to 1 + :param stage: Stage of TrustProvisioning flow, defaults to 0x4B + :return: True if set_wrapped_data operation is successfully completed + """ + logger.info(f"CMD: [TrustProvisioning] SetWrappedData(address={hex(address)})") + if address == 0: + control = 2 + + address_msb = (address >> 32) & 0xFFFF_FFFF + address_lsb = address & 0xFFFF_FFFF + stage_control = control << 8 | stage + sentinel_cmd = _tp_sentinel_frame( + TrustProvOperation.ISP_SET_WRAPPED_DATA.tag, + args=[stage_control, address_msb, address_lsb], + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, data=sentinel_cmd + ) + cmd_response = self._process_cmd(cmd_packet) + return cmd_response.status == StatusCode.SUCCESS + + def fuse_program(self, address: int, data: bytes, mem_id: int = 0) -> bool: + """Program fuse. + + :param address: Start address + :param data: List of bytes + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FuseProgram(address=0x{address:08X}, length={len(data)}, mem_id={mem_id})" + ) + data_chunks = self._split_data(data=data) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.FUSE_PROGRAM, CommandFlag.HAS_DATA_PHASE.tag, address, len(data), mem_id + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover + # command is not supported in any device, thus we can't measure coverage + return self._send_data(CommandTag.FUSE_PROGRAM, data_chunks) + return False + + def fuse_read(self, address: int, length: int, mem_id: int = 0) -> Optional[bytes]: + """Read fuse. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :return: Data read from the fuse; None in case of a failure + """ + logger.info(f"CMD: ReadFuse(address=0x{address:08X}, length={length}, mem_id={mem_id})") + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket(CommandTag.FUSE_READ, CommandFlag.NONE.tag, address, length, mem_id) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover + # command is not supported in any device, thus we can't measure coverage + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data(CommandTag.FUSE_READ, cmd_response.length) + return None + + def update_life_cycle(self, life_cycle: int) -> bool: + """Update device life cycle. + + :param life_cycle: New life cycle value. + :return: False in case of any problems, True otherwise. + """ + logger.info(f"CMD: UpdateLifeCycle (life cycle=0x{life_cycle:02X})") + cmd_packet = CmdPacket(CommandTag.UPDATE_LIFE_CYCLE, CommandFlag.NONE.tag, life_cycle) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def ele_message( + self, cmdMsgAddr: int, cmdMsgCnt: int, respMsgAddr: int, respMsgCnt: int + ) -> bool: + """Send EdgeLock Enclave message. + + :param cmdMsgAddr: Address in RAM where is prepared the command message words + :param cmdMsgCnt: Count of 32bits command words + :param respMsgAddr: Address in RAM where the command store the response + :param respMsgCnt: Count of 32bits response words + + :return: False in case of any problems, True otherwise. + """ + logger.info( + f"CMD: EleMessage Command (cmdMsgAddr=0x{cmdMsgAddr:08X}, cmdMsgCnt={cmdMsgCnt})" + ) + if respMsgCnt: + logger.info( + f"CMD: EleMessage Response (respMsgAddr=0x{respMsgAddr:08X}, respMsgCnt={respMsgCnt})" + ) + cmd_packet = CmdPacket( + CommandTag.ELE_MESSAGE, + CommandFlag.NONE.tag, + 0, # reserved for future use as a sub command ID or anything else + cmdMsgAddr, + cmdMsgCnt, + respMsgAddr, + respMsgCnt, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_hsm_gen_key( + self, + key_type: int, + reserved: int, + key_blob_output_addr: int, + key_blob_output_size: int, + ecdsa_puk_output_addr: int, + ecdsa_puk_output_size: int, + ) -> Optional[List[int]]: + """Trust provisioning: OEM generate common keys. + + :param key_type: Key to generate (MFW_ISK, MFW_ENCK, GEN_SIGNK, GET_CUST_MK_SK) + :param reserved: Reserved, must be zero + :param key_blob_output_addr: The output buffer address where ROM writes the key blob to + :param key_blob_output_size: The output buffer size in byte + :param ecdsa_puk_output_addr: The output buffer address where ROM writes the public key to + :param ecdsa_puk_output_size: The output buffer size in byte + :return: Return byte count of the key blob + byte count of the public key from the device; + None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] OEM generate common keys") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_GEN_KEY.tag, + key_type, + reserved, + key_blob_output_addr, + key_blob_output_size, + ecdsa_puk_output_addr, + ecdsa_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_oem_gen_master_share( + self, + oem_share_input_addr: int, + oem_share_input_size: int, + oem_enc_share_output_addr: int, + oem_enc_share_output_size: int, + oem_enc_master_share_output_addr: int, + oem_enc_master_share_output_size: int, + oem_cust_cert_puk_output_addr: int, + oem_cust_cert_puk_output_size: int, + ) -> Optional[List[int]]: + """Takes the entropy seed provided by the OEM as input. + + :param oem_share_input_addr: The input buffer address + where the OEM Share(entropy seed) locates at + :param oem_share_input_size: The byte count of the OEM Share + :param oem_enc_share_output_addr: The output buffer address + where ROM writes the Encrypted OEM Share to + :param oem_enc_share_output_size: The output buffer size in byte + :param oem_enc_master_share_output_addr: The output buffer address + where ROM writes the Encrypted OEM Master Share to + :param oem_enc_master_share_output_size: The output buffer size in byte. + :param oem_cust_cert_puk_output_addr: The output buffer address where + ROM writes the OEM Customer Certificate Public Key to + :param oem_cust_cert_puk_output_size: The output buffer size in byte + :return: Sizes of two encrypted blobs(the Encrypted OEM Share and the Encrypted OEM Master Share) + and a public key(the OEM Customer Certificate Public Key). + """ + logger.info("CMD: [TrustProvisioning] OEM generate master share") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_GEN_MASTER_SHARE.tag, + oem_share_input_addr, + oem_share_input_size, + oem_enc_share_output_addr, + oem_enc_share_output_size, + oem_enc_master_share_output_addr, + oem_enc_master_share_output_size, + oem_cust_cert_puk_output_addr, + oem_cust_cert_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_oem_set_master_share( + self, + oem_share_input_addr: int, + oem_share_input_size: int, + oem_enc_master_share_input_addr: int, + oem_enc_master_share_input_size: int, + ) -> bool: + """Takes the entropy seed and the Encrypted OEM Master Share. + + :param oem_share_input_addr: The input buffer address + where the OEM Share(entropy seed) locates at + :param oem_share_input_size: The byte count of the OEM Share + :param oem_enc_master_share_input_addr: The input buffer address + where the Encrypted OEM Master Share locates at + :param oem_enc_master_share_input_size: The byte count of the Encrypted OEM Master Share + :return: False in case of any problem; True otherwise + """ + logger.info( + "CMD: [TrustProvisioning] Takes the entropy seed and the Encrypted OEM Master Share." + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_SET_MASTER_SHARE.tag, + oem_share_input_addr, + oem_share_input_size, + oem_enc_master_share_input_addr, + oem_enc_master_share_input_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_oem_get_cust_cert_dice_puk( + self, + oem_rkth_input_addr: int, + oem_rkth_input_size: int, + oem_cust_cert_dice_puk_output_addr: int, + oem_cust_cert_dice_puk_output_size: int, + ) -> Optional[int]: + """Creates the initial trust provisioning keys. + + :param oem_rkth_input_addr: The input buffer address where the OEM RKTH locates at + :param oem_rkth_input_size: The byte count of the OEM RKTH + :param oem_cust_cert_dice_puk_output_addr: The output buffer address where ROM writes the OEM Customer + Certificate Public Key for DICE to + :param oem_cust_cert_dice_puk_output_size: The output buffer size in byte + :return: The byte count of the OEM Customer Certificate Public Key for DICE + """ + logger.info("CMD: [TrustProvisioning] Creates the initial trust provisioning keys") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_GET_CUST_CERT_DICE_PUK.tag, + oem_rkth_input_addr, + oem_rkth_input_size, + oem_cust_cert_dice_puk_output_addr, + oem_cust_cert_dice_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def tp_hsm_store_key( + self, + key_type: int, + key_property: int, + key_input_addr: int, + key_input_size: int, + key_blob_output_addr: int, + key_blob_output_size: int, + ) -> Optional[List[int]]: + """Trust provisioning: OEM generate common keys. + + :param key_type: Key to generate (CKDFK, HKDFK, HMACK, CMACK, AESK, KUOK) + :param key_property: Bit 0: Key Size, 0 for 128bit, 1 for 256bit. + Bits 30-31: set key protection CSS mode. + :param key_input_addr: The input buffer address where the key locates at + :param key_input_size: The byte count of the key + :param key_blob_output_addr: The output buffer address where ROM writes the key blob to + :param key_blob_output_size: The output buffer size in byte + :return: Return header of the key blob + byte count of the key blob + (header is not included) from the device; None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] OEM generate common keys") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_STORE_KEY.tag, + key_type, + key_property, + key_input_addr, + key_input_size, + key_blob_output_addr, + key_blob_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_hsm_enc_blk( + self, + mfg_cust_mk_sk_0_blob_input_addr: int, + mfg_cust_mk_sk_0_blob_input_size: int, + kek_id: int, + sb3_header_input_addr: int, + sb3_header_input_size: int, + block_num: int, + block_data_addr: int, + block_data_size: int, + ) -> bool: + """Trust provisioning: Encrypt the given SB3 data block. + + :param mfg_cust_mk_sk_0_blob_input_addr: The input buffer address + where the CKDF Master Key Blob locates at + :param mfg_cust_mk_sk_0_blob_input_size: The byte count of the CKDF Master Key Blob + :param kek_id: The CKDF Master Key Encryption Key ID + (0x10: NXP_CUST_KEK_INT_SK, 0x11: NXP_CUST_KEK_EXT_SK) + :param sb3_header_input_addr: The input buffer address, + where the SB3 Header(block0) locates at + :param sb3_header_input_size: The byte count of the SB3 Header + :param block_num: The index of the block. Due to SB3 Header(block 0) is always unencrypted, + the index starts from block1 + :param block_data_addr: The buffer address where the SB3 data block locates at + :param block_data_size: The byte count of the SB3 data block + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: [TrustProvisioning] Encrypt the given SB3 data block") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_ENC_BLOCK.tag, + mfg_cust_mk_sk_0_blob_input_addr, + mfg_cust_mk_sk_0_blob_input_size, + kek_id, + sb3_header_input_addr, + sb3_header_input_size, + block_num, + block_data_addr, + block_data_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_hsm_enc_sign( + self, + key_blob_input_addr: int, + key_blob_input_size: int, + block_data_input_addr: int, + block_data_input_size: int, + signature_output_addr: int, + signature_output_size: int, + ) -> Optional[int]: + """Signs the given data. + + :param key_blob_input_addr: The input buffer address where signing key blob locates at + :param key_blob_input_size: The byte count of the signing key blob + :param block_data_input_addr: The input buffer address where the data locates at + :param block_data_input_size: The byte count of the data + :param signature_output_addr: The output buffer address where ROM writes the signature to + :param signature_output_size: The output buffer size in byte + :return: Return signature size; None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] HSM ENC SIGN") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_ENC_SIGN.tag, + key_blob_input_addr, + key_blob_input_size, + block_data_input_addr, + block_data_input_size, + signature_output_addr, + signature_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def wpc_get_id( + self, + wpc_id_blob_addr: int, + wpc_id_blob_size: int, + ) -> Optional[int]: + """Command used for harvesting device ID blob. + + :param wpc_id_blob_addr: Buffer address + :param wpc_id_blob_size: Buffer size + """ + logger.info("CMD: [TrustProvisioning] WPC GET ID") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_GET_ID.tag, + wpc_id_blob_addr, + wpc_id_blob_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def nxp_get_id( + self, + id_blob_addr: int, + id_blob_size: int, + ) -> Optional[int]: + """Command used for harvesting device ID blob during wafer test as part of RTS flow. + + :param id_blob_addr: address of ID blob defined by Round-trip trust provisioning specification. + :param id_blob_size: length of buffer in bytes + """ + logger.info("CMD: [TrustProvisioning] NXP GET ID") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.NXP_GET_ID.tag, + id_blob_addr, + id_blob_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def wpc_insert_cert( + self, + wpc_cert_addr: int, + wpc_cert_len: int, + ec_id_offset: int, + wpc_puk_offset: int, + ) -> Optional[int]: + """Command used for certificate validation before it is written into flash. + + This command does following things: + Extracts ECID and WPC PUK from certificate + Validates ECID and WPC PUK. If both are OK it returns success. Otherwise returns fail + + :param wpc_cert_addr: address of inserted certificate + :param wpc_cert_len: length in bytes of inserted certificate + :param ec_id_offset: offset to 72-bit ECID + :param wpc_puk_offset: WPC PUK offset from beginning of inserted certificate + """ + logger.info("CMD: [TrustProvisioning] WPC INSERT CERT") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_INSERT_CERT.tag, + wpc_cert_addr, + wpc_cert_len, + ec_id_offset, + wpc_puk_offset, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return 0 + return None + + def wpc_sign_csr( + self, + csr_tbs_addr: int, + csr_tbs_len: int, + signature_addr: int, + signature_len: int, + ) -> Optional[int]: + """Command used sign CSR data (TBS portion). + + :param csr_tbs_addr: address of CSR-TBS data + :param csr_tbs_len: length in bytes of CSR-TBS data + :param signature_addr: address where to store signature + :param signature_len: expected length of signature + :return: actual signature length + """ + logger.info("CMD: [TrustProvisioning] WPC SIGN CSR-TBS DATA") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_SIGN_CSR.tag, + csr_tbs_addr, + csr_tbs_len, + signature_addr, + signature_len, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_create_session( + self, + oem_seed_input_addr: int, + oem_seed_input_size: int, + oem_share_output_addr: int, + oem_share_output_size: int, + ) -> Optional[int]: + """Command used by OEM to provide it share to create the initial trust provisioning keys. + + :param oem_seed_input_addr: address of 128-bit entropy seed value provided by the OEM. + :param oem_seed_input_size: OEM seed size in bytes + :param oem_share_output_addr: A 128-bit encrypted token. + :param oem_share_output_size: size in bytes + """ + logger.info("CMD: [TrustProvisioning] DSC HSM CREATE SESSION") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_CREATE_SESSION.tag, + oem_seed_input_addr, + oem_seed_input_size, + oem_share_output_addr, + oem_share_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_enc_blk( + self, + sbx_header_input_addr: int, + sbx_header_input_size: int, + block_num: int, + block_data_addr: int, + block_data_size: int, + ) -> Optional[int]: + """Command used to encrypt the given block sliced by the nxpimage. + + This command is only supported after issuance of dsc_hsm_create_session. + + :param sbx_header_input_addr: SBx header containing file size, Firmware version and Timestamp data. + Except for hash digest of block 0, all other fields should be valid. + :param sbx_header_input_size: size of the header in bytes + :param block_num: Number of block + :param block_data_addr: Address of data block + :param block_data_size: Size of data block + """ + logger.info("CMD: [TrustProvisioning] DSC HSM ENC BLK") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_ENC_BLK.tag, + sbx_header_input_addr, + sbx_header_input_size, + block_num, + block_data_addr, + block_data_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_enc_sign( + self, + block_data_input_addr: int, + block_data_input_size: int, + signature_output_addr: int, + signature_output_size: int, + ) -> Optional[int]: + """Command used for signing the data buffer provided. + + This command is only supported after issuance of dsc_hsm_create_session. + + :param block_data_input_addr: Address of data buffer to be signed + :param block_data_input_size: Size of data buffer in bytes + :param signature_output_addr: Address to output signature data + :param signature_output_size: Size of the output signature data in bytes + """ + logger.info("CMD: [TrustProvisioning] DSC HSM ENC SIGN") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_ENC_SIGN.tag, + block_data_input_addr, + block_data_input_size, + signature_output_addr, + signature_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + +#################### +# Helper functions # +#################### + + +def _tp_sentinel_frame(command: int, args: List[int], tag: int = 0x17, version: int = 0) -> bytes: + """Prepare frame used by sentinel.""" + data = struct.pack("<4B", command, len(args), version, tag) + for item in args: + data += struct.pack(" int: + if memory_id > 255 or memory_id == 0: + return memory_id + logger.warning("Note: memoryId is not required when accessing mapped external memory") + return 0 diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py new file mode 100644 index 00000000..d365eb0e --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Various types of memory identifiers used in the MBoot module.""" + +from typing import List, Optional, cast + +from spsdk.utils.misc import size_fmt +from spsdk.utils.spsdk_enum import SpsdkEnum + +LEGACY_MEM_ID = { + "internal": "INTERNAL", + "qspi": "QSPI", + "fuse": "FUSE", + "ifr": "IFR0", + "semcnor": "SEMC_NOR", + "flexspinor": "FLEX-SPI-NOR", + "semcnand": "SEMC-NAND", + "spinand": "SPI-NAND", + "spieeprom": "SPI-MEM", + "i2ceeprom": "I2C-MEM", + "sdcard": "SD", + "mmccard": "MMC", +} + + +######################################################################################################################## +# McuBoot External Memory ID +######################################################################################################################## +class MemIdEnum(SpsdkEnum): + """McuBoot Memory Base class.""" + + @classmethod + def get_legacy_str(cls, key: str) -> Optional[int]: + """Converts legacy str to new enum key. + + :param key: str value of legacy enum + :return: new enum value + """ + new_key = LEGACY_MEM_ID.get(key) + return cast(int, cls.get_tag(new_key)) if new_key else None + + @classmethod + def get_legacy_int(cls, key: int) -> Optional[str]: + """Converts legacy int to new enum key. + + :param key: int value of legacy enum + :return: new enum value + """ + if isinstance(key, int): + new_value = cls.from_tag(key) + if new_value: + return [k for k, v in LEGACY_MEM_ID.items() if v == new_value.label][0] + + return None + + +class ExtMemId(MemIdEnum): + """McuBoot External Memory Property Tags.""" + + QUAD_SPI0 = (1, "QSPI", "Quad SPI Memory 0") + IFR = (4, "IFR0", "Nonvolatile information register 0 (only used by SB loader)") + FUSE = (4, "FUSE", "Nonvolatile information register 0 (only used by SB loader)") + SEMC_NOR = (8, "SEMC-NOR", "SEMC NOR Memory") + FLEX_SPI_NOR = (9, "FLEX-SPI-NOR", "Flex SPI NOR Memory") + SPIFI_NOR = (10, "SPIFI-NOR", "SPIFI NOR Memory") + FLASH_EXEC_ONLY = (16, "FLASH-EXEC", "Execute-Only region on internal Flash") + SEMC_NAND = (256, "SEMC-NAND", "SEMC NAND Memory") + SPI_NAND = (257, "SPI-NAND", "SPI NAND Memory") + SPI_NOR_EEPROM = (272, "SPI-MEM", "SPI NOR/EEPROM Memory") + I2C_NOR_EEPROM = (273, "I2C-MEM", "I2C NOR/EEPROM Memory") + SD_CARD = (288, "SD", "eSD/SD/SDHC/SDXC Memory Card") + MMC_CARD = (289, "MMC", "MMC/eMMC Memory Card") + + +class MemId(MemIdEnum): + """McuBoot Internal/External Memory Property Tags.""" + + INTERNAL_MEMORY = (0, "RAM/FLASH", "Internal RAM/FLASH (Used for the PRINCE configuration)") + QUAD_SPI0 = (1, "QSPI", "Quad SPI Memory 0") + IFR = (4, "IFR0", "Nonvolatile information register 0 (only used by SB loader)") + FUSE = (4, "FUSE", "Nonvolatile information register 0 (only used by SB loader)") + SEMC_NOR = (8, "SEMC-NOR", "SEMC NOR Memory") + FLEX_SPI_NOR = (9, "FLEX-SPI-NOR", "Flex SPI NOR Memory") + SPIFI_NOR = (10, "SPIFI-NOR", "SPIFI NOR Memory") + FLASH_EXEC_ONLY = (16, "FLASH-EXEC", "Execute-Only region on internal Flash") + SEMC_NAND = (256, "SEMC-NAND", "SEMC NAND Memory") + SPI_NAND = (257, "SPI-NAND", "SPI NAND Memory") + SPI_NOR_EEPROM = (272, "SPI-MEM", "SPI NOR/EEPROM Memory") + I2C_NOR_EEPROM = (273, "I2C-MEM", "I2C NOR/EEPROM Memory") + SD_CARD = (288, "SD", "eSD/SD/SDHC/SDXC Memory Card") + MMC_CARD = (289, "MMC", "MMC/eMMC Memory Card") + + +######################################################################################################################## +# McuBoot External Memory Property Tags +######################################################################################################################## + + +class ExtMemPropTags(SpsdkEnum): + """McuBoot External Memory Property Tags.""" + + INIT_STATUS = (0x00000000, "INIT_STATUS") + START_ADDRESS = (0x00000001, "START_ADDRESS") + SIZE_IN_KBYTES = (0x00000002, "SIZE_IN_KBYTES") + PAGE_SIZE = (0x00000004, "PAGE_SIZE") + SECTOR_SIZE = (0x00000008, "SECTOR_SIZE") + BLOCK_SIZE = (0x00000010, "BLOCK_SIZE") + + +class MemoryRegion: + """Base class for memory regions.""" + + def __init__(self, start: int, end: int) -> None: + """Initialize the memory region object. + + :param start: start address of region + :param end: end address of region + + """ + self.start = start + self.end = end + self.size = end - start + 1 + + def __repr__(self) -> str: + return f"Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + return f"0x{self.start:08X} - 0x{self.end:08X}; Total Size: {size_fmt(self.size)}" + + +class RamRegion(MemoryRegion): + """RAM memory regions.""" + + def __init__(self, index: int, start: int, size: int) -> None: + """Initialize the RAM memory region object. + + :param index: number of region + :param start: start address of region + :param size: size of region + + """ + super().__init__(start, start + size - 1) + self.index = index + + def __repr__(self) -> str: + return f"RAM Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + return f"Region {self.index}: {super().__str__()}" + + +class FlashRegion(MemoryRegion): + """Flash memory regions.""" + + def __init__(self, index: int, start: int, size: int, sector_size: int) -> None: + """Initialize the Flash memory region object. + + :param index: number of region + :param start: start address of region + :param size: size of region + :param sector_size: size of sector + + """ + super().__init__(start, start + size - 1) + self.index = index + self.sector_size = sector_size + + def __repr__(self) -> str: + return f"Flash Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + msg = f"Region {self.index}: {super().__str__()} Sector size: {size_fmt(self.sector_size)}" + return msg + + +class ExtMemRegion(MemoryRegion): + """External memory regions.""" + + def __init__(self, mem_id: int, raw_values: Optional[List[int]] = None) -> None: + """Initialize the external memory region object. + + :param mem_id: ID of the external memory + :param raw_values: List of integers representing the property + + """ + self.mem_id = mem_id + if not raw_values: + self.value = None + return + super().__init__(0, 0) + self.start_address = ( + raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None + ) + self.total_size = ( + raw_values[2] * 1024 if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag else None + ) + self.page_size = raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + self.sector_size = raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + self.block_size = raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None + self.value = raw_values[0] + + @property + def name(self) -> str: + """Get the name of external memory for given memory ID.""" + return ExtMemId.get_label(self.mem_id) + + def __repr__(self) -> str: + return f"EXT Memory region, name: {self.name}, start: {hex(self.start)}" + + def __str__(self) -> str: + if not self.value: + return "Not Configured" + info = f"Start Address = 0x{self.start_address:08X} " + if self.total_size: + info += f"Total Size = {size_fmt(self.total_size)} " + info += f"Page Size = {self.page_size} " + info += f"Sector Size = {self.sector_size} " + if self.block_size: + info += f"Block Size = {self.block_size} " + return info diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py new file mode 100644 index 00000000..2d68658d --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py @@ -0,0 +1,850 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Helper module for more human-friendly interpretation of the target device properties.""" + + +import ctypes +from copy import deepcopy +from typing import Callable, Dict, List, Optional, Tuple, Type, Union + +from spsdk.exceptions import SPSDKKeyError +from spsdk.mboot.exceptions import McuBootError +from spsdk.utils.misc import Endianness +from spsdk.utils.spsdk_enum import SpsdkEnum + +from .commands import CommandTag +from .error_codes import StatusCode +from .memories import ExtMemPropTags, MemoryRegion + + +######################################################################################################################## +# McuBoot helper functions +######################################################################################################################## +def size_fmt(value: Union[int, float], kibibyte: bool = True) -> str: + """Convert size value into string format. + + :param value: The raw value + :param kibibyte: True if 1024 Bytes represent 1kB or False if 1000 Bytes represent 1kB + :return: Stringified value + """ + base, suffix = [(1000.0, "B"), (1024.0, "iB")][kibibyte] + x = "B" + for x in ["B"] + [prefix + suffix for prefix in list("kMGTP")]: + if -base < value < base: + break + value /= base + + return f"{value} {x}" if x == "B" else f"{value:3.1f} {x}" + + +######################################################################################################################## +# McuBoot helper classes +######################################################################################################################## + + +class Version: + """McuBoot current and target version type.""" + + def __init__(self, *args: Union[str, int], **kwargs: int): + """Initialize the Version object. + + :raises McuBootError: Argument passed the not str not int + """ + self.mark = kwargs.get("mark", "K") + self.major = kwargs.get("major", 0) + self.minor = kwargs.get("minor", 0) + self.fixation = kwargs.get("fixation", 0) + if args: + if isinstance(args[0], int): + self.from_int(args[0]) + elif isinstance(args[0], str): + self.from_str(args[0]) + else: + raise McuBootError("Value must be 'str' or 'int' type !") + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, Version) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __lt__(self, obj: "Version") -> bool: + return self.to_int(True) < obj.to_int(True) + + def __le__(self, obj: "Version") -> bool: + return self.to_int(True) <= obj.to_int(True) + + def __gt__(self, obj: "Version") -> bool: + return self.to_int(True) > obj.to_int(True) + + def __ge__(self, obj: "Version") -> bool: + return self.to_int(True) >= obj.to_int(True) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.to_str() + + def from_int(self, value: int) -> None: + """Parse version data from raw int value. + + :param value: Raw integer input + """ + mark = (value >> 24) & 0xFF + self.mark = chr(mark) if 64 < mark < 91 else None # type: ignore + self.major = (value >> 16) & 0xFF + self.minor = (value >> 8) & 0xFF + self.fixation = value & 0xFF + + def from_str(self, value: str) -> None: + """Parse version data from string value. + + :param value: String representation input + """ + mark_major, minor, fixation = value.split(".") + if len(mark_major) > 1 and mark_major[0] not in "0123456789": + self.mark = mark_major[0] + self.major = int(mark_major[1:]) + else: + self.major = int(mark_major) + self.minor = int(minor) + self.fixation = int(fixation) + + def to_int(self, no_mark: bool = False) -> int: + """Get version value in raw integer format. + + :param no_mark: If True, return value without mark + :return: Integer representation + """ + value = self.major << 16 | self.minor << 8 | self.fixation + mark = 0 if no_mark or self.mark is None else ord(self.mark) << 24 # type: ignore + return value | mark + + def to_str(self, no_mark: bool = False) -> str: + """Get version value in readable string format. + + :param no_mark: If True, return value without mark + :return: String representation + """ + value = f"{self.major}.{self.minor}.{self.fixation}" + mark = "" if no_mark or self.mark is None else self.mark + return f"{mark}{value}" + + +######################################################################################################################## +# McuBoot Properties +######################################################################################################################## + +# fmt: off +class PropertyTag(SpsdkEnum): + """McuBoot Properties.""" + + LIST_PROPERTIES = (0x00, 'ListProperties', 'List Properties') + CURRENT_VERSION = (0x01, "CurrentVersion", "Current Version") + AVAILABLE_PERIPHERALS = (0x02, "AvailablePeripherals", "Available Peripherals") + FLASH_START_ADDRESS = (0x03, "FlashStartAddress", "Flash Start Address") + FLASH_SIZE = (0x04, "FlashSize", "Flash Size") + FLASH_SECTOR_SIZE = (0x05, "FlashSectorSize", "Flash Sector Size") + FLASH_BLOCK_COUNT = (0x06, "FlashBlockCount", "Flash Block Count") + AVAILABLE_COMMANDS = (0x07, "AvailableCommands", "Available Commands") + CRC_CHECK_STATUS = (0x08, "CrcCheckStatus", "CRC Check Status") + LAST_ERROR = (0x09, "LastError", "Last Error Value") + VERIFY_WRITES = (0x0A, "VerifyWrites", "Verify Writes") + MAX_PACKET_SIZE = (0x0B, "MaxPacketSize", "Max Packet Size") + RESERVED_REGIONS = (0x0C, "ReservedRegions", "Reserved Regions") + VALIDATE_REGIONS = (0x0D, "ValidateRegions", "Validate Regions") + RAM_START_ADDRESS = (0x0E, "RamStartAddress", "RAM Start Address") + RAM_SIZE = (0x0F, "RamSize", "RAM Size") + SYSTEM_DEVICE_IDENT = (0x10, "SystemDeviceIdent", "System Device Identification") + FLASH_SECURITY_STATE = (0x11, "FlashSecurityState", "Security State") + UNIQUE_DEVICE_IDENT = (0x12, "UniqueDeviceIdent", "Unique Device Identification") + FLASH_FAC_SUPPORT = (0x13, "FlashFacSupport", "Flash Fac. Support") + FLASH_ACCESS_SEGMENT_SIZE = (0x14, "FlashAccessSegmentSize", "Flash Access Segment Size",) + FLASH_ACCESS_SEGMENT_COUNT = (0x15, "FlashAccessSegmentCount", "Flash Access Segment Count",) + FLASH_READ_MARGIN = (0x16, "FlashReadMargin", "Flash Read Margin") + QSPI_INIT_STATUS = (0x17, "QspiInitStatus", "QuadSPI Initialization Status") + TARGET_VERSION = (0x18, "TargetVersion", "Target Version") + EXTERNAL_MEMORY_ATTRIBUTES = (0x19, "ExternalMemoryAttributes", "External Memory Attributes",) + RELIABLE_UPDATE_STATUS = (0x1A, "ReliableUpdateStatus", "Reliable Update Status") + FLASH_PAGE_SIZE = (0x1B, "FlashPageSize", "Flash Page Size") + IRQ_NOTIFIER_PIN = (0x1C, "IrqNotifierPin", "Irq Notifier Pin") + PFR_KEYSTORE_UPDATE_OPT = (0x1D, "PfrKeystoreUpdateOpt", "PFR Keystore Update Opt") + BYTE_WRITE_TIMEOUT_MS = (0x1E, "ByteWriteTimeoutMs", "Byte Write Timeout in ms") + FUSE_LOCKED_STATUS = (0x1F, "FuseLockedStatus", "Fuse Locked Status") + UNKNOWN = (0xFF, "Unknown", "Unknown property") + + +class PropertyTagKw45xx(SpsdkEnum): + """McuBoot Properties.""" + + VERIFY_ERASE = (0x0A, "VerifyErase", "Verify Erase") + BOOT_STATUS_REGISTER = (0x14, "BootStatusRegister", "Boot Status Register",) + FIRMWARE_VERSION = (0x15, "FirmwareVersion", "Firmware Version",) + FUSE_PROGRAM_VOLTAGE = (0x16, "FuseProgramVoltage", "Fuse Program Voltage") + +class PeripheryTag(SpsdkEnum): + """Tags representing peripherals.""" + + UART = (0x01, "UART", "UART Interface") + I2C_SLAVE = (0x02, "I2C-Slave", "I2C Slave Interface") + SPI_SLAVE = (0x04, "SPI-Slave", "SPI Slave Interface") + CAN = (0x08, "CAN", "CAN Interface") + USB_HID = (0x10, "USB-HID", "USB HID-Class Interface") + USB_CDC = (0x20, "USB-CDC", "USB CDC-Class Interface") + USB_DFU = (0x40, "USB-DFU", "USB DFU-Class Interface") + LIN = (0x80, "LIN", "LIN Interface") + + +class FlashReadMargin(SpsdkEnum): + """Scopes for flash read.""" + + NORMAL = (0, "NORMAL") + USER = (1, "USER") + FACTORY = (2, "FACTORY") + + +class PfrKeystoreUpdateOpt(SpsdkEnum): + """Options for PFR updating.""" + + KEY_PROVISIONING = (0, "KEY_PROVISIONING", "KeyProvisioning") + WRITE_MEMORY = (1, "WRITE_MEMORY", "WriteMemory") +# fmt: on + +######################################################################################################################## +# McuBoot Properties Values +######################################################################################################################## + + +class PropertyValueBase: + """Base class for property value.""" + + __slots__ = ("tag", "name", "desc") + + def __init__(self, tag: int, name: Optional[str] = None, desc: Optional[str] = None) -> None: + """Initialize the base of property. + + :param tag: Property tag, see: `PropertyTag` + :param name: Optional name for the property + :param desc: Optional description for the property + """ + self.tag = tag + self.name = name or PropertyTag.get_label(tag) or "" + self.desc = desc or PropertyTag.get_description(tag, "") + + def __str__(self) -> str: + return f"{self.desc} = {self.to_str()}" + + def to_str(self) -> str: + """Stringified representation of a property. + + Derived classes should implement this function. + + :return: String representation + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + +class IntValue(PropertyValueBase): + """Integer-based value property.""" + + __slots__ = ( + "value", + "_fmt", + ) + + def __init__(self, tag: int, raw_values: List[int], str_format: str = "dec") -> None: + """Initialize the integer-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param str_format: Format to display the value ('dec', 'hex', 'size') + """ + super().__init__(tag) + self._fmt = str_format + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer property representation.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + if self._fmt == "size": + str_value = size_fmt(self.value) + elif self._fmt == "hex": + str_value = f"0x{self.value:08X}" + elif self._fmt == "dec": + str_value = str(self.value) + elif self._fmt == "int32": + str_value = str(ctypes.c_int32(self.value).value) + else: + str_value = self._fmt.format(self.value) + return str_value + + +class BoolValue(PropertyValueBase): + """Boolean-based value property.""" + + __slots__ = ( + "value", + "_true_values", + "_false_values", + "_true_string", + "_false_string", + ) + + def __init__( + self, + tag: int, + raw_values: List[int], + true_values: Tuple[int] = (1,), + true_string: str = "YES", + false_values: Tuple[int] = (0,), + false_string: str = "NO", + ) -> None: + """Initialize the Boolean-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param true_values: Values representing 'True', defaults to (1,) + :param true_string: String representing 'True, defaults to 'YES' + :param false_values: Values representing 'False', defaults to (0,) + :param false_string: String representing 'False, defaults to 'NO' + """ + super().__init__(tag) + self._true_values = true_values + self._true_string = true_string + self._false_values = false_values + self._false_string = false_string + self.value = raw_values[0] + + def __bool__(self) -> bool: + return self.value in self._true_values + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + return self._true_string if self.value in self._true_values else self._false_string + + +class EnumValue(PropertyValueBase): + """Enumeration value property.""" + + __slots__ = ("value", "enum", "_na_msg") + + def __init__( + self, + tag: int, + raw_values: List[int], + enum: Type[SpsdkEnum], + na_msg: str = "Unknown Item", + ) -> None: + """Initialize the enumeration-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param enum: Enumeration to pick from + :param na_msg: Message to display if an item is not found in the enum + """ + super().__init__(tag) + self._na_msg = na_msg + self.enum = enum + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + try: + return self.enum.get_label(self.value) + except SPSDKKeyError: + return f"{self._na_msg}: {self.value}" + + +class VersionValue(PropertyValueBase): + """Version property class.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the Version-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = Version(raw_values[0]) + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value.to_int() + + def to_str(self) -> str: + """Get stringified property representation.""" + return self.value.to_str() + + +class DeviceUidValue(PropertyValueBase): + """Device UID value property.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the Version-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = b"".join( + [int.to_bytes(val, length=4, byteorder=Endianness.LITTLE.value) for val in raw_values] + ) + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return int.from_bytes(self.value, byteorder=Endianness.BIG.value) + + def to_str(self) -> str: + """Get stringified property representation.""" + return " ".join(f"{item:02X}" for item in self.value) + + +class ReservedRegionsValue(PropertyValueBase): + """Reserver Regions property.""" + + __slots__ = ("regions",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the ReserverRegion-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.regions: List[MemoryRegion] = [] + for i in range(0, len(raw_values), 2): + if raw_values[i + 1] == 0: + continue + self.regions.append(MemoryRegion(raw_values[i], raw_values[i + 1])) + + def __str__(self) -> str: + return f"{self.desc} =\n{self.to_str()}" + + def to_str(self) -> str: + """Get stringified property representation.""" + return "\n".join([f" Region {i}: {region}" for i, region in enumerate(self.regions)]) + + +class AvailablePeripheralsValue(PropertyValueBase): + """Available Peripherals property.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the AvailablePeripherals-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + return ", ".join( + [ + peripheral_tag.label + for peripheral_tag in PeripheryTag + if peripheral_tag.tag & self.value + ] + ) + + +class AvailableCommandsValue(PropertyValueBase): + """Available commands property.""" + + __slots__ = ("value",) + + @property + def tags(self) -> List[str]: + """List of tags representing Available commands.""" + return [ + cmd_tag.tag # type: ignore + for cmd_tag in CommandTag + if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value + ] + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the AvailableCommands-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def __contains__(self, item: int) -> bool: + return isinstance(item, int) and bool((1 << item - 1) & self.value) + + def to_str(self) -> str: + """Get stringified property representation.""" + return [ + cmd_tag.label # type: ignore + for cmd_tag in CommandTag + if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value + ] + + +class IrqNotifierPinValue(PropertyValueBase): + """IRQ notifier pin property.""" + + __slots__ = ("value",) + + @property + def pin(self) -> int: + """Number of the pin used for reporting IRQ.""" + return self.value & 0xFF + + @property + def port(self) -> int: + """Number of the port used for reporting IRQ.""" + return (self.value >> 8) & 0xFF + + @property + def enabled(self) -> bool: + """Indicates whether IRQ reporting is enabled.""" + return bool(self.value & (1 << 32)) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the IrqNotifierPin-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def __bool__(self) -> bool: + return self.enabled + + def to_str(self) -> str: + """Get stringified property representation.""" + return ( + f"IRQ Port[{self.port}], Pin[{self.pin}] is {'enabled' if self.enabled else 'disabled'}" + ) + + +class ExternalMemoryAttributesValue(PropertyValueBase): + """Attributes for external memories.""" + + __slots__ = ( + "value", + "mem_id", + "start_address", + "total_size", + "page_size", + "sector_size", + "block_size", + ) + + def __init__(self, tag: int, raw_values: List[int], mem_id: int = 0) -> None: + """Initialize the ExternalMemoryAttributes-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param mem_id: ID of the external memory + """ + super().__init__(tag) + self.mem_id = mem_id + self.start_address = ( + raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None + ) + self.total_size = ( + raw_values[2] * 1024 if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag else None + ) + self.page_size = raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + self.sector_size = raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + self.block_size = raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None + self.value = raw_values[0] + + def to_str(self) -> str: + """Get stringified property representation.""" + str_values = [] + if self.start_address is not None: + str_values.append(f"Start Address: 0x{self.start_address:08X}") + if self.total_size is not None: + str_values.append(f"Total Size: {size_fmt(self.total_size)}") + if self.page_size is not None: + str_values.append(f"Page Size: {size_fmt(self.page_size)}") + if self.sector_size is not None: + str_values.append(f"Sector Size: {size_fmt(self.sector_size)}") + if self.block_size is not None: + str_values.append(f"Block Size: {size_fmt(self.block_size)}") + return ", ".join(str_values) + + +class FuseLock: + """Fuse Lock.""" + + def __init__(self, index: int, locked: bool) -> None: + """Initialize object representing information about fuse lock. + + :param index: value of OTP index + :param locked: status of the lock, true if locked + """ + self.index = index + self.locked = locked + + def __str__(self) -> str: + status = "LOCKED" if self.locked else "UNLOCKED" + return f" FUSE{(self.index):03d}: {status}\r\n" + + +class FuseLockRegister: + """Fuse Lock Register.""" + + def __init__(self, value: int, index: int, start: int = 0) -> None: + """Initialize object representing the OTP Controller Program Locked Status. + + :param value: value of the register + :param index: index of the fuse + :param start: shift to the start of the register + + """ + self.value = value + self.index = index + self.msg = "" + self.bitfields: List[FuseLock] = [] + + shift = 0 + for _ in range(start, 32): + locked = (value >> shift) & 1 + self.bitfields.append(FuseLock(index + shift, bool(locked))) + shift += 1 + + def __str__(self) -> str: + """Get stringified property representation.""" + if self.bitfields: + for bitfield in self.bitfields: + self.msg += str(bitfield) + return f"\r\n{self.msg}" + + +class FuseLockedStatus(PropertyValueBase): + """Class representing FuseLocked registers.""" + + __slots__ = ("fuses",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the FuseLockedStatus property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.fuses: List[FuseLockRegister] = [] + idx = 0 + for count, val in enumerate(raw_values): + start = 0 + if count == 0: + start = 16 + self.fuses.append(FuseLockRegister(val, idx, start)) + idx += 32 + if count == 0: + idx -= 16 + + def to_str(self) -> str: + """Get stringified property representation.""" + msg = "\r\n" + for count, register in enumerate(self.fuses): + msg += f"OTP Controller Program Locked Status {count} Register: {register}" + return msg + + def get_fuses(self) -> List[FuseLock]: + """Get list of fuses bitfield objects. + + :return: list of FuseLockBitfield objects + """ + fuses = [] + for registers in self.fuses: + fuses.extend(registers.bitfields) + return fuses + + +######################################################################################################################## +# McuBoot property response parser +######################################################################################################################## + +PROPERTIES: Dict[PropertyTag, Dict] = { + PropertyTag.CURRENT_VERSION: {"class": VersionValue, "kwargs": {}}, + PropertyTag.AVAILABLE_PERIPHERALS: { + "class": AvailablePeripheralsValue, + "kwargs": {}, + }, + PropertyTag.FLASH_START_ADDRESS: { + "class": IntValue, + "kwargs": {"str_format": "hex"}, + }, + PropertyTag.FLASH_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.FLASH_SECTOR_SIZE: { + "class": IntValue, + "kwargs": {"str_format": "size"}, + }, + PropertyTag.FLASH_BLOCK_COUNT: {"class": IntValue, "kwargs": {"str_format": "dec"}}, + PropertyTag.AVAILABLE_COMMANDS: {"class": AvailableCommandsValue, "kwargs": {}}, + PropertyTag.CRC_CHECK_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown CRC Status code"}, + }, + PropertyTag.VERIFY_WRITES: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.LAST_ERROR: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.MAX_PACKET_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.RESERVED_REGIONS: {"class": ReservedRegionsValue, "kwargs": {}}, + PropertyTag.VALIDATE_REGIONS: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.RAM_START_ADDRESS: {"class": IntValue, "kwargs": {"str_format": "hex"}}, + PropertyTag.RAM_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.SYSTEM_DEVICE_IDENT: { + "class": IntValue, + "kwargs": {"str_format": "hex"}, + }, + PropertyTag.FLASH_SECURITY_STATE: { + "class": BoolValue, + "kwargs": { + "true_values": (0x00000000, 0x5AA55AA5), + "true_string": "UNSECURE", + "false_values": (0x00000001, 0xC33CC33C), + "false_string": "SECURE", + }, + }, + PropertyTag.UNIQUE_DEVICE_IDENT: {"class": DeviceUidValue, "kwargs": {}}, + PropertyTag.FLASH_FAC_SUPPORT: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.FLASH_ACCESS_SEGMENT_SIZE: { + "class": IntValue, + "kwargs": {"str_format": "size"}, + }, + PropertyTag.FLASH_ACCESS_SEGMENT_COUNT: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTag.FLASH_READ_MARGIN: { + "class": EnumValue, + "kwargs": {"enum": FlashReadMargin, "na_msg": "Unknown Margin"}, + }, + PropertyTag.QSPI_INIT_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.TARGET_VERSION: {"class": VersionValue, "kwargs": {}}, + PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES: { + "class": ExternalMemoryAttributesValue, + "kwargs": {"mem_id": None}, + }, + PropertyTag.RELIABLE_UPDATE_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.FLASH_PAGE_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.IRQ_NOTIFIER_PIN: {"class": IrqNotifierPinValue, "kwargs": {}}, + PropertyTag.PFR_KEYSTORE_UPDATE_OPT: { + "class": EnumValue, + "kwargs": {"enum": PfrKeystoreUpdateOpt, "na_msg": "Unknown"}, + }, + PropertyTag.BYTE_WRITE_TIMEOUT_MS: { + "class": IntValue, + "kwargs": {"str_format": "dec"}, + }, + PropertyTag.FUSE_LOCKED_STATUS: { + "class": FuseLockedStatus, + "kwargs": {}, + }, +} + +PROPERTIES_KW45XX = { + PropertyTagKw45xx.VERIFY_ERASE: { + "class": BoolValue, + "kwargs": {"true_string": "ENABLE", "false_string": "DISABLE"}, + }, + PropertyTagKw45xx.BOOT_STATUS_REGISTER: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTagKw45xx.FIRMWARE_VERSION: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTagKw45xx.FUSE_PROGRAM_VOLTAGE: { + "class": BoolValue, + "kwargs": { + "true_string": "Over Drive Voltage (2.5 V)", + "false_string": "Normal Voltage (1.8 V)", + }, + }, +} + +PROPERTIES_OVERRIDE = {"kw45xx": PROPERTIES_KW45XX, "k32w1xx": PROPERTIES_KW45XX} +PROPERTY_TAG_OVERRIDE = {"kw45xx": PropertyTagKw45xx, "k32w1xx": PropertyTagKw45xx} + + +def parse_property_value( + property_tag: int, + raw_values: List[int], + ext_mem_id: Optional[int] = None, + family: Optional[str] = None, +) -> Optional[PropertyValueBase]: + """Parse the property value received from the device. + + :param property_tag: Tag representing the property + :param raw_values: Data received from the device + :param ext_mem_id: ID of the external memory used to read the property, defaults to None + :param family: supported family + :return: Object representing the property + """ + assert isinstance(property_tag, int) + assert isinstance(raw_values, list) + properties_dict = deepcopy(PROPERTIES) + if family: + properties_dict.update(PROPERTIES_OVERRIDE[family]) # type: ignore + if property_tag not in list(properties_dict.keys()): + return None + property_value = next( + value for key, value in properties_dict.items() if key.tag == property_tag + ) + cls: Callable = property_value["class"] + kwargs: dict = property_value["kwargs"] + if "mem_id" in kwargs: + kwargs["mem_id"] = ext_mem_id + obj = cls(property_tag, raw_values, **kwargs) + if family: + property_tag_override = PROPERTY_TAG_OVERRIDE[family].from_tag(property_tag) + obj.name = property_tag_override.label + obj.desc = property_tag_override.description + return obj diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/__init__.py new file mode 100644 index 00000000..de8a3212 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Mboot Protocols.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py new file mode 100644 index 00000000..e62346fb --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""MBoot protocol base.""" +from spsdk.utils.interfaces.protocol.protocol_base import ProtocolBase + + +class MbootProtocolBase(ProtocolBase): + """MBoot protocol base class.""" + + allow_abort: bool = False + need_data_split: bool = True diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py new file mode 100644 index 00000000..5355c5f9 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Mboot bulk implementation.""" +import logging +from struct import pack, unpack_from +from typing import Optional, Union + +from spsdk.exceptions import SPSDKAttributeError +from spsdk.mboot.commands import CmdResponse, parse_cmd_response +from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from spsdk.mboot.protocol.base import MbootProtocolBase +from spsdk.utils.exceptions import SPSDKTimeoutError +from spsdk.utils.interfaces.commands import CmdPacketBase +from spsdk.utils.spsdk_enum import SpsdkEnum + + +class ReportId(SpsdkEnum): + """Report ID enum.""" + + CMD_OUT = (0x01, "CMD_OUT") + CMD_IN = (0x03, "CMD_IN") + DATA_OUT = (0x02, "DATA_OUT") + DATA_IN = (0x04, "DATA_IN") + + +logger = logging.getLogger(__name__) + + +class MbootBulkProtocol(MbootProtocolBase): + """Mboot Bulk protocol.""" + + def open(self) -> None: + """Open the interface.""" + self.device.open() + + def close(self) -> None: + """Close the interface.""" + self.device.close() + + @property + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + return self.device.is_opened + + def write_data(self, data: bytes) -> None: + """Encapsulate data into frames and send them to device. + + :param data: Data to be sent + """ + frame = self._create_frame(data, ReportId.DATA_OUT) + if self.allow_abort: + try: + abort_data = self.device.read(1024, timeout=10) + logger.debug(f"Read {len(abort_data)} bytes of abort data") + except Exception as e: + raise McuBootConnectionError(str(e)) from e + if abort_data: + logger.debug(f"{', '.join(f'{b:02X}' for b in abort_data)}") + raise McuBootDataAbortError() + self.device.write(frame) + + def write_command(self, packet: CmdPacketBase) -> None: + """Encapsulate command into frames and send them to device. + + :param packet: Command packet object to be sent + :raises SPSDKAttributeError: Command packed contains no data to be sent + """ + data = packet.to_bytes(padding=False) + if not data: + raise SPSDKAttributeError("Incorrect packet type") + frame = self._create_frame(data, ReportId.CMD_OUT) + self.device.write(frame) + + def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: + """Read data from device. + + :return: read data + :raises SPSDKTimeoutError: Timeout occurred + """ + data = self.device.read(1024) + if not data: + logger.error("Cannot read from HID device") + raise SPSDKTimeoutError() + return self._parse_frame(bytes(data)) + + def _create_frame(self, data: bytes, report_id: ReportId) -> bytes: + """Encode the USB packet. + + :param report_id: ID of the report (see: HID_REPORT) + :param data: Data to send + :return: Encoded bytes and length of the final report frame + """ + raw_data = pack("<2BH", report_id.tag, 0x00, len(data)) + raw_data += data + logger.debug(f"OUT[{len(raw_data)}]: {', '.join(f'{b:02X}' for b in raw_data)}") + return raw_data + + @staticmethod + def _parse_frame(raw_data: bytes) -> Union[CmdResponse, bytes]: + """Decodes the data read on USB interface. + + :param raw_data: Data received + :return: CmdResponse object or data read + :raises McuBootDataAbortError: Transaction aborted by target + """ + logger.debug(f"IN [{len(raw_data)}]: {', '.join(f'{b:02X}' for b in raw_data)}") + report_id, _, plen = unpack_from("<2BH", raw_data) + if plen == 0: + raise McuBootDataAbortError() + data = raw_data[4 : 4 + plen] + if report_id == ReportId.CMD_IN: + return parse_cmd_response(data) + return data diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py new file mode 100644 index 00000000..5510b1cc --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Mboot serial implementation.""" +import logging +import struct +import time +from contextlib import contextmanager +from typing import Generator, NamedTuple, Optional, Tuple, Union + +from crcmod.predefined import mkPredefinedCrcFun +from typing_extensions import Self + +from spsdk.exceptions import SPSDKAttributeError +from spsdk.mboot.commands import CmdResponse, parse_cmd_response +from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from spsdk.mboot.protocol.base import MbootProtocolBase +from spsdk.utils.interfaces.commands import CmdPacketBase +from spsdk.utils.misc import Endianness, Timeout +from spsdk.utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + + +class PingResponse(NamedTuple): + """Special type of response for Ping Command.""" + + version: int + options: int + crc: int + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse raw data into PingResponse object. + + :param data: bytes to be unpacked to PingResponse object + 4B version, 2B data, 2B CRC16 + :raises McuBootConnectionError: Received invalid ping response + :return: PingResponse + """ + try: + version, options, crc = struct.unpack(" int: + """Convert bytes into single integer. + + :param data: bytes to convert + :param little_endian: indicate byte ordering in data, defaults to True + :return: integer + """ + byte_order = Endianness.LITTLE if little_endian else Endianness.BIG + return int.from_bytes(data, byteorder=byte_order.value) + + +class MbootSerialProtocol(MbootProtocolBase): + """Mboot Serial protocol.""" + + FRAME_START_BYTE = 0x5A + FRAME_START_NOT_READY_LIST = [0x00] + PING_TIMEOUT_MS = 500 + MAX_PING_RESPONSE_DUMMY_BYTES = 50 + MAX_UART_OPEN_ATTEMPTS = 3 + protocol_version: int = 0 + options: int = 0 + + def open(self) -> None: + """Open the interface. + + :raises McuBootConnectionError: In any case of fail of UART open operation. + """ + for i in range(self.MAX_UART_OPEN_ATTEMPTS): + try: + self.device.open() + self._ping() + logger.debug(f"Interface opened after {i + 1} attempts.") + return + except TimeoutError as e: + # Closing may take up 30-40 seconds + self.close() + logger.debug(f"Timeout when pinging the device: {repr(e)}") + except McuBootConnectionError as e: + self.close() + logger.debug(f"Opening interface failed with: {repr(e)}") + except Exception as exc: + self.close() + raise McuBootConnectionError("UART Interface open operation fails.") from exc + raise McuBootConnectionError( + f"Cannot open UART interface after {self.MAX_UART_OPEN_ATTEMPTS} attempts." + ) + + def close(self) -> None: + """Close the interface.""" + self.device.close() + + @property + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + return self.device.is_opened + + def write_data(self, data: bytes) -> None: + """Encapsulate data into frames and send them to device. + + :param data: Data to be sent + """ + frame = self._create_frame(data, FPType.DATA) + self._send_frame(frame) + + def write_command(self, packet: CmdPacketBase) -> None: + """Encapsulate command into frames and send them to device. + + :param packet: Command packet object to be sent + :raises SPSDKAttributeError: Command packed contains no data to be sent + """ + data = packet.to_bytes(padding=False) + if not data: + raise SPSDKAttributeError("Incorrect packet type") + frame = self._create_frame(data, FPType.CMD) + self._send_frame(frame) + + def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: + """Read data from device. + + :return: read data + :raises McuBootDataAbortError: Indicates data transmission abort + :raises McuBootConnectionError: When received invalid CRC + """ + _, frame_type = self._read_frame_header() + _length = to_int(self._read(2)) + crc = to_int(self._read(2)) + if not _length: + self._send_ack() + raise McuBootDataAbortError() + data = self._read(_length) + self._send_ack() + calculated_crc = self._calc_frame_crc(data, frame_type) + if crc != calculated_crc: + raise McuBootConnectionError("Received invalid CRC") + if frame_type == FPType.CMD: + return parse_cmd_response(data) + return data + + def _read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Internal read, done mainly due BUSPAL, where this is overriden.""" + return self.device.read(length, timeout) + + def _send_ack(self) -> None: + """Send ACK command.""" + ack_frame = struct.pack(" None: + """Write frame to the device and wait for ack. + + :param data: Data to be send + """ + self.device.write(frame) + if wait_for_ack: + self._read_frame_header(FPType.ACK) + + def _create_frame(self, data: bytes, frame_type: FPType) -> bytes: + """Encapsulate data into frame.""" + crc = self._calc_frame_crc(data, frame_type.tag) + frame = struct.pack( + f" int: + """Calculate the CRC of a frame. + + :param data: frame data + :param frame_type: frame type + :return: calculated CRC + """ + crc_data = struct.pack( + f" int: + """Calculate CRC from the data. + + :param data: data to calculate CRC from + :return: calculated CRC + """ + crc_function = mkPredefinedCrcFun("xmodem") + return crc_function(data) + + def _read_frame_header(self, expected_frame_type: Optional[FPType] = None) -> Tuple[int, int]: + """Read frame header and frame type. Return them as tuple of integers. + + :param expected_frame_type: Check if the frame_type is exactly as expected + :return: Tuple of integers representing frame header and frame type + :raises McuBootDataAbortError: Target sens Data Abort frame + :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) + :raises McuBootConnectionError: When received invalid ACK + """ + assert isinstance(self.device.timeout, int) + timeout = Timeout(self.device.timeout, "ms") + while not timeout.overflow(): + header = to_int(self._read(1)) + if header not in self.FRAME_START_NOT_READY_LIST: + break + # This is workaround addressing SPI ISP issue on RT5/6xx when sometimes + # ACK frames and START BYTE frames are swapped, see SPSDK-1824 for more details + if header not in [self.FRAME_START_BYTE, FPType.ACK]: + raise McuBootConnectionError( + f"Received invalid frame header '{header:#X}' expected '{self.FRAME_START_BYTE:#X}'" + + "\nTry increasing the timeout, some operations might take longer" + ) + if header == FPType.ACK: + frame_type: int = header + else: + frame_type = to_int(self._read(1)) + if frame_type == FPType.ABORT: + raise McuBootDataAbortError() + if expected_frame_type: + if frame_type == self.FRAME_START_BYTE: + frame_type = header + if frame_type != expected_frame_type: + raise McuBootConnectionError( + f"received invalid ACK '{frame_type:#X}' expected '{expected_frame_type.tag:#X}'" + ) + return header, frame_type + + def _ping(self) -> None: + """Ping the target device, retrieve protocol version. + + :raises McuBootConnectionError: If the target device doesn't respond to ping + :raises McuBootConnectionError: If the start frame is not received + :raises McuBootConnectionError: If the header is invalid + :raises McuBootConnectionError: If the frame type is invalid + :raises McuBootConnectionError: If the ping response is not received + :raises McuBootConnectionError: If crc does not match + """ + with self.ping_timeout(timeout=self.PING_TIMEOUT_MS): + ping = struct.pack(" Generator[None, None, None]: + """Context manager for changing UART's timeout. + + :param timeout: New temporary timeout in milliseconds, defaults to PING_TIMEOUT_MS (500ms) + :return: Generator[None, None, None] + """ + assert isinstance(self.device.timeout, int) + context_timeout = min(timeout, self.device.timeout) + original_timeout = self.device.timeout + self.device.timeout = context_timeout + logger.debug(f"Setting timeout to {context_timeout} ms") + # driver needs to be reconfigured after timeout change, wait for a little while + time.sleep(0.005) + + yield + + self.device.timeout = original_timeout + logger.debug(f"Restoring timeout to {original_timeout} ms") + time.sleep(0.005) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py new file mode 100644 index 00000000..805ca4f5 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Helper module used for scanning the existing devices.""" +from typing import List, Optional + +from spsdk.exceptions import SPSDKError +from spsdk.mboot.protocol.base import MbootProtocolBase +from spsdk.utils.interfaces.scanner_helper import InterfaceParams, parse_plugin_config + + +def get_mboot_interface( + port: Optional[str] = None, + usb: Optional[str] = None, + sdio: Optional[str] = None, + buspal: Optional[str] = None, + lpcusbsio: Optional[str] = None, + plugin: Optional[str] = None, + timeout: int = 5000, +) -> MbootProtocolBase: + """Get appropriate interface. + + 'port', 'usb', 'sdio', 'lpcusbsio' parameters are mutually exclusive; one of them is required. + + :param port: name and speed of the serial port (format: name[,speed]), defaults to None + :param usb: PID,VID of the USB interface, defaults to None + :param sdio: SDIO path of the SDIO interface, defaults to None + :param buspal: buspal interface settings, defaults to None + :param timeout: timeout in milliseconds + :param lpcusbsio: LPCUSBSIO spi or i2c config string + :param plugin: Additional plugin to be used + :return: Selected interface instance + :raises SPSDKError: Only one of the appropriate interfaces must be specified + :raises SPSDKError: When SPSDK-specific error occurs + """ + # check that one and only one interface is defined + interface_params: List[InterfaceParams] = [] + plugin_params = parse_plugin_config(plugin) if plugin else ("Unknown", "") + interface_params.extend( + [ + InterfaceParams(identifier="usb", is_defined=bool(usb), params=usb), + InterfaceParams(identifier="uart", is_defined=bool(port and not buspal), params=port), + InterfaceParams( + identifier="buspal_spi", + is_defined=bool(port and buspal and "spi" in buspal), + params=port, + extra_params=buspal, + ), + InterfaceParams( + identifier="buspal_i2c", + is_defined=bool(port and buspal and "i2c" in buspal), + params=port, + extra_params=buspal, + ), + InterfaceParams( + identifier="usbsio_spi", + is_defined=bool(lpcusbsio and "spi" in lpcusbsio), + params=lpcusbsio, + ), + InterfaceParams( + identifier="usbsio_i2c", + is_defined=bool(lpcusbsio and "i2c" in lpcusbsio), + params=lpcusbsio, + ), + InterfaceParams(identifier="sdio", is_defined=bool(sdio), params=sdio), + InterfaceParams( + identifier=plugin_params[0], is_defined=bool(plugin), params=plugin_params[1] + ), + ] + ) + interface_params = [ifce for ifce in interface_params if ifce.is_defined] + if len(interface_params) == 0: + raise SPSDKError( + "One of '--port', '--usb', '--sdio', '--lpcusbsio' or '--plugin' must be specified." + ) + if len(interface_params) > 1: + raise SPSDKError( + "Only one of '--port', '--usb', '--sdio', '--lpcusbsio' or '--plugin must be specified." + ) + interface = MbootProtocolBase.get_interface(interface_params[0].identifier) + assert interface_params[0].params + devices = interface.scan_from_args( + params=interface_params[0].params, + extra_params=interface_params[0].extra_params, + timeout=timeout, + ) + if len(devices) == 0: + raise SPSDKError(f"Selected '{interface_params[0].identifier}' device not found.") + if len(devices) > 1: + raise SPSDKError( + f"Multiple '{interface_params[0].identifier}' devices found: {len(devices)}" + ) + return devices[0] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py new file mode 100644 index 00000000..5f008483 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Miscellaneous functions in SBFile module.""" + +from datetime import datetime, timezone +from typing import Any, Sequence, Union + +from spsdk.exceptions import SPSDKError +from spsdk.utils import misc + + +class SecBootBlckSize: + """Helper methods allowing to convert size to number of blocks and back. + + Note: The class is not intended to be instantiated + """ + + # Size of cipher block in bytes + BLOCK_SIZE = 16 + + @staticmethod + def is_aligned(size: int) -> bool: + """Whether size is aligned to cipher block size. + + :param size: given size in bytes + :return: True if yes, False otherwise + """ + return size % SecBootBlckSize.BLOCK_SIZE == 0 + + @staticmethod + def align(size: int) -> int: + """Align given size to block size. + + :param size: in bytes + :return: size aligned up to block size + """ + return misc.align(size, SecBootBlckSize.BLOCK_SIZE) + + @staticmethod + def to_num_blocks(size: int) -> int: + """Converts size to number of cipher blocks. + + :param size: to be converted, the size must be aligned to block boundary + :return: corresponding number of cipher blocks + :raises SPSDKError: Raised when size is not aligned to block boundary + """ + if not SecBootBlckSize.is_aligned(size): + raise SPSDKError( + f"Invalid size {size}, expected number aligned to BLOCK size {SecBootBlckSize.BLOCK_SIZE}" + ) + return size // SecBootBlckSize.BLOCK_SIZE + + @staticmethod + def align_block_fill_random(data: bytes) -> bytes: + """Align block size to cipher block size. + + :param data: to be aligned + :return: data aligned to cipher block size, filled with random values + """ + return misc.align_block_fill_random(data, SecBootBlckSize.BLOCK_SIZE) + + +# the type represents input formats for BcdVersion3 value, see BcdVersion3.to_version +BcdVersion3Format = Union["BcdVersion3", str] + + +class BcdVersion3: + """Version in format #.#.#, where # is BCD number (1-4 digits).""" + + # default value + DEFAULT = "999.999.999" + + @staticmethod + def _check_number(num: int) -> bool: + """Check given number is a valid version number. + + :param num: to be checked + :return: True if number format is valid + :raises SPSDKError: If number format is not valid + """ + if num < 0 or num > 0x9999: + raise SPSDKError("Invalid number range") + for index in range(4): + if (num >> 4 * index) & 0xF > 0x9: + raise SPSDKError("Invalid number, contains digit > 9") + return True + + @staticmethod + def _num_from_str(text: str) -> int: + """Converts BCD number from text to int. + + :param text: given string to be converted to a version number + :return: version number + :raises SPSDKError: If format is not valid + """ + if len(text) < 0 or len(text) > 4: + raise SPSDKError("Invalid text length") + result = int(text, 16) + BcdVersion3._check_number(result) + return result + + @staticmethod + def from_str(text: str) -> "BcdVersion3": + """Convert string to BcdVersion instance. + + :param text: version in format #.#.#, where # is 1-4 decimal digits + :return: BcdVersion3 instance + :raises SPSDKError: If format is not valid + """ + parts = text.split(".") + if len(parts) != 3: + raise SPSDKError("Invalid length") + major = BcdVersion3._num_from_str(parts[0]) + minor = BcdVersion3._num_from_str(parts[1]) + service = BcdVersion3._num_from_str(parts[2]) + return BcdVersion3(major, minor, service) + + @staticmethod + def to_version(input_version: BcdVersion3Format) -> "BcdVersion3": + """Convert different input formats into BcdVersion3 instance. + + :param input_version: either directly BcdVersion3 or string + :raises SPSDKError: Raises when the format is unsupported + :return: BcdVersion3 instance + """ + if isinstance(input_version, BcdVersion3): + return input_version + if isinstance(input_version, str): + return BcdVersion3.from_str(input_version) + raise SPSDKError("unsupported format") + + def __init__(self, major: int = 1, minor: int = 0, service: int = 0): + """Initialize BcdVersion3. + + :param major: number in BCD format, 1-4 decimal digits + :param minor: number in BCD format, 1-4 decimal digits + :param service: number in BCD format, 1-4 decimal digits + :raises SPSDKError: Invalid version + """ + if not all( + [ + BcdVersion3._check_number(major), + BcdVersion3._check_number(minor), + BcdVersion3._check_number(service), + ] + ): + raise SPSDKError("Invalid version") + self.major = major + self.minor = minor + self.service = service + + def __str__(self) -> str: + return f"{self.major:X}.{self.minor:X}.{self.service:X}" + + def __repr__(self) -> str: + return self.__class__.__name__ + ": " + self.__str__() + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, BcdVersion3) + and (self.major == other.major) + and (self.minor == other.minor) + and (self.service == other.service) + ) + + @property + def nums(self) -> Sequence[int]: + """Return array of version numbers: [major, minor, service].""" + return [self.major, self.minor, self.service] + + +def pack_timestamp(value: datetime) -> int: + """Converts datetime to millisecond since 1.1.2000. + + :param value: datetime to be converted + :return: number of milliseconds since 1.1.2000 00:00:00; 64-bit integer + :raises SPSDKError: When there is incorrect result of conversion + """ + assert isinstance(value, datetime) + start = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() + result = int((value.timestamp() - start) * 1000000) + if result < 0 or result > 0xFFFFFFFFFFFFFFFF: + raise SPSDKError("Incorrect result of conversion") + return result + + +def unpack_timestamp(value: int) -> datetime: + """Converts timestamp in milliseconds into datetime. + + :param value: number of milliseconds since 1.1.2000 00:00:00; 64-bit integer + :return: corresponding datetime + :raises SPSDKError: When there is incorrect result of conversion + """ + assert isinstance(value, int) + if value < 0 or value > 0xFFFFFFFFFFFFFFFF: + raise SPSDKError("Incorrect result of conversion") + start = int(datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() * 1000000) + return datetime.fromtimestamp((start + value) / 1000000) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/__init__.py new file mode 100644 index 00000000..02d575a2 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing SB2 and SB2.1 File.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_ebnf_grammar.txt b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_ebnf_grammar.txt new file mode 100644 index 00000000..50d8ba63 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_ebnf_grammar.txt @@ -0,0 +1,174 @@ +/* BD file grammar in EBNF form */ +command_file ::= pre_section_block* section_block* + +pre_section_block ::= options_block +| constants_block +| sources_block +| keyblob_block + +options_block ::= OPTIONS '{' option_def* '}' + +option_def ::= IDENT '=' const_expr ';' + +constants_block ::= CONSTANTS '{' constant_def* '}' + +constant_def ::= IDENT '=' bool_expr ';' + +sources_block ::= SOURCES '{' source_def* '}' + +source_def ::= IDENT '=' source_value ( '(' option_list? ')' )? ';' + +source_value ::= STRING_LITERAL +| EXTERN '(' int_const_expr ')' + +option_list ::= IDENT '=' const_expr (',' IDENT '=' const_expr )* + +keyblob_block ::= KEYBLOB '(' int_const_expr ')' '{' keyblob_contents '}' + +keyblob_contents ::= '(' option_list* ')' + +section_block ::= SECTION '(' int_const_expr section_options? ')' section_contents + +section_options ::= ';' option_list? + +section_contents ::= '{' statement* '}' +| '<=' source_name ';' + +statement ::= basic_stmt ';' +| from_stmt +| if_stmt +| keywrap_stmt + +basic_stmt ::= load_stmt +| load_ifr_stmt +| call_stmt +| jump_sp_stmt +| mode_stmt +| message_stmt +| erase_stmt +| enable_stmt +| reset_stmt +| encrypt_stmt +| keystore_stmt + +load_stmt ::= LOAD load_opt load_data load_target + +load_opt ::= IDENT +| int_const_expr +| empty + +load_data ::= int_const_expr +| STRING_LITERAL +| IDENT +| section_list +| section_list FROM IDENT +| BINARY_BLOB + +load_target ::= '>' '.' +| '>' address_or_range + +section_list ::= section_ref (',' section_ref )* + +section_ref ::= '~' SECTION_NAME +| SECTION_NAME + +erase_stmt ::= ERASE address_or_range +| ERASE ALL + +address_or_range ::= int_const_expr ( '..' int_const_expr)? + +symbol_ref ::= IDENT'?' ':' IDENT + +load_ifr_stmt ::= LOAD IFR int_const_expr '>' int_const_expr + +call_stmt ::= call_type call_target call_arg? + +call_type ::= CALL +| JUMP + +call_target ::= int_const_expr +| symbol_ref +| IDENT + +call_arg ::= '(' int_const_expr? ')' + +jump_sp_stmt ::= JUMP_SP int_const_expr call_target call_arg? + +from_stmt ::= FROM IDENT '{' in_from_stmt* '}' + +in_from_stmt ::= basic_stmt ';' +| if_stmt + +mode_stmt ::= MODE int_const_expr + +message_stmt ::= message_type STRING_LITERAL + +message_type ::= INFO +| WARNING +| ERROR + +keystore_stmt ::= KEYSTORE_TO_NV mem_opt address_or_range +| KEYSTORE_FROM_NV mem_opt address_or_range + +mem_opt ::= IDENT +| '@' int_const_expr +| empty + +if_stmt ::= IF bool_expr '{' statement* '}' else_stmt? + +else_stmt ::= ELSE '(' statement* ')' +| ELSE if_stmt + +keywrap_stmt ::= KEYWRAP '(' int_const_expr ')' '{' statement* '}' + +encrypt_stmt ::= ENCRYPT '(' int_const_expr ')' '{' statement* '}' + +enable_stmt ::= ENABLE AT_INT_LITERAL int_const_expr + +reset_stmt ::= RESET + +ver_check_stmt ::= VERSION_CHECK sec_or_nsec int_const_expr + +sec_or_nsec ::= SEC +| NSEC + +const_expr ::= STRING_LITERAL +| bool_expr + +int_const_expr ::= expr + +bool_expr ::= bool_expr '<' bool_expr +| bool_expr '<=' bool_expr +| bool_expr '>' bool_expr +| bool_expr '>=' bool_expr +| bool_expr '==' bool_expr +| bool_expr '!=' bool_expr +| bool_expr '&&' bool_expr +| bool_expr '||' bool_expr +| '(' bool_expr ')' +| int_const_expr +| '!' bool_expr +| DEFINED '(' IDENT ')' +| IDENT '(' source_name ')' + +expr ::= expr '+' expr +| expr '-' expr +| expr '*' expr +| expr '/' expr +| expr '%' expr +| expr '<<' expr +| expr '>>' expr +| expr '&' expr +| expr '|' expr +| expr '^' expr +| expr '.' INT_SIZE +| '(' expr ')' +| INT_LITERAL +| IDENT +| SYMBOL_REF +| unary_expr +| SIZEOF '(' SYMBOL_REF ')' +| SIZEOF '(' IDENT ')' + +unary_expr ::= '+' expr +| '-' expr diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_grammer.txt b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_grammer.txt new file mode 100644 index 00000000..51f8e7fd --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/bd_grammer.txt @@ -0,0 +1,210 @@ +/* BD file grammar in BNF form */ +command_file ::= pre_section_block section_block + +pre_section_block ::= pre_section_block options_block +| pre_section_block constants_block +| pre_section_block sources_block +| pre_section_block keyblob_block +| empty + +options_block ::= OPTIONS '{' option_def '}' + +option_def ::= option_def IDENT '=' const_expr ';' +| empty + +constants_block ::= CONSTANTS '{' constant_def '}' + +constant_def ::= constant_def IDENT '=' bool_expr ';' +| empty + +sources_block ::= SOURCES '{' source_def '}' + +source_def ::= source_def IDENT '=' source_value ';' +| source_def IDENT '=' source_value '(' source_attr_list ')' ';' +| empty + +source_value ::= STRING_LITERAL +| EXTERN '(' int_const_expr ')' + +source_attr_list ::= option_list +| empty + +option_list ::= IDENT '=' const_expr ',' option_list +| IDENT '=' const_expr + +keyblob_block ::= KEYBLOB '(' int_const_expr ')' '{' keyblob_contents '}' + +# ----------------------------- Original keyblob grammar ------------- +# keyblob_contents ::= keyblob_contents '(' keyblob_options_list ')' +# | empty + +# keyblob_options_list ::= keyblob_options +# | empty + +# keyblob_options ::= IDENT '=' const_expr ',' keyblob_options +# | IDENT '=' const_expr + +# ----------------------------- New keyblob grammar ------------------ +keyblob_contents ::= '(' keyblob_options ')' + +keyblob_options ::= IDENT '=' const_expr ',' keyblob_options +| IDENT '=' const_expr + +section_block ::= section_block SECTION '(' int_const_expr section_options ')' section_contents +| empty + +section_options ::= ';' option_list +| ';' +| empty + +section_contents ::= '{' statement '}' +| '<=' source_name ';' + +statement ::= statement basic_stmt ';' +| statement from_stmt +| statement if_stmt +| statement encrypt_block +| statement keywrap_block +| empty + +basic_stmt ::= load_stmt +| load_ifr_stmt +| call_stmt +| jump_sp_stmt +| mode_stmt +| message_stmt +| erase_stmt +| enable_stmt +| reset_stmt +| keystore_stmt + +load_stmt ::= LOAD load_opt load_data load_target + +load_opt ::= IDENT +| int_const_expr +| empty + +load_data ::= int_const_expr +| STRING_LITERAL +| SOURCE_NAME +| section_list +| section_list FROM SOURCE_NAME +| BINARY_BLOB + +load_target ::= '>' '.' +| '>' address_or_range +| empty + +section_list ::= section_list ',' section_ref +| section_ref + +section_ref ::= '~' SECTION_NAME +| SECTION_NAME + +erase_stmt ::= ERASE address_or_range +| ERASE ALL + +address_or_range ::= int_const_expr +| int_const_expr '..' int_const_expr + +symbol_ref ::= SOURCE_NAME'?' ':' IDENT + +load_ifr_stmt ::= LOAD IFR int_const_expr '>' int_const_expr + +call_stmt ::= call_type call_target call_arg + +call_type ::= CALL +| JUMP + +call_target ::= int_const_expr +| symbol_ref +| IDENT + +call_arg ::= '(' ')' +| '(' int_const_expr ')' +| empty + +jump_sp_stmt ::= JUMP_SP int_const_expr call_target call_arg + +from_stmt ::= FROM IDENT '{' in_from_stmt '}' + +in_from_stmt ::= in_from_stmt basic_stmt ';' +| in_from_stmt if_stmt +| empty + +mode_stmt ::= MODE int_const_expr + +messate_stmt ::= message_type STRING_LITERAL + +message_type ::= INFO +| WARNING +| ERROR + +keystore_stmt ::= KEYSTORE_TO_NV mem_opt address_or_range +| KEYSTORE_FROM_NV mem_opt address_or_range + +mem_opt ::= IDENT +| '@' int_const_expr +| empty + +if_stmt ::= IF bool_expr '{' statement '}' else_stmt + +else_stmt ::= ELSE '(' statement ')' +| ELSE if_stmt +| empty + +keywrap_block ::= KEYWRAP '(' int_const_expr ')' '{' LOAD BINARY_BLOB GT int_const_expr SEMICOLON '}' + +encrypt_block ::= ENCRYPT '(' int_const_expr ')' '{' load_stmt '}' + +enable_stmt ::= ENABLE AT_INT_LITERAL int_const_expr + +reset_stmt ::= RESET + +ver_check_stmt ::= VERSION_CHECK sec_or_nsec int_const_expr + +sec_or_nsec ::= SEC +| NSEC + +const_expr ::= STRING_LITERAL +| bool_expr + +int_const_expr ::= expr + +bool_expr ::= bool_expr '<' bool_expr +| bool_expr '<=' bool_expr +| bool_expr '>' bool_expr +| bool_expr '>=' bool_expr +| bool_expr '==' bool_expr +| bool_expr '!=' bool_expr +| bool_expr '&&' bool_expr +| bool_expr '||' bool_expr +| '(' bool_expr ')' +| int_const_expr +| '!' bool_expr +| DEFINED '(' IDENT ')' +| IDENT '(' source_name ')' + +expr ::= expr '+' expr +| expr '-' expr +| expr '*' expr +| expr '/' expr +| expr '%' expr +| expr '<<' expr +| expr '>>' expr +| expr '&' expr +| expr '|' expr +| expr '^' expr +| expr '.' INT_SIZE +| '(' expr ')' +| INT_LITERAL +| IDENT +| SYMBOL_REF +| unary_expr +| SIZEOF '(' SYMBOL_REF ')' +| SIZEOF '(' IDENT ')' + +unary_expr ::= '+' expr +| '-' expr + +empty ::= diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py new file mode 100644 index 00000000..61f5b0b2 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py @@ -0,0 +1,1030 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Commands used by SBFile module.""" +import math +from abc import abstractmethod +from struct import calcsize, pack, unpack_from +from typing import Mapping, Optional, Type + +from crcmod.predefined import mkPredefinedCrcFun +from typing_extensions import Self + +from spsdk.exceptions import SPSDKError +from spsdk.mboot.memories import ExtMemId +from spsdk.sbfile.misc import SecBootBlckSize +from spsdk.utils.abstract import BaseClass +from spsdk.utils.misc import Endianness +from spsdk.utils.spsdk_enum import SpsdkEnum + +######################################################################################################################## +# Constants +######################################################################################################################## + +DEVICE_ID_MASK = 0xFF +DEVICE_ID_SHIFT = 0 +GROUP_ID_MASK = 0xF00 +GROUP_ID_SHIFT = 8 + + +######################################################################################################################## +# Enums +######################################################################################################################## +class EnumCmdTag(SpsdkEnum): + """Command tags.""" + + NOP = (0x0, "NOP") + TAG = (0x1, "TAG") + LOAD = (0x2, "LOAD") + FILL = (0x3, "FILL") + JUMP = (0x4, "JUMP") + CALL = (0x5, "CALL") + ERASE = (0x7, "ERASE") + RESET = (0x8, "RESET") + MEM_ENABLE = (0x9, "MEM_ENABLE") + PROG = (0xA, "PROG") + FW_VERSION_CHECK = (0xB, "FW_VERSION_CHECK", "Check FW version fuse value") + WR_KEYSTORE_TO_NV = ( + 0xC, + "WR_KEYSTORE_TO_NV", + "Restore key-store restore to non-volatile memory", + ) + WR_KEYSTORE_FROM_NV = (0xD, "WR_KEYSTORE_FROM_NV", "Backup key-store from non-volatile memory") + + +class EnumSectionFlag(SpsdkEnum): + """Section flags.""" + + BOOTABLE = (0x0001, "BOOTABLE") + CLEARTEXT = (0x0002, "CLEARTEXT") + LAST_SECT = (0x8000, "LAST_SECT") + + +######################################################################################################################## +# Header Class +######################################################################################################################## +class CmdHeader(BaseClass): + """SBFile command header.""" + + FORMAT = "<2BH3L" + SIZE = calcsize(FORMAT) + + @property + def crc(self) -> int: + """Calculate CRC for the header data.""" + raw_data = self._raw_data(crc=0) + checksum = 0x5A + for i in range(1, self.SIZE): + checksum = (checksum + raw_data[i]) & 0xFF + return checksum + + def __init__(self, tag: int, flags: int = 0) -> None: + """Initialize header.""" + if tag not in EnumCmdTag.tags(): + raise SPSDKError("Incorrect command tag") + self.tag = tag + self.flags = flags + self.address = 0 + self.count = 0 + self.data = 0 + + def __repr__(self) -> str: + return f"SB2 Command header, TAG:{self.tag}" + + def __str__(self) -> str: + tag = ( + EnumCmdTag.get_label(self.tag) if self.tag in EnumCmdTag.tags() else f"0x{self.tag:02X}" + ) + return ( + f"tag={tag}, flags=0x{self.flags:04X}, " + f"address=0x{self.address:08X}, count=0x{self.count:08X}, data=0x{self.data:08X}" + ) + + def _raw_data(self, crc: int) -> bytes: + """Return raw data of the header with specified CRC. + + :param crc: value to be used + :return: binary representation of the header + """ + return pack(self.FORMAT, crc, self.tag, self.flags, self.address, self.count, self.data) + + def export(self) -> bytes: + """Export command header as bytes.""" + return self._raw_data(self.crc) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command header from bytes. + + :param data: Input data as bytes + :return: CMDHeader object + :raises SPSDKError: raised when size is incorrect + :raises SPSDKError: Raised when CRC is incorrect + """ + if calcsize(cls.FORMAT) > len(data): + raise SPSDKError("Incorrect size") + obj = cls(EnumCmdTag.NOP.tag) + (crc, obj.tag, obj.flags, obj.address, obj.count, obj.data) = unpack_from(cls.FORMAT, data) + if crc != obj.crc: + raise SPSDKError("CRC does not match") + return obj + + +######################################################################################################################## +# Commands Classes +######################################################################################################################## +class CmdBaseClass(BaseClass): + """Base class for all commands.""" + + # bit mask for device ID inside flags + ROM_MEM_DEVICE_ID_MASK = 0xFF00 + # shift for device ID inside flags + ROM_MEM_DEVICE_ID_SHIFT = 8 + # bit mask for group ID inside flags + ROM_MEM_GROUP_ID_MASK = 0xF0 + # shift for group ID inside flags + ROM_MEM_GROUP_ID_SHIFT = 4 + + def __init__(self, tag: EnumCmdTag) -> None: + """Initialize CmdBase.""" + self._header = CmdHeader(tag.tag) + + @property + def header(self) -> CmdHeader: + """Return command header.""" + return self._header + + @property + def raw_size(self) -> int: + """Return size of the command in binary format (including header).""" + return CmdHeader.SIZE # this is default implementation + + def __repr__(self) -> str: + return "Command: " + str(self._header) # default implementation: use command name + + def __str__(self) -> str: + """Return text info about the instance.""" + return repr(self) + "\n" # default implementation is same as __repr__ + + def export(self) -> bytes: + """Return object serialized into bytes.""" + return self._header.export() # default implementation + + +class CmdNop(CmdBaseClass): + """Command NOP class.""" + + def __init__(self) -> None: + """Initialize Command Nop.""" + super().__init__(EnumCmdTag.NOP) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: CMD Nop object + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.NOP: + raise SPSDKError("Incorrect header tag") + return cls() + + +class CmdTag(CmdBaseClass): + """Command TAG class. + + It is also used as header for boot section for SB file 1.x. + """ + + def __init__(self) -> None: + """Initialize Command Tag.""" + super().__init__(EnumCmdTag.TAG) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed instance + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.TAG: + raise SPSDKError("Incorrect header tag") + result = cls() + result._header = header + return result + + +class CmdLoad(CmdBaseClass): + """Command Load. The load statement is used to store data into the memory.""" + + @property + def address(self) -> int: + """Return address in target processor to load data.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Setter. + + :param value: address in target processor to load data + :raises SPSDKError: When there is incorrect address + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + @property + def raw_size(self) -> int: + """Return aligned size of the command including header and data.""" + size = CmdHeader.SIZE + len(self.data) + if size % CmdHeader.SIZE: + size += CmdHeader.SIZE - (size % CmdHeader.SIZE) + return size + + def __init__(self, address: int, data: bytes, mem_id: int = 0) -> None: + """Initialize CMD Load.""" + super().__init__(EnumCmdTag.LOAD) + assert isinstance(data, (bytes, bytearray)) + self.address = address + self.data = bytes(data) + self.mem_id = mem_id + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"LOAD: Address=0x{self.address:08X}, DataLen={len(self.data)}, " + f"Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + def export(self) -> bytes: + """Export command as binary.""" + self._update_data() + result = super().export() + return result + self.data + + def _update_data(self) -> None: + """Update command data.""" + # padding data + self.data = SecBootBlckSize.align_block_fill_random(self.data) + # update header + self._header.count = len(self.data) + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + self._header.data = crc32_function(self.data, 0xFFFFFFFF) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: CMD Load object + :raises SPSDKError: Raised when there is invalid CRC + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.LOAD: + raise SPSDKError("Incorrect header tag") + header_count = SecBootBlckSize.align(header.count) + cmd_data = data[CmdHeader.SIZE : CmdHeader.SIZE + header_count] + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + if header.data != crc32_function(cmd_data, 0xFFFFFFFF): + raise SPSDKError("Invalid CRC in the command header") + device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + obj = cls(header.address, cmd_data, mem_id) + obj.header.data = header.data + obj.header.flags = header.flags + obj._update_data() + return obj + + +class CmdFill(CmdBaseClass): + """Command Fill class.""" + + PADDING_VALUE = 0x00 + + @property + def address(self) -> int: + """Return address of the command Fill.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set address for the command Fill.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def raw_size(self) -> int: + """Calculate raw size of header.""" + size = CmdHeader.SIZE + size += len(self._pattern) - 4 + if size % CmdHeader.SIZE: + size += CmdHeader.SIZE - (size % CmdHeader.SIZE) + return size + + def __init__(self, address: int, pattern: int, length: Optional[int] = None) -> None: + """Initialize Command Fill. + + :param address: to write data + :param pattern: data to be written + :param length: length of data to be filled, defaults to 4 + :raises SPSDKError: Raised when size is not aligned to 4 bytes + """ + super().__init__(EnumCmdTag.FILL) + length = length or 4 + if length % 4: + raise SPSDKError("Length of memory range to fill must be a multiple of 4") + # if the pattern is a zero, the length is considered also as zero and the + # conversion to bytes produces empty byte "array", which is wrong, as + # zero should be converted to zero byte. Thus in case the pattern_len + # evaluates to 0, we set it to 1. + pattern_len = pattern.bit_length() / 8 or 1 + # We can get a number of 3 bytes, so we consider this as a word and set + # the length to 4 bytes with the first byte being zero. + if 3 == math.ceil(pattern_len): + pattern_len = 4 + pattern_bytes = pattern.to_bytes(math.ceil(pattern_len), Endianness.BIG.value) + # The pattern length is computed above, but as we transform the number + # into bytes, compute the len again just in case - a bit paranoid + # approach chosen. + if len(pattern_bytes) not in [1, 2, 4]: + raise SPSDKError("Pattern must be 1, 2 or 4 bytes long") + replicate = 4 // len(pattern_bytes) + final_pattern = replicate * pattern_bytes + self.address = address + self._pattern = final_pattern + # update header + self._header.data = unpack_from(">L", self._pattern)[0] + self._header.count = length + + @property + def pattern(self) -> bytes: + """Return binary data to fill.""" + return self._pattern + + def __str__(self) -> str: + return f"FILL: Address=0x{self.address:08X}, Pattern=" + " ".join( + f"{byte:02X}" for byte in self._pattern + ) + + def export(self) -> bytes: + """Return command in binary form (serialization).""" + # export cmd + data = super().export() + # export additional data + data = SecBootBlckSize.align_block_fill_random(data) + return data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Fill object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.FILL: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data, header.count) + + +class CmdJump(CmdBaseClass): + """Command Jump class.""" + + @property + def address(self) -> int: + """Return address of the command Jump.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set address of the command Jump.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def argument(self) -> int: + """Return command's argument.""" + return self._header.data + + @argument.setter + def argument(self, value: int) -> None: + """Set command's argument.""" + self._header.data = value + + @property + def spreg(self) -> Optional[int]: + """Return command's Stack Pointer.""" + if self._header.flags == 2: + return self._header.count + + return None + + @spreg.setter + def spreg(self, value: Optional[int] = None) -> None: + """Set command's Stack Pointer.""" + if value is None: + self._header.flags = 0 + self._header.count = 0 + else: + self._header.flags = 2 + self._header.count = value + + def __init__(self, address: int = 0, argument: int = 0, spreg: Optional[int] = None) -> None: + """Initialize Command Jump.""" + super().__init__(EnumCmdTag.JUMP) + self.address = address + self.argument = argument + self.spreg = spreg + + def __str__(self) -> str: + nfo = f"JUMP: Address=0x{self.address:08X}, Argument=0x{self.argument:08X}" + if self.spreg is not None: + nfo += f", SP=0x{self.spreg:08X}" + return nfo + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Jump object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.JUMP: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data, header.count if header.flags else None) + + +class CmdCall(CmdBaseClass): + """Command Call. + + The call statement is used for inserting a bootloader command that executes a function + from one of the files that are loaded into the memory. + """ + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def argument(self) -> int: + """Return command's argument.""" + return self._header.data + + @argument.setter + def argument(self, value: int) -> None: + """Set command's argument.""" + self._header.data = value + + def __init__(self, address: int = 0, argument: int = 0) -> None: + """Initialize Command Call.""" + super().__init__(EnumCmdTag.CALL) + self.address = address + self.argument = argument + + def __str__(self) -> str: + return f"CALL: Address=0x{self.address:08X}, Argument=0x{self.argument:08X}" + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Call object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.CALL: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data) + + +class CmdErase(CmdBaseClass): + """Command Erase class.""" + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def length(self) -> int: + """Return command's count.""" + return self._header.count + + @length.setter + def length(self, value: int) -> None: + """Set command's count.""" + self._header.count = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + def __init__(self, address: int = 0, length: int = 0, flags: int = 0, mem_id: int = 0) -> None: + """Initialize Command Erase.""" + super().__init__(EnumCmdTag.ERASE) + self.address = address + self.length = length + self.flags = flags + self.mem_id = mem_id + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"ERASE: Address=0x{self.address:08X}, Length={self.length}, Flags=0x{self.flags:08X}, " + f"MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Erase object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.ERASE: + raise SPSDKError("Invalid header tag") + device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + return cls(header.address, header.count, header.flags, mem_id) + + +class CmdReset(CmdBaseClass): + """Command Reset class.""" + + def __init__(self) -> None: + """Initialize Command Reset.""" + super().__init__(EnumCmdTag.RESET) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Cmd Reset object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.RESET: + raise SPSDKError("Invalid header tag") + return cls() + + +class CmdMemEnable(CmdBaseClass): + """Command to configure certain memory.""" + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + self._header.address = value + + @property + def size(self) -> int: + """Return command's size.""" + return self._header.count + + @size.setter + def size(self, value: int) -> None: + """Set command's size.""" + self._header.count = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + def __init__(self, address: int, size: int, mem_id: int): + """Initialize CmdMemEnable. + + :param address: source address with configuration data for memory initialization + :param size: size of configuration data used for memory initialization + :param mem_id: identification of memory + """ + super().__init__(EnumCmdTag.MEM_ENABLE) + self.address = address + self.mem_id = mem_id + self.size = size + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"MEM-ENABLE: Address=0x{self.address:08X}, Size={self.size}, " + f"Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Memory Enable object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.MEM_ENABLE: + raise SPSDKError("Invalid header tag") + device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + return cls(header.address, header.count, mem_id) + + +class CmdProg(CmdBaseClass): + """Command Program class.""" + + @property + def address(self) -> int: + """Return address in target processor to program data.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Setter. + + :param value: address in target processor to load data + :raises SPSDKError: When there is incorrect address + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = self.is_eight_byte + self._header.flags |= value + + @property + def data_word1(self) -> int: + """Return data word 1.""" + return self._header.count + + @data_word1.setter + def data_word1(self, value: int) -> None: + """Setter. + + :param value: first data word + :raises SPSDKError: When there is incorrect value + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect data word 1") + self._header.count = value + + @property + def data_word2(self) -> int: + """Return data word 2.""" + return self._header.data + + @data_word2.setter + def data_word2(self, value: int) -> None: + """Setter. + + :param value: second data word + :raises SPSDKError: When there is incorrect value + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect data word 2") + self._header.data = value + + def __init__( + self, address: int, mem_id: int, data_word1: int, data_word2: int = 0, flags: int = 0 + ) -> None: + """Initialize CMD Prog.""" + super().__init__(EnumCmdTag.PROG) + + if data_word2: + self.is_eight_byte = 1 + else: + self.is_eight_byte = 0 + + if mem_id < 0 or mem_id > 0xFF: + raise SPSDKError("Invalid ID of memory") + + self.address = address + self.data_word1 = data_word1 + self.data_word2 = data_word2 + self.mem_id = mem_id + self.flags = flags + + self.flags = (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (self.mem_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"PROG: Index=0x{self.address:08X}, DataWord1=0x{self.data_word1:08X}, " + f"DataWord2=0x{self.data_word2:08X}, Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.PROG: + raise SPSDKError("Invalid header tag") + mem_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + return cls(header.address, mem_id, header.count, header.data, header.flags) + + +class VersionCheckType(SpsdkEnum): + """Select type of the version check: either secure or non-secure firmware to be checked.""" + + SECURE_VERSION = (0, "SECURE_VERSION") + NON_SECURE_VERSION = (1, "NON_SECURE_VERSION") + + +class CmdVersionCheck(CmdBaseClass): + """FW Version Check command class. + + Validates version of secure or non-secure firmware. + The command fails if version is < expected. + """ + + def __init__(self, ver_type: VersionCheckType, version: int) -> None: + """Initialize CmdVersionCheck. + + :param ver_type: version check type, see `VersionCheckType` enum + :param version: to be checked + :raises SPSDKError: If invalid version check type + """ + super().__init__(EnumCmdTag.FW_VERSION_CHECK) + if ver_type not in VersionCheckType: + raise SPSDKError("Invalid version check type") + self.header.address = ver_type.tag + self.header.count = version + + @property + def type(self) -> VersionCheckType: + """Return type of the check version, see VersionCheckType enumeration.""" + return VersionCheckType.from_tag(self.header.address) + + @property + def version(self) -> int: + """Return minimal version expected.""" + return self.header.count + + def __str__(self) -> str: + return ( + f"CVER: Type={self.type.label}, Version={str(self.version)}, " + f"Flags=0x{self.header.flags:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.FW_VERSION_CHECK: + raise SPSDKError("Invalid header tag") + ver_type = VersionCheckType.from_tag(header.address) + version = header.count + return cls(ver_type, version) + + +class CmdKeyStoreBackupRestore(CmdBaseClass): + """Shared, abstract implementation for key-store backup and restore command.""" + + # bit mask for controller ID inside flags + ROM_MEM_DEVICE_ID_MASK = 0xFF00 + # shift for controller ID inside flags + ROM_MEM_DEVICE_ID_SHIFT = 8 + + @classmethod + @abstractmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def __init__(self, address: int, controller_id: ExtMemId): + """Initialize CmdKeyStoreBackupRestore. + + :param address: where to backup key-store or source for restoring key-store + :param controller_id: ID of the memory to backup key-store or source memory to load key-store back + :raises SPSDKError: If invalid address + :raises SPSDKError: If invalid id of memory + """ + super().__init__(self.cmd_id()) + if address < 0 or address > 0xFFFFFFFF: + raise SPSDKError("Invalid address") + self.header.address = address + if controller_id.tag < 0 or controller_id.tag > 0xFF: + raise SPSDKError("Invalid ID of memory") + self.header.flags = (self.header.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (controller_id.tag << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + self.header.count = ( + 4 # this is useless, but it is kept for backward compatibility with elftosb + ) + + @property + def address(self) -> int: + """Return address where to backup key-store or source for restoring key-store.""" + return self.header.address + + @property + def controller_id(self) -> int: + """Return controller ID of the memory to backup key-store or source memory to load key-store back.""" + return (self.header.flags & self.ROM_MEM_DEVICE_ID_MASK) >> self.ROM_MEM_DEVICE_ID_SHIFT + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse command from bytes. + + :param data: Input data as bytes + :return: CmdKeyStoreBackupRestore object + :raises SPSDKError: When there is invalid header tag + """ + header = CmdHeader.parse(data) + if header.tag != cls.cmd_id(): + raise SPSDKError("Invalid header tag") + address = header.address + controller_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + return cls(address, ExtMemId.from_tag(controller_id)) + + +class CmdKeyStoreBackup(CmdKeyStoreBackupRestore): + """Command to backup keystore from non-volatile memory.""" + + @classmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID for backup operation.""" + return EnumCmdTag.WR_KEYSTORE_FROM_NV + + +class CmdKeyStoreRestore(CmdKeyStoreBackupRestore): + """Command to restore keystore into non-volatile memory.""" + + @classmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID for restore operation.""" + return EnumCmdTag.WR_KEYSTORE_TO_NV + + +######################################################################################################################## +# Command parser from binary format +######################################################################################################################## +_CMD_CLASS: Mapping[EnumCmdTag, Type[CmdBaseClass]] = { + EnumCmdTag.NOP: CmdNop, + EnumCmdTag.TAG: CmdTag, + EnumCmdTag.LOAD: CmdLoad, + EnumCmdTag.FILL: CmdFill, + EnumCmdTag.JUMP: CmdJump, + EnumCmdTag.CALL: CmdCall, + EnumCmdTag.ERASE: CmdErase, + EnumCmdTag.RESET: CmdReset, + EnumCmdTag.MEM_ENABLE: CmdMemEnable, + EnumCmdTag.PROG: CmdProg, + EnumCmdTag.FW_VERSION_CHECK: CmdVersionCheck, + EnumCmdTag.WR_KEYSTORE_TO_NV: CmdKeyStoreRestore, + EnumCmdTag.WR_KEYSTORE_FROM_NV: CmdKeyStoreBackup, +} + + +def parse_command(data: bytes) -> CmdBaseClass: + """Parse SB 2.x command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: Raised when there is unsupported command provided + """ + header_tag = data[1] + for cmd_tag, cmd in _CMD_CLASS.items(): + if cmd_tag.tag == header_tag: + return cmd.parse(data) + raise SPSDKError(f"Unsupported command: {str(header_tag)}") + + +def get_device_id(mem_id: int) -> int: + """Get device ID from memory ID. + + :param mem_id: memory ID + :return: device ID + """ + return ((mem_id) & DEVICE_ID_MASK) >> DEVICE_ID_SHIFT + + +def get_group_id(mem_id: int) -> int: + """Get group ID from memory ID. + + :param mem_id: memory ID + :return: group ID + """ + return ((mem_id) & GROUP_ID_MASK) >> GROUP_ID_SHIFT + + +def get_memory_id(device_id: int, group_id: int) -> int: + """Get memory ID from device ID and group ID. + + :param device_id: device ID + :param group_id: group ID + :return: memory ID + """ + return (((group_id) << GROUP_ID_SHIFT) & GROUP_ID_MASK) | ( + ((device_id) << DEVICE_ID_SHIFT) & DEVICE_ID_MASK + ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py new file mode 100644 index 00000000..0e37d594 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Image header.""" + +from datetime import datetime +from struct import calcsize, pack, unpack_from +from typing import Optional + +from typing_extensions import Self + +from spsdk.crypto.rng import random_bytes +from spsdk.exceptions import SPSDKError +from spsdk.sbfile.misc import BcdVersion3, pack_timestamp, unpack_timestamp +from spsdk.utils.abstract import BaseClass +from spsdk.utils.misc import swap16 + + +######################################################################################################################## +# Image Header Class (Version SB2) +######################################################################################################################## +# pylint: disable=too-many-instance-attributes +class ImageHeaderV2(BaseClass): + """Image Header V2 class.""" + + FORMAT = "<16s4s4s2BH4I4H4sQ12HI4s" + SIZE = calcsize(FORMAT) + SIGNATURE1 = b"STMP" + SIGNATURE2 = b"sgtl" + + def __init__( + self, + version: str = "2.0", + product_version: str = "1.0.0", + component_version: str = "1.0.0", + build_number: int = 0, + flags: int = 0x08, + nonce: Optional[bytes] = None, + timestamp: Optional[datetime] = None, + ) -> None: + """Initialize Image Header Version 2.x. + + :param version: The image version value (default: 2.0) + :param product_version: The product version (default: 1.0.0) + :param component_version: The component version (default: 1.0.0) + :param build_number: The build number value (default: 0) + :param flags: The flags value (default: 0x08) + :param nonce: The NONCE value; None if TODO ???? + :param timestamp: value requested in the test; None to use current value + """ + self.nonce = nonce + self.version = version + self.flags = flags + self.image_blocks = 0 # will be updated from boot image + self.first_boot_tag_block = 0 + self.first_boot_section_id = 0 + self.offset_to_certificate_block = 0 # will be updated from boot image + self.header_blocks = 0 # will be calculated in the BootImage later + self.key_blob_block = 8 + self.key_blob_block_count = 5 + self.max_section_mac_count = 0 # will be calculated in the BootImage later + self.timestamp = ( + timestamp + if timestamp is not None + else datetime.fromtimestamp(int(datetime.now().timestamp())) + ) + self.product_version: BcdVersion3 = BcdVersion3.to_version(product_version) + self.component_version: BcdVersion3 = BcdVersion3.to_version(component_version) + self.build_number = build_number + + def __repr__(self) -> str: + return f"Header: v{self.version}, {self.image_blocks}" + + def flags_desc(self) -> str: + """Return flag description.""" + return "Signed" if self.flags == 0x8 else "Unsigned" + + def __str__(self) -> str: + """Get info of Header as string.""" + nfo = str() + nfo += f" Version: {self.version}\n" + if self.nonce is not None: + nfo += f" Digest: {self.nonce.hex().upper()}\n" + nfo += f" Flag: 0x{self.flags:X} ({self.flags_desc()})\n" + nfo += f" Image Blocks: {self.image_blocks}\n" + nfo += f" First Boot Tag Block: {self.first_boot_tag_block}\n" + nfo += f" First Boot SectionID: {self.first_boot_section_id}\n" + nfo += f" Offset to Cert Block: {self.offset_to_certificate_block}\n" + nfo += f" Key Blob Block: {self.key_blob_block}\n" + nfo += f" Header Blocks: {self.header_blocks}\n" + nfo += f" Sections MAC Count: {self.max_section_mac_count}\n" + nfo += f" Key Blob Block Count: {self.key_blob_block_count}\n" + nfo += f" Timestamp: {self.timestamp.strftime('%H:%M:%S (%d.%m.%Y)')}\n" + nfo += f" Product Version: {self.product_version}\n" + nfo += f" Component Version: {self.component_version}\n" + nfo += f" Build Number: {self.build_number}\n" + return nfo + + def export(self, padding: Optional[bytes] = None) -> bytes: + """Serialize object into bytes. + + :param padding: header padding 8 bytes (for testing purposes); None to use random value + :return: binary representation + :raises SPSDKError: Raised when format is incorrect + :raises SPSDKError: Raised when length of padding is incorrect + :raises SPSDKError: Raised when length of header is incorrect + """ + if not isinstance(self.nonce, bytes) or len(self.nonce) != 16: + raise SPSDKError("Format is incorrect") + major_version, minor_version = [int(v) for v in self.version.split(".")] + product_version_words = [swap16(v) for v in self.product_version.nums] + component_version_words = [swap16(v) for v in self.product_version.nums] + if padding is None: + padding = random_bytes(8) + else: + if len(padding) != 8: + raise SPSDKError("Invalid length of padding") + + result = pack( + self.FORMAT, + self.nonce, + # padding 8 bytes + padding, + self.SIGNATURE1, + # header version + major_version, + minor_version, + self.flags, + self.image_blocks, + self.first_boot_tag_block, + self.first_boot_section_id, + self.offset_to_certificate_block, + self.header_blocks, + self.key_blob_block, + self.key_blob_block_count, + self.max_section_mac_count, + self.SIGNATURE2, + pack_timestamp(self.timestamp), + # product version + product_version_words[0], + 0, + product_version_words[1], + 0, + product_version_words[2], + 0, + # component version + component_version_words[0], + 0, + component_version_words[1], + 0, + component_version_words[2], + 0, + self.build_number, + # padding[4] + padding[4:], + ) + if len(result) != self.SIZE: + raise SPSDKError("Invalid length of header") + return result + + # pylint: disable=too-many-locals + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialization from binary form. + + :param data: binary representation + :return: parsed instance of the header + :raises SPSDKError: Unable to parse data + """ + if cls.SIZE > len(data): + raise SPSDKError("Insufficient amount of data") + ( + nonce, + # padding0 + _, + signature1, + # header version + major_version, + minor_version, + flags, + image_blocks, + first_boot_tag_block, + first_boot_section_id, + offset_to_certificate_block, + header_blocks, + key_blob_block, + key_blob_block_count, + max_section_mac_count, + signature2, + raw_timestamp, + # product version + pv0, + _, + pv1, + _, + pv2, + _, + # component version + cv0, + _, + cv1, + _, + cv2, + _, + build_number, + # padding1 + _, + ) = unpack_from(cls.FORMAT, data) + + # check header signature 1 + if signature1 != cls.SIGNATURE1: + raise SPSDKError("SIGNATURE #1 doesn't match") + + # check header signature 2 + if signature2 != cls.SIGNATURE2: + raise SPSDKError("SIGNATURE #2 doesn't match") + + obj = cls( + version=f"{major_version}.{minor_version}", + flags=flags, + product_version=f"{swap16(pv0):X}.{swap16(pv1):X}.{swap16(pv2):X}", + component_version=f"{swap16(cv0):X}.{swap16(cv1):X}.{swap16(cv2):X}", + build_number=build_number, + ) + + obj.nonce = nonce + obj.image_blocks = image_blocks + obj.first_boot_tag_block = first_boot_tag_block + obj.first_boot_section_id = first_boot_section_id + obj.offset_to_certificate_block = offset_to_certificate_block + obj.header_blocks = header_blocks + obj.key_blob_block = key_blob_block + obj.key_blob_block_count = key_blob_block_count + obj.max_section_mac_count = max_section_mac_count + obj.timestamp = unpack_timestamp(raw_timestamp) + + return obj diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py new file mode 100644 index 00000000..f879e5fe --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py @@ -0,0 +1,1041 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Boot Image V2.0, V2.1.""" + +import logging +import os +from datetime import datetime +from typing import Any, Dict, Iterator, List, Optional + +from typing_extensions import Self + +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash +from spsdk.crypto.hmac import hmac +from spsdk.crypto.rng import random_bytes +from spsdk.crypto.signature_provider import ( + SignatureProvider, + get_signature_provider, + try_to_verify_public_key, +) +from spsdk.crypto.symmetric import Counter, aes_key_unwrap, aes_key_wrap +from spsdk.exceptions import SPSDKError +from spsdk.sbfile.misc import SecBootBlckSize +from spsdk.sbfile.sb2.sb_21_helper import SB21Helper +from spsdk.utils.abstract import BaseClass +from spsdk.utils.crypto.cert_blocks import CertBlockV1 +from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file +from spsdk.utils.misc import ( + find_first, + load_configuration, + load_hex_string, + load_text, + value_to_int, + write_file, +) +from spsdk.utils.schema_validator import CommentedConfig, check_config + +from . import sly_bd_parser as bd_parser +from .commands import CmdHeader +from .headers import ImageHeaderV2 +from .sections import BootSectionV2, CertSectionV2 + +logger = logging.getLogger(__name__) + + +class SBV2xAdvancedParams: + """The class holds advanced parameters for the SB file encryption. + + These parameters are used for the tests; for production, use can use default values (random keys + current time) + """ + + @staticmethod + def _create_nonce() -> bytes: + """Return random nonce.""" + nonce = bytearray(random_bytes(16)) + # clear nonce bit at offsets 31 and 63 + nonce[9] &= 0x7F + nonce[13] &= 0x7F + return bytes(nonce) + + def __init__( + self, + dek: Optional[bytes] = None, + mac: Optional[bytes] = None, + nonce: Optional[bytes] = None, + timestamp: Optional[datetime] = None, + ): + """Initialize SBV2xAdvancedParams. + + :param dek: DEK key + :param mac: MAC key + :param nonce: nonce + :param timestamp: fixed timestamp for the header; use None to use current date/time + :raises SPSDKError: Invalid dek or mac + :raises SPSDKError: Invalid length of nonce + """ + self._dek: bytes = dek if dek else random_bytes(32) + self._mac: bytes = mac if mac else random_bytes(32) + self._nonce: bytes = nonce if nonce else SBV2xAdvancedParams._create_nonce() + if timestamp is None: + timestamp = datetime.now() + self._timestamp = datetime.fromtimestamp(int(timestamp.timestamp())) + if len(self._dek) != 32 and len(self._mac) != 32: + raise SPSDKError("Invalid dek or mac") + if len(self._nonce) != 16: + raise SPSDKError("Invalid length of nonce") + + @property + def dek(self) -> bytes: + """Return DEK key.""" + return self._dek + + @property + def mac(self) -> bytes: + """Return MAC key.""" + return self._mac + + @property + def nonce(self) -> bytes: + """Return NONCE.""" + return self._nonce + + @property + def timestamp(self) -> datetime: + """Return timestamp.""" + return self._timestamp + + +######################################################################################################################## +# Secure Boot Image Class (Version 2.0) +######################################################################################################################## +class BootImageV20(BaseClass): + """Boot Image V2.0 class.""" + + # Image specific data + # size of the MAC key + HEADER_MAC_SIZE = 32 + # AES encrypted DEK and MAC, including padding + DEK_MAC_SIZE = 32 + 32 + 16 + + KEY_BLOB_SIZE = 80 + + def __init__( + self, + signed: bool, + kek: bytes, + *sections: BootSectionV2, + product_version: str = "1.0.0", + component_version: str = "1.0.0", + build_number: int = 0, + advanced_params: SBV2xAdvancedParams = SBV2xAdvancedParams(), + ) -> None: + """Initialize Secure Boot Image V2.0. + + :param signed: True if image is signed, False otherwise + :param kek: key for wrapping DEK and MAC keys + :param product_version: The product version (default: 1.0.0) + :param component_version: The component version (default: 1.0.0) + :param build_number: The build number value (default: 0) + :param advanced_params: Advanced parameters for encryption of the SB file, use for tests only + :param sections: Boot sections + :raises SPSDKError: Invalid dek or mac + """ + self._kek = kek + # Set Flags value + self._signed = signed + self.signature_provider: Optional[SignatureProvider] = None + flags = 0x08 if self.signed else 0x04 + # Set private attributes + self._dek: bytes = advanced_params.dek + self._mac: bytes = advanced_params.mac + if ( + len(self._dek) != self.HEADER_MAC_SIZE and len(self._mac) != self.HEADER_MAC_SIZE + ): # pragma: no cover # condition checked in SBV2xAdvancedParams constructor + raise SPSDKError("Invalid dek or mac") + self._header = ImageHeaderV2( + version="2.0", + product_version=product_version, + component_version=component_version, + build_number=build_number, + flags=flags, + nonce=advanced_params.nonce, + timestamp=advanced_params.timestamp, + ) + self._cert_section: Optional[CertSectionV2] = None + self._boot_sections: List[BootSectionV2] = [] + # Generate nonce + if self._header.nonce is None: + nonce = bytearray(random_bytes(16)) + # clear nonce bit at offsets 31 and 63 + nonce[9] &= 0x7F + nonce[13] &= 0x7F + self._header.nonce = bytes(nonce) + # Sections + for section in sections: + self.add_boot_section(section) + + @property + def header(self) -> ImageHeaderV2: + """Return image header.""" + return self._header + + @property + def dek(self) -> bytes: + """Data encryption key.""" + return self._dek + + @property + def mac(self) -> bytes: + """Message authentication code.""" + return self._mac + + @property + def kek(self) -> bytes: + """Return key for wrapping DEK and MAC keys.""" + return self._kek + + @property + def signed(self) -> bool: + """Check whether sb is signed + encrypted or only encrypted.""" + return self._signed + + @property + def cert_block(self) -> Optional[CertBlockV1]: + """Return certificate block; None if SB file not signed or block not assigned yet.""" + cert_sect = self._cert_section + if cert_sect is None: + return None + + return cert_sect.cert_block + + @cert_block.setter + def cert_block(self, value: Optional[CertBlockV1]) -> None: + """Setter. + + :param value: block to be assigned; None to remove previously assigned block + :raises SPSDKError: When certificate block is used when SB file is not signed + """ + if value is not None: + if not self.signed: + raise SPSDKError("Certificate block cannot be used unless SB file is signed") + self._cert_section = CertSectionV2(value) if value else None + + @property + def cert_header_size(self) -> int: + """Return image raw size (not aligned) for certificate header.""" + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + for boot_section in self._boot_sections: + size += boot_section.raw_size + return size + + @property + def raw_size_without_signature(self) -> int: + """Return image raw size without signature, used to calculate image blocks.""" + # Header, HMAC and KeyBlob + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + # Certificates Section + if self.signed: + size += self.DEK_MAC_SIZE + cert_block = self.cert_block + if not cert_block: + raise SPSDKError("Certification block not present") + size += cert_block.raw_size + # Boot Sections + for boot_section in self._boot_sections: + size += boot_section.raw_size + return size + + @property + def raw_size(self) -> int: + """Return image raw size.""" + size = self.raw_size_without_signature + + if self.signed: + cert_block = self.cert_block + if not cert_block: # pragma: no cover # already checked in raw_size_without_signature + raise SPSDKError("Certificate block not present") + size += cert_block.signature_size + + return size + + def __len__(self) -> int: + return len(self._boot_sections) + + def __getitem__(self, key: int) -> BootSectionV2: + return self._boot_sections[key] + + def __setitem__(self, key: int, value: BootSectionV2) -> None: + self._boot_sections[key] = value + + def __iter__(self) -> Iterator[BootSectionV2]: + return self._boot_sections.__iter__() + + def update(self) -> None: + """Update boot image.""" + if self._boot_sections: + self._header.first_boot_section_id = self._boot_sections[0].uid + # calculate first boot tag block + data_size = self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + if self._cert_section is not None: + data_size += self._cert_section.raw_size + self._header.first_boot_tag_block = SecBootBlckSize.to_num_blocks(data_size) + # ... + self._header.flags = 0x08 if self.signed else 0x04 + self._header.image_blocks = SecBootBlckSize.to_num_blocks(self.raw_size_without_signature) + self._header.header_blocks = SecBootBlckSize.to_num_blocks(self._header.SIZE) + self._header.max_section_mac_count = 0 + if self.signed: + self._header.offset_to_certificate_block = ( + self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + ) + self._header.offset_to_certificate_block += CmdHeader.SIZE + CertSectionV2.HMAC_SIZE * 2 + self._header.max_section_mac_count = 1 + for boot_sect in self._boot_sections: + boot_sect.is_last = True # this is unified with elftosb + self._header.max_section_mac_count += boot_sect.hmac_count + # Update certificates block header + cert_blk = self.cert_block + if cert_blk is not None: + cert_blk.header.build_number = self._header.build_number + cert_blk.header.image_length = self.cert_header_size + + def __repr__(self) -> str: + return f"SB2.0, {'Signed' if self.signed else 'Plain'} " + + def __str__(self) -> str: + """Return text description of the instance.""" + self.update() + nfo = "\n" + nfo += ":::::::::::::::::::::::::::::::::: IMAGE HEADER ::::::::::::::::::::::::::::::::::::::\n" + nfo += str(self._header) + if self._cert_section is not None: + nfo += "::::::::::::::::::::::::::::::: CERTIFICATES BLOCK ::::::::::::::::::::::::::::::::::::\n" + nfo += str(self._cert_section) + nfo += "::::::::::::::::::::::::::::::::::: BOOT SECTIONS ::::::::::::::::::::::::::::::::::::\n" + for index, section in enumerate(self._boot_sections): + nfo += f"[ SECTION: {index} | UID: 0x{section.uid:08X} ]\n" + nfo += str(section) + return nfo + + def add_boot_section(self, section: BootSectionV2) -> None: + """Add new Boot section into image. + + :param section: Boot section + :raises SPSDKError: Raised when section is not instance of BootSectionV2 class + :raises SPSDKError: Raised when boot section has duplicate UID + """ + if not isinstance(section, BootSectionV2): + raise SPSDKError("Section is not instance of BootSectionV2 class") + duplicate_uid = find_first(self._boot_sections, lambda bs: bs.uid == section.uid) + if duplicate_uid is not None: + raise SPSDKError(f"Boot section with duplicate UID: {str(section.uid)}") + self._boot_sections.append(section) + + def export(self, padding: Optional[bytes] = None) -> bytes: + """Serialize image object. + + :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) + :return: exported bytes + :raises SPSDKError: Raised when there are no boot sections or is not signed or private keys are missing + :raises SPSDKError: Raised when there is invalid dek or mac + :raises SPSDKError: Raised when certificate data is not present + :raises SPSDKError: Raised when there is invalid certificate block + :raises SPSDKError: Raised when there is invalid length of exported data + """ + if len(self.dek) != 32 or len(self.mac) != 32: + raise SPSDKError("Invalid dek or mac") + # validate params + if not self._boot_sections: + raise SPSDKError("No boot section") + if self.signed and (self._cert_section is None): + raise SPSDKError("Certificate section is required for signed images") + # update internals + self.update() + # Add Image Header data + data = self._header.export(padding=padding) + # Add Image Header HMAC data + data += hmac(self.mac, data) + # Add DEK and MAC keys + data += aes_key_wrap(self.kek, self.dek + self.mac) + # Add Padding + data += padding if padding else random_bytes(8) + # Add Certificates data + if not self._header.nonce: + raise SPSDKError("There is no nonce in the header") + counter = Counter(self._header.nonce) + counter.increment(SecBootBlckSize.to_num_blocks(len(data))) + if self._cert_section is not None: + cert_sect_bin = self._cert_section.export(dek=self.dek, mac=self.mac, counter=counter) + counter.increment(SecBootBlckSize.to_num_blocks(len(cert_sect_bin))) + data += cert_sect_bin + # Add Boot Sections data + for sect in self._boot_sections: + data += sect.export(dek=self.dek, mac=self.mac, counter=counter) + # Add Signature data + if self.signed: + if self.signature_provider is None: + raise SPSDKError("Signature provider is not assigned, cannot sign the image.") + if self.cert_block is None: + raise SPSDKError("Certificate block is not assigned.") + + public_key = self.cert_block.certificates[-1].get_public_key() + try_to_verify_public_key(self.signature_provider, public_key.export()) + data += self.signature_provider.get_signature(data) + + if len(data) != self.raw_size: + raise SPSDKError("Invalid length of exported data") + return data + + # pylint: disable=too-many-locals + @classmethod + def parse(cls, data: bytes, kek: bytes = bytes()) -> Self: + """Parse image from bytes. + + :param data: Raw data of parsed image + :param kek: The Key for unwrapping DEK and MAC keys (required) + :return: parsed image object + :raises SPSDKError: raised when header is in wrong format + :raises SPSDKError: raised when there is invalid header version + :raises SPSDKError: raised when signature is incorrect + :raises SPSDKError: Raised when kek is empty + :raises SPSDKError: raised when header's nonce is not present + """ + if not kek: + raise SPSDKError("kek cannot be empty") + index = 0 + header_raw_data = data[index : index + ImageHeaderV2.SIZE] + index += ImageHeaderV2.SIZE + header_mac_data = data[index : index + cls.HEADER_MAC_SIZE] + index += cls.HEADER_MAC_SIZE + key_blob = data[index : index + cls.KEY_BLOB_SIZE] + index += cls.KEY_BLOB_SIZE + key_blob_unwrap = aes_key_unwrap(kek, key_blob[:-8]) + dek = key_blob_unwrap[:32] + mac = key_blob_unwrap[32:] + header_mac_data_calc = hmac(mac, header_raw_data) + if header_mac_data != header_mac_data_calc: + raise SPSDKError("Invalid header MAC data") + # Parse Header + header = ImageHeaderV2.parse(header_raw_data) + if header.version != "2.0": + raise SPSDKError(f"Invalid Header Version: {header.version} instead 2.0") + image_size = header.image_blocks * 16 + # Initialize counter + if not header.nonce: + raise SPSDKError("Header's nonce not present") + counter = Counter(header.nonce) + counter.increment(SecBootBlckSize.to_num_blocks(index)) + # ... + signed = header.flags == 0x08 + adv_params = SBV2xAdvancedParams( + dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp + ) + obj = cls( + signed, + kek=kek, + product_version=str(header.product_version), + component_version=str(header.component_version), + build_number=header.build_number, + advanced_params=adv_params, + ) + # Parse Certificate section + if header.flags == 0x08: + cert_sect = CertSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) + obj._cert_section = cert_sect + index += cert_sect.raw_size + # Check Signature + if not cert_sect.cert_block.verify_data(data[image_size:], data[:image_size]): + raise SPSDKError("Parsing Certification section failed") + # Parse Boot Sections + while index < (image_size): + boot_section = BootSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) + obj.add_boot_section(boot_section) + index += boot_section.raw_size + return obj + + +######################################################################################################################## +# Secure Boot Image Class (Version 2.1) +######################################################################################################################## +class BootImageV21(BaseClass): + """Boot Image V2.1 class.""" + + # Image specific data + HEADER_MAC_SIZE = 32 + KEY_BLOB_SIZE = 80 + SHA_256_SIZE = 32 + + # defines + FLAGS_SHA_PRESENT_BIT = 0x8000 # image contains SHA-256 + FLAGS_ENCRYPTED_SIGNED_BIT = 0x0008 # image is signed and encrypted + + def __init__( + self, + kek: bytes, + *sections: BootSectionV2, + product_version: str = "1.0.0", + component_version: str = "1.0.0", + build_number: int = 0, + advanced_params: SBV2xAdvancedParams = SBV2xAdvancedParams(), + flags: int = FLAGS_SHA_PRESENT_BIT | FLAGS_ENCRYPTED_SIGNED_BIT, + ) -> None: + """Initialize Secure Boot Image V2.1. + + :param kek: key to wrap DEC and MAC keys + + :param product_version: The product version (default: 1.0.0) + :param component_version: The component version (default: 1.0.0) + :param build_number: The build number value (default: 0) + + :param advanced_params: optional advanced parameters for encryption; it is recommended to use default value + :param flags: see flags defined in class. + :param sections: Boot sections + """ + self._kek = kek + self.signature_provider: Optional[ + SignatureProvider + ] = None # this should be assigned for export, not needed for parsing + self._dek = advanced_params.dek + self._mac = advanced_params.mac + self._header = ImageHeaderV2( + version="2.1", + product_version=product_version, + component_version=component_version, + build_number=build_number, + flags=flags, + nonce=advanced_params.nonce, + timestamp=advanced_params.timestamp, + ) + self._cert_block: Optional[CertBlockV1] = None + self.boot_sections: List[BootSectionV2] = [] + # ... + for section in sections: + self.add_boot_section(section) + + @property + def header(self) -> ImageHeaderV2: + """Return image header.""" + return self._header + + @property + def dek(self) -> bytes: + """Data encryption key.""" + return self._dek + + @property + def mac(self) -> bytes: + """Message authentication code.""" + return self._mac + + @property + def kek(self) -> bytes: + """Return key to wrap DEC and MAC keys.""" + return self._kek + + @property + def cert_block(self) -> Optional[CertBlockV1]: + """Return certificate block; None if SB file not signed or block not assigned yet.""" + return self._cert_block + + @cert_block.setter + def cert_block(self, value: CertBlockV1) -> None: + """Setter. + + :param value: block to be assigned; None to remove previously assigned block + """ + assert isinstance(value, CertBlockV1) + self._cert_block = value + self._cert_block.alignment = 16 + + @property + def signed(self) -> bool: + """Return flag whether SB file is signed.""" + return True # SB2.1 is always signed + + @property + def cert_header_size(self) -> int: + """Return image raw size (not aligned) for certificate header.""" + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + size += self.KEY_BLOB_SIZE + # Certificates Section + cert_blk = self.cert_block + if cert_blk: + size += cert_blk.raw_size + return size + + @property + def raw_size(self) -> int: + """Return image raw size (not aligned).""" + # Header, HMAC and KeyBlob + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + size += self.KEY_BLOB_SIZE + # Certificates Section + cert_blk = self.cert_block + if cert_blk: + size += cert_blk.raw_size + if not self.signed: # pragma: no cover # SB2.1 is always signed + raise SPSDKError("Certificate block is not signed") + size += cert_blk.signature_size + # Boot Sections + for boot_section in self.boot_sections: + size += boot_section.raw_size + return size + + def __len__(self) -> int: + return len(self.boot_sections) + + def __getitem__(self, key: int) -> BootSectionV2: + return self.boot_sections[key] + + def __setitem__(self, key: int, value: BootSectionV2) -> None: + self.boot_sections[key] = value + + def __iter__(self) -> Iterator[BootSectionV2]: + return self.boot_sections.__iter__() + + def update(self) -> None: + """Update BootImageV21.""" + if self.boot_sections: + self._header.first_boot_section_id = self.boot_sections[0].uid + # calculate first boot tag block + data_size = self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + cert_blk = self.cert_block + if cert_blk is not None: + data_size += cert_blk.raw_size + if not self.signed: # pragma: no cover # SB2.1 is always signed + raise SPSDKError("Certificate block is not signed") + data_size += cert_blk.signature_size + self._header.first_boot_tag_block = SecBootBlckSize.to_num_blocks(data_size) + # ... + self._header.image_blocks = SecBootBlckSize.to_num_blocks(self.raw_size) + self._header.header_blocks = SecBootBlckSize.to_num_blocks(self._header.SIZE) + self._header.offset_to_certificate_block = ( + self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + ) + # Get HMAC count + self._header.max_section_mac_count = 0 + for boot_sect in self.boot_sections: + boot_sect.is_last = True # unified with elftosb + self._header.max_section_mac_count += boot_sect.hmac_count + # Update certificates block header + cert_clk = self.cert_block + if cert_clk is not None: + cert_clk.header.build_number = self._header.build_number + cert_clk.header.image_length = self.cert_header_size + + def __repr__(self) -> str: + return f"SB2.1, {'Signed' if self.signed else 'Plain'} " + + def __str__(self) -> str: + """Return text description of the instance.""" + self.update() + nfo = "\n" + nfo += ":::::::::::::::::::::::::::::::::: IMAGE HEADER ::::::::::::::::::::::::::::::::::::::\n" + nfo += str(self._header) + if self.cert_block is not None: + nfo += "::::::::::::::::::::::::::::::: CERTIFICATES BLOCK ::::::::::::::::::::::::::::::::::::\n" + nfo += str(self.cert_block) + nfo += "::::::::::::::::::::::::::::::::::: BOOT SECTIONS ::::::::::::::::::::::::::::::::::::\n" + for index, section in enumerate(self.boot_sections): + nfo += f"[ SECTION: {index} | UID: 0x{section.uid:08X} ]\n" + nfo += str(section) + return nfo + + def add_boot_section(self, section: BootSectionV2) -> None: + """Add new Boot section into image. + + :param section: Boot section to be added + :raises SPSDKError: Raised when section is not instance of BootSectionV2 class + """ + if not isinstance(section, BootSectionV2): + raise SPSDKError("Section is not instance of BootSectionV2 class") + self.boot_sections.append(section) + + # pylint: disable=too-many-locals + def export(self, padding: Optional[bytes] = None) -> bytes: + """Serialize image object. + + :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) + :return: exported bytes + :raises SPSDKError: Raised when there is no boot section to be added + :raises SPSDKError: Raised when certificate is not assigned + :raises SPSDKError: Raised when private key is not assigned + :raises SPSDKError: Raised when private header's nonce is invalid + :raises SPSDKError: Raised when private key does not match certificate + :raises SPSDKError: Raised when there is no debug info + """ + # validate params + if not self.boot_sections: + raise SPSDKError("At least one Boot Section must be added") + if self.cert_block is None: + raise SPSDKError("Certificate is not assigned") + if self.signature_provider is None: + raise SPSDKError("Signature provider is not assigned, cannot sign the image") + # Update internals + self.update() + # Export Boot Sections + bs_data = bytes() + bs_offset = ( + ImageHeaderV2.SIZE + + self.HEADER_MAC_SIZE + + self.KEY_BLOB_SIZE + + self.cert_block.raw_size + + self.cert_block.signature_size + ) + if self.header.flags & self.FLAGS_SHA_PRESENT_BIT: + bs_offset += self.SHA_256_SIZE + + if not self._header.nonce: + raise SPSDKError("Invalid header's nonce") + counter = Counter(self._header.nonce, SecBootBlckSize.to_num_blocks(bs_offset)) + for sect in self.boot_sections: + bs_data += sect.export(dek=self.dek, mac=self.mac, counter=counter) + # Export Header + signed_data = self._header.export(padding=padding) + # Add HMAC data + first_bs_hmac_count = self.boot_sections[0].hmac_count + hmac_data = bs_data[CmdHeader.SIZE : CmdHeader.SIZE + (first_bs_hmac_count * 32) + 32] + hmac_bytes = hmac(self.mac, hmac_data) + signed_data += hmac_bytes + # Add KeyBlob data + key_blob = aes_key_wrap(self.kek, self.dek + self.mac) + key_blob += b"\00" * (self.KEY_BLOB_SIZE - len(key_blob)) + signed_data += key_blob + # Add Certificates data + signed_data += self.cert_block.export() + # Add SHA-256 of Bootable sections if requested + if self.header.flags & self.FLAGS_SHA_PRESENT_BIT: + signed_data += get_hash(bs_data) + # Add Signature data + signature = self.signature_provider.get_signature(signed_data) + + return signed_data + signature + bs_data + + # pylint: disable=too-many-locals + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + kek: bytes = bytes(), + plain_sections: bool = False, + ) -> "BootImageV21": + """Parse image from bytes. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param kek: The Key for unwrapping DEK and MAC keys (required) + :param plain_sections: Sections are not encrypted; this is used only for debugging, + not supported by ROM code + :return: BootImageV21 parsed object + :raises SPSDKError: raised when header is in incorrect format + :raises SPSDKError: raised when signature is incorrect + :raises SPSDKError: Raised when kek is empty + :raises SPSDKError: raised when header's nonce not present" + """ + if not kek: + raise SPSDKError("kek cannot be empty") + index = offset + header_raw_data = data[index : index + ImageHeaderV2.SIZE] + index += ImageHeaderV2.SIZE + # Not used right now: hmac_data = data[index: index + cls.HEADER_MAC_SIZE] + index += cls.HEADER_MAC_SIZE + key_blob = data[index : index + cls.KEY_BLOB_SIZE] + index += cls.KEY_BLOB_SIZE + key_blob_unwrap = aes_key_unwrap(kek, key_blob[:-8]) + dek = key_blob_unwrap[:32] + mac = key_blob_unwrap[32:] + # Parse Header + header = ImageHeaderV2.parse(header_raw_data) + if header.offset_to_certificate_block != (index - offset): + raise SPSDKError("Invalid offset") + # Parse Certificate Block + cert_block = CertBlockV1.parse(data[index:]) + index += cert_block.raw_size + + # Verify Signature + signature_index = index + # The image may contain SHA, in such a case the signature is placed + # after SHA. Thus we must shift the index by SHA size. + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + signature_index += BootImageV21.SHA_256_SIZE + result = cert_block.verify_data( + data[signature_index : signature_index + cert_block.signature_size], + data[offset:signature_index], + ) + + if not result: + raise SPSDKError("Verification failed") + # Check flags, if 0x8000 bit is set, the SB file contains SHA-256 between + # certificate and signature. + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + bootable_section_sha256 = data[index : index + BootImageV21.SHA_256_SIZE] + index += BootImageV21.SHA_256_SIZE + index += cert_block.signature_size + # Check first Boot Section HMAC + # Not implemented yet + # hmac_data_calc = hmac(mac, data[index + CmdHeader.SIZE: index + CmdHeader.SIZE + ((2) * 32)]) + # if hmac_data != hmac_data_calc: + # raise SPSDKError("HMAC failed") + if not header.nonce: + raise SPSDKError("Header's nonce not present") + counter = Counter(header.nonce) + counter.increment(SecBootBlckSize.to_num_blocks(index - offset)) + boot_section = BootSectionV2.parse( + data, index, dek=dek, mac=mac, counter=counter, plain_sect=plain_sections + ) + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + computed_bootable_section_sha256 = get_hash( + data[index:], algorithm=EnumHashAlgorithm.SHA256 + ) + + if bootable_section_sha256 != computed_bootable_section_sha256: + raise SPSDKError( + desc=( + "Error: invalid Bootable section SHA." + f"Expected {bootable_section_sha256.decode('utf-8')}," + f"got {computed_bootable_section_sha256.decode('utf-8')}" + ) + ) + adv_params = SBV2xAdvancedParams( + dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp + ) + obj = cls( + kek=kek, + product_version=str(header.product_version), + component_version=str(header.component_version), + build_number=header.build_number, + advanced_params=adv_params, + ) + obj.cert_block = cert_block + obj.add_boot_section(boot_section) + return obj + + @staticmethod + def get_supported_families() -> List[str]: + """Return list of supported families. + + :return: List of supported families. + """ + return get_families(DatabaseManager.SB21) + + @classmethod + def get_commands_validation_schemas(cls, family: Optional[str] = None) -> List[Dict[str, Any]]: + """Create the list of validation schemas. + + :param family: Device family filter, if None all commands are returned. + :return: List of validation schemas. + """ + sb2_sch_cfg = get_schema_file(DatabaseManager.SB21) + + schemas: List[Dict[str, Any]] = [sb2_sch_cfg["sb2_sections"]] + if family: + db = get_db(family, "latest") + # remove unused command for current family + supported_commands = db.get_list(DatabaseManager.SB21, "supported_commands") + list_of_commands: List[Dict] = schemas[0]["properties"]["sections"]["items"][ + "properties" + ]["commands"]["items"]["oneOf"] + + schemas[0]["properties"]["sections"]["items"]["properties"]["commands"]["items"][ + "oneOf" + ] = [ + command + for command in list_of_commands + if list(command["properties"].keys())[0] in supported_commands + ] + + return schemas + + @classmethod + def get_validation_schemas(cls, family: Optional[str] = None) -> List[Dict[str, Any]]: + """Create the list of validation schemas. + + :param family: Device family + :return: List of validation schemas. + """ + sb2_schema = get_schema_file(DatabaseManager.SB21) + mbi_schema = get_schema_file(DatabaseManager.MBI) + + schemas: List[Dict[str, Any]] = [] + schemas.extend([mbi_schema[x] for x in ["signature_provider", "cert_block_v1"]]) + schemas.extend([sb2_schema[x] for x in ["sb2_output", "sb2_family", "common", "sb2"]]) + + add_keyblob = True + + if family: + add_keyblob = get_db(family, "latest").get_bool( + DatabaseManager.SB21, "keyblobs", default=True + ) + + if add_keyblob: + schemas.append(sb2_schema["keyblobs"]) + schemas.extend(cls.get_commands_validation_schemas(family)) + + # find family + for schema in schemas: + if "properties" in schema and "family" in schema["properties"]: + if family: + schema["properties"]["family"]["template_value"] = family + schema["properties"]["family"]["enum"] = cls.get_supported_families() + if family: + schema["properties"]["family"]["template_value"] = family + break + + return schemas + + @classmethod + def generate_config_template(cls, family: Optional[str]) -> str: + """Generate configuration template. + + :param family: Device family. + :return: Dictionary of individual templates (key is name of template, value is template itself). + """ + title = "Secure Binary v2.1 Configuration template" + if family in cls.get_supported_families(): + title += f" for {family}" + return CommentedConfig( + title, + cls.get_validation_schemas(family), + ).get_template() + + @classmethod + def parse_sb21_config( + cls, + config_path: str, + external_files: Optional[List[str]] = None, + ) -> Dict[Any, Any]: + """Create lexer and parser, load the BD file content and parse it. + + :param config_path: Path to configuration file either BD or YAML formatted. + :param external_files: Optional list of external files for BD processing + :return: Dictionary with parsed configuration. + """ + try: + bd_file_content = load_text(config_path) + parser = bd_parser.BDParser() + parsed_conf = parser.parse(text=bd_file_content, extern=external_files) + if parsed_conf is None: + raise SPSDKError("Invalid bd file, secure binary file generation terminated") + except SPSDKError: + parsed_conf = load_configuration(config_path) + config_dir = os.path.dirname(config_path) + family = parsed_conf.get("family") + schemas = BootImageV21.get_validation_schemas(family) + check_config(parsed_conf, schemas, search_paths=[config_dir]) + + return parsed_conf + + @classmethod + def load_from_config( + cls, + config: Dict[str, Any], + key_file_path: Optional[str] = None, + signature_provider: Optional[SignatureProvider] = None, + signing_certificate_file_paths: Optional[List[str]] = None, + root_key_certificate_paths: Optional[List[str]] = None, + rkth_out_path: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> "BootImageV21": + """Creates an instance of BootImageV21 from configuration. + + :param config: Input standard configuration. + :param key_file_path: path to key file. + :param signature_provider: Signature provider to sign final image + :param signing_certificate_file_paths: signing certificate chain. + :param root_key_certificate_paths: paths to root key certificate(s) for + verifying other certificates. Only 4 root key certificates are allowed, + others are ignored. One of the certificates must match the first certificate + passed in signing_certificate_file_paths. + :param rkth_out_path: output path to hash of hashes of root keys. If set to + None, 'hash.bin' is created under working directory. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Instance of Secure Binary V2.1 class + """ + flags = config["options"].get( + "flags", BootImageV21.FLAGS_SHA_PRESENT_BIT | BootImageV21.FLAGS_ENCRYPTED_SIGNED_BIT + ) + # Flags may be a hex string + flags = value_to_int(flags) + + product_version = config["options"].get("productVersion", "1.0.0") + component_version = config["options"].get("componentVersion", "1.0.0") + + if signing_certificate_file_paths and root_key_certificate_paths: + build_number = config["options"].get("buildNumber", 1) + cert_block = CertBlockV1(build_number=build_number) + for cert_path in signing_certificate_file_paths: + cert = Certificate.load(cert_path) + cert_block.add_certificate(cert) + for cert_idx, cert_path in enumerate(root_key_certificate_paths): + cert = Certificate.load(cert_path) + cert_block.set_root_key_hash(cert_idx, cert) + else: + cert_block = CertBlockV1.from_config(config, search_paths=search_paths) + + if key_file_path: + key = key_file_path + else: + key = config["containerKeyBlobEncryptionKey"] + + sb_kek = load_hex_string(key, expected_size=32, search_paths=search_paths) + + # validate keyblobs and perform appropriate actions + keyblobs = config.get("keyblobs", []) + + sb21_helper = SB21Helper(search_paths) + sb_sections = [] + sections = config["sections"] + for section_id, section in enumerate(sections): + commands = [] + for cmd in section["commands"]: + for key, value in cmd.items(): + # we use a helper function, based on the key ('load', 'erase' + # etc.) to create a command object. The helper function knows + # how to handle the parameters of each command. + cmd_fce = sb21_helper.get_command(key) + if key in ("keywrap", "encrypt"): + keyblob = {"keyblobs": keyblobs} + value.update(keyblob) + cmd = cmd_fce(value) + commands.append(cmd) + + sb_sections.append(BootSectionV2(section_id, *commands)) + + # We have a list of sections and their respective commands, lets create + # a boot image v2.1 object + secure_binary = BootImageV21( + sb_kek, + *sb_sections, + product_version=product_version, + component_version=component_version, + build_number=cert_block.header.build_number, + flags=flags, + ) + + # We have our secure binary, now we attach to it the certificate block and + # the private key content + secure_binary.cert_block = cert_block + + if not signature_provider: + signing_key_path = config.get("signPrivateKey", config.get("mainCertPrivateKeyFile")) + signature_provider = get_signature_provider( + sp_cfg=config.get("signProvider"), + local_file_key=signing_key_path, + search_paths=search_paths, + ) + + secure_binary.signature_provider = signature_provider + + if not rkth_out_path: + rkth_out_path = config.get("RKTHOutputPath", os.path.join(os.getcwd(), "hash.bin")) + assert isinstance(rkth_out_path, str), "Hash of hashes path must be string" + write_file(secure_binary.cert_block.rkth, rkth_out_path, mode="wb") + + return secure_binary diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py new file mode 100644 index 00000000..14fa8269 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module containing helper functions for nxpimage.""" + +import logging +import struct +from numbers import Number +from typing import Callable, Dict, List, Optional, Union + +from spsdk.exceptions import SPSDKError +from spsdk.mboot.memories import ExtMemId, MemId +from spsdk.sbfile.sb2.commands import ( + CmdBaseClass, + CmdErase, + CmdFill, + CmdJump, + CmdKeyStoreBackup, + CmdKeyStoreRestore, + CmdLoad, + CmdMemEnable, + CmdProg, + CmdVersionCheck, + VersionCheckType, +) +from spsdk.utils.crypto.otfad import KeyBlob +from spsdk.utils.misc import ( + align_block, + get_bytes_cnt_of_int, + load_binary, + swap32, + value_to_bytes, + value_to_int, +) + +logger = logging.getLogger(__name__) + + +class SB21Helper: + """SB21 Helper class.""" + + def __init__(self, search_paths: Optional[List[str]] = None): + """SB21 helper constructor.""" + self.search_paths = search_paths + self.cmds = { + "load": self._load, + "fill": self._fill_memory, + "erase": self._erase_cmd_handler, + "enable": self._enable, + "encrypt": self._encrypt, + "keywrap": self._keywrap, + "keystore_to_nv": self._keystore_to_nv, + "keystore_from_nv": self._keystore_from_nv, + "version_check": self._version_check, + "jump": self._jump, + "programFuses": self._prog, + } + + @staticmethod + def get_mem_id(mem_opt: Union[int, str]) -> int: + """Get memory ID from str or int in BD file. + + :param mem_opt: memory option in BD file + :raises SPSDKError: if memory option is not supported + :return: int memory ID + """ + if isinstance(mem_opt, int): + return mem_opt + if isinstance(mem_opt, str): + try: + return int(mem_opt, 0) + except ValueError: + mem_id = MemId.get_legacy_str(mem_opt) + if mem_id: + return mem_id + raise SPSDKError(f"Unsupported memory option: {mem_opt}") + + def get_command(self, cmd_name: str) -> Callable[[Dict], CmdBaseClass]: + """Returns a function based on input argument name. + + The json file generated by bd file parser uses command names (load, fill, + etc.). These names are used to get the proper function name, which creates + corresponding object. + + :param cmd_name: one of 'load', 'fill', 'erase', 'enable', 'reset', 'encrypt', + 'keywrap' + :return: appropriate Command object + """ + command_object = self.cmds[cmd_name] + return command_object + + def _fill_memory(self, cmd_args: dict) -> CmdFill: + """Returns a CmdFill object initialized based on cmd_args. + + Fill is a type of load command used for filling a region of memory with pattern. + + Example: + section(0) { + // pattern fill + load 0x55.b > 0x2000..0x3000; + // load two bytes at an address + load 0x1122.h > 0xf00; + } + + :param cmd_args: dictionary holding address and pattern + :return: CmdFill object + """ + address = value_to_int(cmd_args["address"]) + pattern = value_to_int(cmd_args["pattern"]) + return CmdFill(address=address, pattern=pattern) + + def _load(self, cmd_args: dict) -> Union[CmdLoad, CmdProg]: + """Returns a CmdLoad object initialized based on cmd_args. + + The load statement is used to store data into the memory. + The load command is also used to write to the flash memory. + When loading to the flash memory, the region being loaded to must be erased before to the load operation. + The most common form of a load statement is loading a source file by name. + Only plain binary images are supported. + + Example: + section (0) { + // load an entire binary file to an address + load myBinFile > 0x70000000; + // load an eight byte blob + load {{ ff 2e 90 07 77 5f 1d 20 }} > 0xa0000000; + // 4 byte load IFR statement + load ifr 0x1234567 > 0x30; + // Program fuse statement + load fuse {{00 00 00 01}} > 0x01000188; + // load to sdcard + load sdcard {{aa bb cc dd}} > 0x08000188; + load @288 {{aa bb cc dd}} > 0x08000188; + } + + :param cmd_args: dictionary holding path to file or values and address + :raises SPSDKError: If dict doesn't contain 'file' or 'values' key + :return: CmdLoad object + """ + prog_mem_id = 4 + address = value_to_int(cmd_args["address"]) + load_opt = cmd_args.get("load_opt") + mem_id = 0 + if load_opt: + mem_id = self.get_mem_id(load_opt) + + # general non-authenticated load command + if cmd_args.get("file"): + data = load_binary(cmd_args["file"], self.search_paths) + return CmdLoad(address=address, data=data, mem_id=mem_id) + if cmd_args.get("values"): + # if the memory ID is fuse or IFR change load command to program command + if mem_id == prog_mem_id: + return self._prog(cmd_args) + + values = [int(s, 16) for s in cmd_args["values"].split(",")] + if max(values) > 0xFFFFFFFF or min(values) < 0: + raise SPSDKError( + f"Invalid values for load command, values: {(values)}" + + ", expected unsigned 32bit comma separated values" + ) + data = struct.pack(f"<{len(values)}L", *values) + return CmdLoad(address=address, data=data, mem_id=mem_id) + if cmd_args.get("pattern"): + # if the memory ID is fuse or IFR change load command to program command + # pattern in this case represents 32b int data word 1 + if mem_id == prog_mem_id: + return self._prog(cmd_args) + + raise SPSDKError(f"Unsupported LOAD command args: {cmd_args}") + + def _prog(self, cmd_args: dict) -> CmdProg: + """Returns a CmdProg object initialized based on cmd_args. + + :param cmd_args: dictionary holding path to file or values and address + :raises SPSDKError: If data words are wrong + :return: CmdProg object + """ + address = value_to_int(cmd_args["address"]) + mem_id = self.get_mem_id(cmd_args.get("load_opt", 4)) + data_word1 = 0 + data_word2 = 0 + # values provided as binary blob {{aa bb cc dd}} either 4 or 8 bytes: + if cmd_args.get("values"): + int_value = int(cmd_args["values"], 16) + byte_count = get_bytes_cnt_of_int(int_value) + + if byte_count <= 4: + data_word1 = int_value + elif byte_count <= 8: + data_words = value_to_bytes(int_value, byte_cnt=8) + data_word1 = value_to_int(data_words[:4]) + data_word2 = value_to_int(data_words[4:]) + else: + raise SPSDKError("Program operation requires 4 or 8 byte segment") + + # swap byte order + data_word1 = swap32(data_word1) + data_word2 = swap32(data_word2) + + # values provided as integer e.g. 0x1000 represents data_word1 + elif cmd_args.get("pattern"): + int_value = value_to_int(cmd_args["pattern"]) + byte_count = get_bytes_cnt_of_int(int_value) + + if byte_count <= 4: + data_word1 = int_value + else: + raise SPSDKError("Data word 1 must be 4 bytes long") + else: + raise SPSDKError("Unsupported program command arguments") + + return CmdProg(address=address, data_word1=data_word1, data_word2=data_word2, mem_id=mem_id) + + def _erase_cmd_handler(self, cmd_args: dict) -> CmdErase: + """Returns a CmdErase object initialized based on cmd_args. + + The erase statement inserts a bootloader command to erase the flash memory. + There are two forms of the erase statement. The simplest form (erase all) + creates a command that erases the available flash memory. + The actual effect of this command depends on the runtime settings + of the bootloader and whether + the bootloader resides in the flash, ROM, or RAM. + + Example: + section (0){ + // Erase all + erase all; + // Erase unsecure all + erase unsecure all; + // erase statements specifying memory ID and range + erase @8 all; + erase @288 0x8001000..0x80074A4; + erase sdcard 0x8001000..0x80074A4; + erase mmccard 0x8001000..0x80074A4; + } + + :param cmd_args: dictionary holding path to address, length and flags + :return: CmdErase object + """ + address = value_to_int(cmd_args["address"]) + length = value_to_int(cmd_args.get("length", 0)) + flags = cmd_args.get("flags", 0) + + mem_opt = cmd_args.get("mem_opt") + mem_id = 0 + if mem_opt: + mem_id = self.get_mem_id(mem_opt) + + return CmdErase(address=address, length=length, flags=flags, mem_id=mem_id) + + def _enable(self, cmd_args: dict) -> CmdMemEnable: + """Returns a CmdEnable object initialized based on cmd_args. + + Enable statement is used for initialization of external memories + using a parameter block that was previously loaded to RAM. + + Example: + section (0){ + # Load quadspi config block bin file to RAM, use it to enable QSPI. + load myBinFile > 0x20001000; + enable qspi 0x20001000; + } + + :param cmd_args: dictionary holding address, size and memory type + :return: CmdEnable object + """ + address = value_to_int(cmd_args["address"]) + size = cmd_args.get("size", 4) + mem_opt = cmd_args.get("mem_opt") + mem_id = 0 + if mem_opt: + mem_id = self.get_mem_id(mem_opt) + return CmdMemEnable(address=address, size=size, mem_id=mem_id) + + def _encrypt(self, cmd_args: dict) -> CmdLoad: + """Returns a CmdLoad object initialized based on cmd_args. + + Encrypt holds an ID, which is a reference to keyblob to be used for + encryption. So the encrypt command requires a list of keyblobs, the keyblob + ID and load command. + + e.g. + encrypt (0){ + load myImage > 0x0810000; + } + + :param cmd_args: dictionary holding list of keyblobs, keyblob ID and load dict + :raises SPSDKError: If keyblob to be used is not in the list or is invalid + :return: CmdLoad object + """ + keyblob_id = cmd_args["keyblob_id"] + keyblobs = cmd_args.get("keyblobs", []) + + address = value_to_int(cmd_args["address"]) + + if cmd_args.get("file"): + data = load_binary(cmd_args["file"], self.search_paths) + if cmd_args.get("values"): + values = [int(s, 16) for s in cmd_args["values"].split(",")] + data = struct.pack(f"<{len(values)}L", *values) + + try: + valid_keyblob = self._validate_keyblob(keyblobs, keyblob_id) + except SPSDKError as exc: + raise SPSDKError(f"Invalid key blob {str(exc)}") from exc + + if valid_keyblob is None: + raise SPSDKError(f"Missing keyblob {keyblob_id} for encryption.") + + start_addr = value_to_int(valid_keyblob["keyblob_content"][0]["start"]) + end_addr = value_to_int(valid_keyblob["keyblob_content"][0]["end"]) + key = bytes.fromhex(valid_keyblob["keyblob_content"][0]["key"]) + counter = bytes.fromhex(valid_keyblob["keyblob_content"][0]["counter"]) + byte_swap = valid_keyblob["keyblob_content"][0].get("byte_swap", False) + + keyblob = KeyBlob(start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter) + + # Encrypt only if the ADE and VLD flags are set + if bool(end_addr & keyblob.KEY_FLAG_ADE) and bool(end_addr & keyblob.KEY_FLAG_VLD): + encoded_data = keyblob.encrypt_image( + base_address=address, data=align_block(data, 512), byte_swap=byte_swap + ) + else: + encoded_data = data + + return CmdLoad(address, encoded_data) + + def _keywrap(self, cmd_args: dict) -> CmdLoad: + """Returns a CmdLoad object initialized based on cmd_args. + + Keywrap holds keyblob ID to be encoded by a value stored in load command and + stored to address defined in the load command. + + Example: + keywrap (0) { + load {{ 00000000 }} > 0x08000000; + } + + :param cmd_args: dictionary holding list of keyblobs, keyblob ID and load dict + :raises SPSDKError: If keyblob to be used is not in the list or is invalid + :return: CmdLoad object + """ + # iterate over keyblobs + keyblobs = cmd_args.get("keyblobs", None) + keyblob_id = cmd_args.get("keyblob_id", None) + + address = value_to_int(cmd_args["address"]) + otfad_key = cmd_args["values"] + + try: + valid_keyblob = self._validate_keyblob(keyblobs, keyblob_id) + except SPSDKError as exc: + raise SPSDKError(f" Key blob validation failed: {str(exc)}") from exc + if valid_keyblob is None: + raise SPSDKError(f"Missing keyblob {keyblob_id} for given keywrap") + + start_addr = value_to_int(valid_keyblob["keyblob_content"][0]["start"]) + end_addr = value_to_int(valid_keyblob["keyblob_content"][0]["end"]) + key = bytes.fromhex(valid_keyblob["keyblob_content"][0]["key"]) + counter = bytes.fromhex(valid_keyblob["keyblob_content"][0]["counter"]) + + blob = KeyBlob(start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter) + + encoded_keyblob = blob.export(kek=otfad_key) + logger.info(f"Creating wrapped keyblob: \n{str(blob)}") + + return CmdLoad(address=address, data=encoded_keyblob) + + def _keystore_to_nv(self, cmd_args: dict) -> CmdKeyStoreRestore: + """Returns a CmdKeyStoreRestore object initialized with memory type and address. + + The keystore_to_nv statement instructs the bootloader to load the backed up + keystore values back into keystore memory region on non-volatile memory. + + Example: + section (0) { + keystore_to_nv @9 0x8000800; + + :param cmd_args: dictionary holding the memory type and address. + :return: CmdKeyStoreRestore object. + """ + mem_opt = cmd_args["mem_opt"] + address = value_to_int(cmd_args["address"]) + return CmdKeyStoreRestore(address, ExtMemId.from_tag(mem_opt)) + + def _keystore_from_nv(self, cmd_args: dict) -> CmdKeyStoreBackup: + """Returns a CmdKeyStoreRestore object initialized with memory type and address. + + The keystore_to_nv statement instructs the bootloader to load the backed up + keystore values back into keystore memory region on non-volatile memory. + + Example: + section (0) { + keystore_from_nv @9 0x8000800; + + :param cmd_args: dictionary holding the memory type and address. + :return: CmdKeyStoreRestore object. + """ + mem_opt = cmd_args["mem_opt"] + address = value_to_int(cmd_args["address"]) + return CmdKeyStoreBackup(address, ExtMemId.from_tag(mem_opt)) + + def _version_check(self, cmd_args: dict) -> CmdVersionCheck: + """Returns a CmdVersionCheck object initialized with version check type and version. + + Validates version of secure or non-secure firmware version with the value stored in the OTP or PFR, + to prevent the FW rollback. + The command fails if version provided in command is lower than version stored in the OTP/PFR. + + Example: + section (0) { + version_check sec 0x2; + version_check nsec 2; + } + + :param cmd_args: dictionary holding the version type and fw version. + :return: CmdKeyStoreRestore object. + """ + ver_type = cmd_args["ver_type"] + fw_version = cmd_args["fw_version"] + return CmdVersionCheck(VersionCheckType.from_tag(ver_type), fw_version) + + def _validate_keyblob(self, keyblobs: List, keyblob_id: Number) -> Optional[Dict]: + """Checks, whether a keyblob is valid. + + Parser returns a list of dicts which contains keyblob definitions. These + definitions should contain a 'start', 'end', 'key' & 'counter' keys with + appropriate values. To be able to create a keyblob, we need these for + values. Otherwise we throw an exception that the keyblob is invalid. + + :param keyblobs: list of dicts defining keyblobs + :param keyblob_id: id of keyblob we want to check + :raises SPSDKError: If the keyblob definition is empty + :raises SPSDKError: If the keyblob definition is missing one key + :return: keyblob If exists and is valid, None otherwise + """ + for keyblob in keyblobs: + if keyblob_id == keyblob["keyblob_id"]: + kb_content = keyblob["keyblob_content"] + if len(kb_content) == 0: + raise SPSDKError(f"Keyblob {keyblob_id} definition is empty!") + + for key in ["start", "end", "key", "counter"]: + if key not in kb_content[0]: + raise SPSDKError(f"Keyblob {keyblob_id} is missing '{key}' definition!") + + return keyblob + + return None + + def _jump(self, cmd_args: dict) -> CmdJump: + """Returns a CmdJump object initialized with memory type and address. + + The "jump" command produces the ROM_JUMP_CMD. + See the boot image format design document for specific details about these commands, + such as the function prototypes they expect. + Jump to entrypoint is not supported. Only fixed address is supported. + + Example: + section (0) { + # jump to a fixed address + jump 0xffff0000; + } + + :param cmd_args: dictionary holding the argument and address. + :return: CmdJump object. + """ + argument = cmd_args.get("argument", 0) + address = value_to_int(cmd_args["address"]) + spreg = cmd_args.get("spreg") + + return CmdJump(address, argument, spreg) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py new file mode 100644 index 00000000..94d5353a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sections within SBfile.""" + +from struct import unpack_from +from typing import Iterator, List, Optional + +from spsdk.crypto.hmac import hmac +from spsdk.crypto.symmetric import Counter, aes_ctr_decrypt, aes_ctr_encrypt +from spsdk.exceptions import SPSDKError +from spsdk.sbfile.misc import SecBootBlckSize +from spsdk.utils.abstract import BaseClass +from spsdk.utils.crypto.cert_blocks import CertBlockV1 + +from .commands import CmdBaseClass, CmdHeader, EnumCmdTag, EnumSectionFlag, parse_command + +######################################################################################################################## +# Boot Image Sections +######################################################################################################################## + + +class BootSectionV2(BaseClass): + """Boot Section V2.""" + + HMAC_SIZE = 32 + + @property + def uid(self) -> int: + """Boot Section UID.""" + return self._header.address + + @uid.setter + def uid(self, value: int) -> None: + self._header.address = value + + @property + def is_last(self) -> bool: + """Check whether the section is the last one.""" + return self._header.flags & EnumSectionFlag.LAST_SECT.tag != 0 + + @is_last.setter + def is_last(self, value: bool) -> None: + assert isinstance(value, bool) + self._header.flags = EnumSectionFlag.BOOTABLE.tag + if value: + self._header.flags |= EnumSectionFlag.LAST_SECT.tag + + @property + def hmac_count(self) -> int: + """Number of HMACs.""" + raw_size = 0 + hmac_count = 0 + for cmd in self._commands: + raw_size += cmd.raw_size + if raw_size > 0: + block_count = (raw_size + 15) // 16 + hmac_count = self._hmac_count if block_count >= self._hmac_count else block_count + return hmac_count + + @property + def raw_size(self) -> int: + """Raw size of section.""" + size = CmdHeader.SIZE + self.HMAC_SIZE + size += self.hmac_count * self.HMAC_SIZE + for cmd in self._commands: + size += cmd.raw_size + if size % 16: + size += 16 - (size % 16) + return size + + def __init__(self, uid: int, *commands: CmdBaseClass, hmac_count: int = 1) -> None: + """Initialize BootSectionV2. + + :param uid: section unique identification + :param commands: List of commands + :param hmac_count: The number of HMAC entries + """ + self._header = CmdHeader(EnumCmdTag.TAG.tag, EnumSectionFlag.BOOTABLE.tag) + self._commands: List[CmdBaseClass] = [] + self._hmac_count = hmac_count + for cmd in commands: + self.append(cmd) + # Initialize HMAC count + if not isinstance(self._hmac_count, int) or self._hmac_count == 0: + self._hmac_count = 1 + # section UID + self.uid = uid + + def __len__(self) -> int: + return len(self._commands) + + def __getitem__(self, key: int) -> CmdBaseClass: + return self._commands[key] + + def __setitem__(self, key: int, value: CmdBaseClass) -> None: + self._commands[key] = value + + def __iter__(self) -> Iterator[CmdBaseClass]: + return self._commands.__iter__() + + def append(self, cmd: CmdBaseClass) -> None: + """Add command to section.""" + assert isinstance(cmd, CmdBaseClass) + self._commands.append(cmd) + + def __repr__(self) -> str: + return f"BootSectionV2: {len(self)} commands." + + def __str__(self) -> str: + """Get object info.""" + nfo = "" + for index, cmd in enumerate(self._commands): + nfo += f" {index}) {str(cmd)}\n" + return nfo + + # pylint: disable=too-many-locals + def export( + self, + dek: bytes = b"", + mac: bytes = b"", + counter: Optional[Counter] = None, + ) -> bytes: + """Serialize Boot Section object. + + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: exported bytes + :raises SPSDKError: raised when dek, mac, counter have invalid format or no commands + """ + if not isinstance(dek, bytes): + raise SPSDKError("Invalid type of dek, should be bytes") + if not isinstance(mac, bytes): + raise SPSDKError("Invalid type of mac, should be bytes") + if not isinstance(counter, Counter): + raise SPSDKError("Invalid type of counter") + if not self._commands: + raise SPSDKError("SB2 must contain commands") + # Export commands + commands_data = b"" + for cmd in self._commands: + cmd_data = cmd.export() + commands_data += cmd_data + if len(commands_data) % 16: + commands_data += b"\x00" * (16 - (len(commands_data) % 16)) + # Encrypt header + self._header.data = self.hmac_count + self._header.count = len(commands_data) // 16 + encrypted_header = aes_ctr_encrypt(dek, self._header.export(), counter.value) + hmac_data = hmac(mac, encrypted_header) + counter.increment(1 + (self.hmac_count + 1) * 2) + + # Encrypt commands + encrypted_commands = b"" + for index in range(0, len(commands_data), 16): + encrypted_block = aes_ctr_encrypt(dek, commands_data[index : index + 16], counter.value) + encrypted_commands += encrypted_block + counter.increment() + # Calculate HMAC of commands + index = 0 + hmac_count = self._header.data + block_size = (self._header.count // hmac_count) * 16 + while hmac_count > 0: + enc_block = ( + encrypted_commands[index:] + if hmac_count == 1 + else encrypted_commands[index : index + block_size] + ) + hmac_data += hmac(mac, enc_block) + hmac_count -= 1 + index += len(enc_block) + return encrypted_header + hmac_data + encrypted_commands + + # pylint: disable=too-many-locals + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + plain_sect: bool = False, + dek: bytes = b"", + mac: bytes = b"", + counter: Optional[Counter] = None, + ) -> "BootSectionV2": + """Parse Boot Section from bytes. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param plain_sect: If the sections are not encrypted; It is used for debugging only, not supported by ROM code + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: exported bytes + :raises SPSDKError: raised when dek, mac, counter have invalid format + """ + if not isinstance(dek, bytes): + raise SPSDKError("Invalid type of dek, should be bytes") + if not isinstance(mac, bytes): + raise SPSDKError("Invalid type of mac, should be bytes") + if not isinstance(counter, Counter): + raise SPSDKError("Invalid type of counter") + # Get Header specific data + header_encrypted = data[offset : offset + CmdHeader.SIZE] + header_hmac_data = data[offset + CmdHeader.SIZE : offset + CmdHeader.SIZE + cls.HMAC_SIZE] + offset += CmdHeader.SIZE + cls.HMAC_SIZE + # Check header HMAC + if header_hmac_data != hmac(mac, header_encrypted): + raise SPSDKError("Invalid header HMAC") + # Decrypt header + header_decrypted = aes_ctr_decrypt(dek, header_encrypted, counter.value) + counter.increment() + # Parse header + header = CmdHeader.parse(header_decrypted) + counter.increment((header.data + 1) * 2) + # Get HMAC data + hmac_data = data[offset : offset + (cls.HMAC_SIZE * header.data)] + offset += cls.HMAC_SIZE * header.data + encrypted_commands = data[offset : offset + (header.count * 16)] + # Check HMAC + hmac_index = 0 + hmac_count = header.data + block_size = (header.count // hmac_count) * 16 + section_size = header.count * 16 + while hmac_count > 0: + if hmac_count == 1: + block_size = section_size + hmac_block = hmac(mac, data[offset : offset + block_size]) + if hmac_block != hmac_data[hmac_index : hmac_index + cls.HMAC_SIZE]: + raise SPSDKError("HMAC failed") + hmac_count -= 1 + hmac_index += cls.HMAC_SIZE + section_size -= block_size + offset += block_size + # Decrypt commands + decrypted_commands = b"" + for hmac_index in range(0, len(encrypted_commands), 16): + encr_block = encrypted_commands[hmac_index : hmac_index + 16] + decrypted_block = ( + encr_block if plain_sect else aes_ctr_decrypt(dek, encr_block, counter.value) + ) + decrypted_commands += decrypted_block + counter.increment() + # ... + cmd_offset = 0 + obj = cls(header.address, hmac_count=header.data) + while cmd_offset < len(decrypted_commands): + cmd_obj = parse_command(decrypted_commands[cmd_offset:]) + cmd_offset += cmd_obj.raw_size + obj.append(cmd_obj) + return obj + + +class CertSectionV2(BaseClass): + """Certificate Section V2 class.""" + + HMAC_SIZE = 32 + SECT_MARK = unpack_from(" CertBlockV1: + """Return certification block.""" + return self._cert_block + + @property + def raw_size(self) -> int: + """Calculate raw size of section.""" + # Section header size + size = CmdHeader.SIZE + # Header HMAC 32 bytes + Certificate block HMAC 32 bytes + size += self.HMAC_SIZE * 2 + # Certificate block size in bytes + size += self.cert_block.raw_size + return size + + def __init__(self, cert_block: CertBlockV1): + """Initialize CertBlockV1.""" + assert isinstance(cert_block, CertBlockV1) + self._header = CmdHeader( + EnumCmdTag.TAG.tag, EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag + ) + self._header.address = self.SECT_MARK + self._header.count = cert_block.raw_size // 16 + self._header.data = 1 + self._cert_block = cert_block + + def __repr__(self) -> str: + return f"CertSectionV2: Length={self._header.count * 16}" + + def __str__(self) -> str: + """Get object info.""" + return str(self.cert_block) + + def export( + self, dek: bytes = b"", mac: bytes = b"", counter: Optional[Counter] = None + ) -> bytes: + """Serialize Certificate Section object. + + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: exported bytes + :raises SPSDKError: raised when dek, mac, counter have invalid format + :raises SPSDKError: Raised size of exported bytes is invalid + """ + if not isinstance(dek, bytes): + raise SPSDKError("DEK value is not in bytes") + if not isinstance(mac, bytes): + raise SPSDKError("MAC value is not in bytes") + if not isinstance(counter, Counter): + raise SPSDKError("Counter value is not incorrect") + # Prepare Header data + header_data = self._header.export() + header_encrypted = aes_ctr_encrypt(dek, header_data, counter.value) + # counter.increment() + # Prepare Certificate Block data + body_data = self.cert_block.export() + # Prepare HMAC data + hmac_data = hmac(mac, header_encrypted) + hmac_data += hmac(mac, body_data) + result = header_encrypted + hmac_data + body_data + if len(result) != self.raw_size: + raise SPSDKError("Invalid size") + return result + + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + dek: bytes = b"", + mac: bytes = b"", + counter: Optional[Counter] = None, + ) -> "CertSectionV2": + """Parse Certificate Section from bytes array. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: parsed cert section v2 object + :raises SPSDKError: Raised when dek, mac, counter are not valid + :raises SPSDKError: Raised when there is invalid header HMAC, TAG, FLAGS, Mark + :raises SPSDKError: Raised when there is invalid certificate block HMAC + """ + if not isinstance(dek, bytes): + raise SPSDKError("DEK value has invalid format") + if not isinstance(mac, bytes): + raise SPSDKError("MAC value has invalid format") + if not isinstance(counter, Counter): + raise SPSDKError("Counter value has invalid format") + index = offset + header_encrypted = data[index : index + CmdHeader.SIZE] + index += CmdHeader.SIZE + header_hmac = data[index : index + cls.HMAC_SIZE] + index += cls.HMAC_SIZE + cert_block_hmac = data[index : index + cls.HMAC_SIZE] + index += cls.HMAC_SIZE + if header_hmac != hmac(mac, header_encrypted): + raise SPSDKError("Invalid Header HMAC") + header_encrypted = aes_ctr_decrypt(dek, header_encrypted, counter.value) + header = CmdHeader.parse(header_encrypted) + if header.tag != EnumCmdTag.TAG: + raise SPSDKError(f"Invalid Header TAG: 0x{header.tag:02X}") + if header.flags != (EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag): + raise SPSDKError(f"Invalid Header FLAGS: 0x{header.flags:02X}") + if header.address != cls.SECT_MARK: + raise SPSDKError(f"Invalid Section Mark: 0x{header.address:08X}") + # Parse Certificate Block + cert_block = CertBlockV1.parse(data[index:]) + if cert_block_hmac != hmac(mac, data[index : index + cert_block.raw_size]): + raise SPSDKError("Invalid Certificate Block HMAC") + index += cert_block.raw_size + cert_section_obj = cls(cert_block) + counter.increment(SecBootBlckSize.to_num_blocks(index - offset)) + return cert_section_obj diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py new file mode 100644 index 00000000..ccd4d549 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Lexer for command (BD) files used by parser.""" + +from typing import List, Union + +from sly import Lexer +from sly.lex import Token + + +# pylint: disable=undefined-variable,invalid-name,no-self-use +# undefined-variable : the lexer uses '_' as a decorator, which throws undefined +# variable error. We can't do much with it. +# invalid-name : tokens are defined as upper case. However this violates the +# snake cae naming style. We can't do much, as this is required by the lexer. +# no-self-use : the public methods must be defined as class methods although +# the self is not used at all. +class Variable: + """Class representing a variable in command file.""" + + def __init__(self, name: str, token: str, value: Union[str, int, float]) -> None: + """Initializer. + + :param name: name of identifier (variable) + :param token: type of variable (option, constant etc.) + :param value: the content of the variable + """ + self.name = name + self.t = token + self.value = value + + def __str__(self) -> str: + """Returns a string with variable info. + + i.e. + ", , " + + :return: string + """ + return f"{self.name}, {self.t}, {self.value}" + + +class BDLexer(Lexer): + """Lexer for bd files.""" + + def __init__(self) -> None: + """Initializer.""" + self._sources: List[Variable] = [] + + def cleanup(self) -> None: + """Resets the lexers internals into initial state.""" + self._sources.clear() + + def add_source(self, source: Variable) -> None: + """Append an identifier of source type into list. + + :param source: identifier defined under sources block in BD file + """ + self._sources.append(source) + + # List of reserved keywords + reserved = { + "call": "CALL", + "constants": "CONSTANTS", + "extern": "EXTERN", + "erase": "ERASE", + "false": "FALSE", + "filters": "FILTERS", + "from": "FROM", + "jump": "JUMP", + "load": "LOAD", + "mode": "MODE", + "else": "ELSE", + "info": "INFO", + "error": "ERROR", + "enable": "ENABLE", + "keywrap": "KEYWRAP", + "keystore_to_nv": "KEYSTORE_TO_NV", + "keystore_from_nv": "KEYSTORE_FROM_NV", + "all": "ALL", + "no": "NO", + "options": "OPTIONS", + "raw": "RAW", + "section": "SECTION", + "sources": "SOURCES", + "switch": "SWITCH", + "true": "TRUE", + "yes": "YES", + "if": "IF", + "defined": "DEFINED", + "warning": "WARNING", + "sizeof": "SIZEOF", + "unsecure": "UNSECURE", + "jump_sp": "JUMP_SP", + "keyblob": "KEYBLOB", + "reset": "RESET", + "encrypt": "ENCRYPT", + "version_check": "VERSION_CHECK", + "sec": "SEC", + "nsec": "NSEC", + } + + # List of token names. This is always required + tokens = [ + "COMMENT", + "IDENT", + "SOURCE_NAME", + "BINARY_BLOB", + "INT_LITERAL", + "STRING_LITERAL", + "RANGE", + "ASSIGN", + "INT_SIZE", + "SECTION_NAME", + #'SYMBOL_REF', replaced with a non-terminal symbol_ref + # Operators (+,-,*,/,%,|,&,~,^,<<,>>, ||, &&, !, <, <=, >, >=, ==, !=) + "PLUS", + "MINUS", + "TIMES", + "DIVIDE", + "MOD", + "OR", + "AND", + "NOT", + "XOR", + "LSHIFT", + "RSHIFT", + "LOR", + "LAND", + "LNOT", + "LT", + "LE", + "GT", + "GE", + "EQ", + "NE", + # Delimiters ( ) { } , . ; : + "LPAREN", + "RPAREN", + "LBRACE", + "RBRACE", + "COMMA", + "PERIOD", + "SEMI", + "COLON", + # Special characters + "QUESTIONMARK", + "DOLLAR", + ] + list(reserved.values()) + + literals = {"@"} + + # A regular expression rules with some action code + # The order of these functions MATTER!!! Make sure you know what you are + # doing, when changing the order of function declarations!!! + @_(r"(//.*)|(/\*(.|\s)*?\*/)|(\#.*)") # type: ignore + def COMMENT(self, token: Token) -> None: + """Token rule to detect comments (including multiline). + + Allowed comments are C/C++ like comments '/* */', '//' and bash-like + comments starting with '#'. + + :param token: token matching a comment + """ + # Multiline comments are counted as a single line. This causes us troubles + # in t_newline(), which treats the multiline comment as a single line causing + # a mismatch in the final line position. + # From this perspective we increment the linenumber here by the total + # number of lines - 1 (the subtracted 1 gets counted byt t_newline) + self.lineno += len(token.value.split("\n")) - 1 + + # It's not possible to detect INT_SIZE token while whitespaces are present between period and + # letter in real use case, because of regex engine limitation in positive lookbehind. + @_(r"(?<=(\d|[0-9a-fA-F])\.)[ \t]*[whb]") # type: ignore + def INT_SIZE(self, token: Token) -> Token: + """Token rule to detect numbers appended with w/h/b. + + Example: + my_number = 4.b + my_number = 1.h + my_number = 3.w + + The w/h/b defines size (Byte, Halfword, Word). This should be taken into + account during number computation. + + :param token: token matching int size + + :return: Token representing the size of int literal + """ + return token + + @_(r"[_a-zA-Z][_a-zA-Z0-9]*") # type: ignore + def IDENT(self, token: Token) -> Token: + """Token rule to detect identifiers. + + A valid identifier can start either with underscore or a letter followed + by any numbers of underscores, letters and numbers. + + If the name of an identifier is from the set of reserved keywords, the + token type is replaced with the keyword name, otherwise the token is + of type 'IDENT'. + Values of type TRUE/YES, FALSE/NO are replaces by 1 or 0 respectively. + + :param token: token matching an identifier pattern + :return: Token representing identifier + """ + # it may happen that we find an identifier, which is a keyword, in such + # a case remap the type from IDENT to reserved word (i.e. keyword) + token_type = self.reserved.get(token.value, "IDENT") + if token_type in ["TRUE", "YES"]: + token.type = "INT_LITERAL" + token.value = 1 + elif token_type in ["FALSE", "NO"]: + token.type = "INT_LITERAL" + token.value = 0 + else: + token.type = token_type + # check, whether the identifier is under sources, in such case + # change the type to SOURCE_NAME + for source in self._sources: + if source.name == token.value: + token.type = "SOURCE_NAME" + break + return token + + @_(r"\b([0-9]+[K]?|0[xX][0-9a-fA-F]+)\b|'.*'") # type: ignore + def INT_LITERAL(self, token: Token) -> Token: + """Token rule to detect integer literals. + + An int literal may be represented as a number in decimal form appended + with a 'K' or number in hexadecimal form. + + Example: + 1024 + 1K # same as above + -256 + 0x25 + + Lexer converts the detected string into a number. String literals + appended with 'K' are multiplied by 1024. + + :param token: token matching integer literal pattern + :return: Token representing integer literal + """ + number = token.value + if number[0] == "'" and number[-1] == "'": + # transform 'dude' into '0x64756465' + number = "0x" + bytearray(number[1:-1], "utf-8").hex() + number = int(number, 0) + elif number[-1] == "K": + number = int(number[:-1], 0) * 1024 + else: + number = int(number, 0) + + token.value = number + return token + + @_(r"\$[\w\.\*\?\-\^\[\]]+") # type: ignore + def SECTION_NAME(self, token: Token) -> Token: + """Token rule to detect section names. + + Section names start with a dollar sign ($) glob-type expression that + can match any number of ELF sections. + + Example: + $section_[ab] + $math* + + :param token: token matching section name pattern + :return: Token representing section name + """ + return token + + @_(r"\{\{([0-9a-fA-F]{2}| )+\}\}") # type: ignore + def BINARY_BLOB(self, token: Token) -> Token: + """Token rule to detect binary blob. + + A binary blob is a sequence of hexadecimal bytes in double curly braces. + + Example: + {{aa bb cc 1F 3C}} + + :param token: token matching binary blob pattern + :return: Token representing binary blob + """ + # return just the content between braces + value = token.value[2:-2] + + token.value = "".join(value.split()) + return token + + # A string containing ignored characters (spaces and tabs) + ignore = " \t" + + @_(r"\n") # type: ignore + def newline(self, token: Token) -> None: + """Token rule to detect new lines. + + On new line character the line number count is incremented. + + :param token: token matching new line character + """ + self.lineno += len(token.value) + + # Operators regular expressions + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + MOD = r"%" + NOT = r"~" + XOR = r"\^" + LSHIFT = r"<<" + RSHIFT = r">>" + LOR = r"\|\|" + OR = r"\|" + LAND = r"&&" + AND = r"&" + LE = r"<=" + LT = r"<" + GE = r">=" + GT = r">" + EQ = r"==" + NE = r"!=" + LNOT = r"!" + + # Tokens regular expressions + STRING_LITERAL = r"\".*\"" + RANGE = r"\.\." + + # Assignment operator regular expressions + ASSIGN = r"=" + + # Delimiters regular expressions + LPAREN = r"\(" + RPAREN = r"\)" + LBRACE = r"\{" + RBRACE = r"\}" + COMMA = r"," + PERIOD = r"\." + SEMI = r";" + COLON = r":" + + # Special characters + QUESTIONMARK = r"\?" + DOLLAR = r"\$" + + # Error handling rule + def error(self, t: Token) -> Token: + """Token error handler. + + The lexing index is incremented so lexing can continue, however, an + error token is returned. The token contains the whole text starting + with the detected error. + + :param t: invalid token. + :return: the invalid token. + """ + self.index += 1 + t.value = t.value[0] + return t diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py new file mode 100644 index 00000000..977d2034 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py @@ -0,0 +1,1550 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing command (BD) file parser.""" + +import logging +from numbers import Number +from typing import Any, Dict, List, Optional + +from sly import Parser +from sly.lex import Token +from sly.yacc import YaccProduction + +from spsdk.exceptions import SPSDKError + +from . import sly_bd_lexer as bd_lexer + + +# pylint: disable=too-many-public-methods,too-many-lines +# too-many-public-methods : every method in the parser represents a syntax rule, +# this is necessary and thus can't be omitted. From this perspective this check +# is disabled. +# too-many-lines : the class can't be shortened, as all the methods represent +# rules. +class BDParser(Parser): + """Command (BD) file parser. + + The parser is based on SLY framework (python implementation of Lex/YACC) + and is used to parse the command file, which serves as an input for nxpimage + utility to create a secure binary in 2.1 format. + See the documentation for details. + """ + + # Import tokens from lexer. This is required by the parser! + tokens = bd_lexer.BDLexer.tokens + # tokens = BDLexer.tokens + + # Uncomment this line to output parser debug file + # debugfile = "parser.out" + + log = logging.getLogger(__name__) + log.setLevel(logging.ERROR) + + def __init__(self) -> None: + """Initialization method.""" + super().__init__() + self._variables: List[bd_lexer.Variable] = [] + self._sources: List[bd_lexer.Variable] = [] + self._keyblobs: List[Dict] = [] + self._sections: List[bd_lexer.Variable] = [] + self._input: Any = None + self._bd_file: Dict = {} + self._parse_error: bool = False + self._extern: List[str] = [] + self._lexer = bd_lexer.BDLexer() + + def _cleanup(self) -> None: + """Cleans up allocated resources before next parsing.""" + self._variables = [] + self._keyblobs = [] + self._sections = [] + # for some strange reason, mypy assumes this is a redefinition of _input + self._input = None + self._bd_file = {} + self._parse_error = False + self._lexer.cleanup() + + def parse( + self, text: str, extern: Optional[List] = None + ) -> Optional[Dict]: # pylint: disable=arguments-differ + """Parse the `input_text` and returns a dictionary of the file content. + + :param text: command file to be parsed in string format + :param extern: additional files defined on command line + + :return: dictionary of the command file content or None on Syntax error + """ + self._cleanup() + self._extern = extern or [] + # for some strange reason, mypy assumes this is a redefinition of _input + self._input: Any = text # type: ignore + + super().parse(self._lexer.tokenize(text)) + + if self._parse_error is True: + print("BD file parsing not successful.") + return None + + return self._bd_file + + # Operators precedence + precedence = ( + ("left", "LOR"), + ("left", "LAND"), + ("left", "OR"), + ("left", "XOR"), + ("left", "AND"), + ("left", "EQ", "NE"), + ("left", "GT", "GE", "LT", "LE"), + ("left", "LSHIFT", "RSHIFT"), + ("left", "PLUS", "MINUS"), + ("left", "TIMES", "DIVIDE", "MOD"), + ("right", "SIZEOF"), + ("right", "LNOT", "NOT"), + ) + + # pylint: disable=undefined-variable,function-redefined,no-self-use,unused-argument + # undefined-variable : the module uses underscore decorator to define + # each rule, however, this causes issues to mypy and pylint. + # function-redefined : each rule is identified by a function name and a + # decorator. However from code checking tools perspective, this is + # function redefinition. Thus we need to disable this rule as well. + # no-self-use : all 'rules' must be class methods, although they don't use + # self. Thus we need to omit this rule. + # unused-argument : not all token input arguments are always used, especially + # in rules which are not supported. + @_("pre_section_block section_block") # type: ignore + def command_file(self, token: YaccProduction) -> None: + """Parser rule. + + :param token: object holding the content defined in decorator. + """ + token.pre_section_block.update(token.section_block) + self._bd_file.update(token.pre_section_block) + + @_("pre_section_block options_block") # type: ignore + def pre_section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary defining the presection_block. + """ + options = token.pre_section_block.get("options", {}) + options.update(token.options_block["options"]) + token.pre_section_block["options"] = options + return token.pre_section_block + + @_("pre_section_block constants_block", "pre_section_block sources_block") # type: ignore + def pre_section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary defining the presection block. + """ + token.pre_section_block.update(token[1]) + return token.pre_section_block + + @_("pre_section_block keyblob_block") # type: ignore + def pre_section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary defining the presection block. + """ + if token.pre_section_block.get("keyblobs") is None: + token.pre_section_block["keyblobs"] = [] + token.pre_section_block["keyblobs"].append(token.keyblob_block) + return token.pre_section_block + + @_("empty") # type: ignore + def pre_section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary defining the presection block. + """ + return token.empty + + @_("OPTIONS LBRACE option_def RBRACE") # type: ignore + def options_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary defining the options block. + """ + return token.option_def + + @_("option_def IDENT ASSIGN const_expr SEMI") # type: ignore + def option_def(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding an option definition. + """ + # it appears, that in the option block anything can be defined, so + # we don't check, whether the identifiers defined there are from the + # allowed options anymore. The code is left just as a reminder. + # identifier = token.IDENT + # if identifier in self.allowed_option_identifiers: + # self._variables.append(self.Variable(token.IDENT, "option", token.const_expr)) + # token.option_def["options"].update({token.IDENT : token.const_expr}) + # return token.option_def + # else: + # column = BDParser._find_column(self._input, token) + # print(f"Unknown option in options block at {token.lineno}/{column}: {token.IDENT}") + # self.error(token) + self._variables.append(bd_lexer.Variable(token.IDENT, "option", token.const_expr)) + token.option_def["options"].update({token.IDENT: token.const_expr}) + return token.option_def + + @_("empty") # type: ignore + def option_def(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding an empty option definition. + """ + return {"options": {}} + + @_("CONSTANTS LBRACE constant_def RBRACE") # type: ignore + def constants_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + For now, we don't store the constants in the final bd file. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of constants block. + """ + dictionary: Dict = {} + return dictionary + + @_("constant_def IDENT ASSIGN bool_expr SEMI") # type: ignore + def constant_def(self, token: YaccProduction): + """Parser rule. + + :param token: object holding the content defined in decorator. + """ + self._variables.append(bd_lexer.Variable(token.IDENT, "constant", token.bool_expr)) + + @_("empty") # type: ignore + def constant_def(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding empty constant definition. + """ + return token.empty + + @_("SOURCES LBRACE source_def RBRACE") # type: ignore + def sources_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't store the sources in the final BD file for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the definition of sources + """ + sources = {} + for source in self._lexer._sources: + sources[source.name] = source.value + return {"sources": sources} + + @_("source_def IDENT ASSIGN source_value SEMI") # type: ignore + def source_def(self, token: YaccProduction) -> None: + """Parser rule. + + :param token: object holding the content defined in decorator. + """ + new_source = bd_lexer.Variable(token.IDENT, "source", token.source_value) + self._lexer.add_source(new_source) + + @_("source_def IDENT ASSIGN source_value LPAREN source_attr_list RPAREN SEMI") # type: ignore + def source_def(self, token: YaccProduction) -> None: + """Parser rule. + + :param token: object holding the content defined in decorator. + """ + # self._sources.append(self.Variable(token.IDENT, "source", token.source_value)) + error_token = Token() + error_token.lineno = token.lineno + error_token.index = token._slice[4].index + self.error(error_token, ": attribute list is not supported") + + @_("empty") # type: ignore + def source_def(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding empty content. + """ + return token.empty + + @_("STRING_LITERAL") # type: ignore + def source_value(self, token: YaccProduction) -> str: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: string defining the source value. + """ + # Everything we read is a string. But strings already contain double quotes, + # from this perspective we need to remove them, this omit the first and last + # character. + return token.STRING_LITERAL[1:-1] + + @_("EXTERN LPAREN int_const_expr RPAREN") # type: ignore + def source_value(self, token: YaccProduction) -> str: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: string defining a path defined on command line. + """ + if token.int_const_expr > len(self._extern) - 1: + self.error(token, ": extern() out of range") + return "" + return self._extern[token.int_const_expr] + + @_("source_attr COMMA source_attr_list") # type: ignore + def source_attr_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: empty dictionary as this is not supported right now. + """ + dictionary = {} + return dictionary + + @_("source_attr") # type: ignore + def source_attr_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: returns dictionary holding content of source attribute. + """ + return token.source_attr + + @_("empty") # type: ignore + def source_attr_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: returns dictionary holding content of empty source attribute list. + """ + return {} + + @_("IDENT ASSIGN const_expr") # type: ignore + def source_attr(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of source file attributes. + """ + return {token.IDENT: token.const_expr} + + @_("KEYBLOB LPAREN int_const_expr RPAREN LBRACE keyblob_contents RBRACE") # type: ignore + def keyblob_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of keyblob block. + """ + dictionary = {"keyblob_id": token.int_const_expr, "keyblob_content": token.keyblob_contents} + dictionary["keyblob_id"] = token.int_const_expr + dictionary["keyblob_content"] = token.keyblob_contents + self._keyblobs.append(dictionary) + return dictionary + + # The legacy tool allowed to have multiple definitions inside a keyblob. + # It has been agreed, that this makes no sense and may be dangerous. + # However, it may happen, that someone comes with a use cases, where legacy + # grammar is needed, thus the code has been left untouched just in case. + # @_("keyblob_contents LPAREN keyblob_options_list RPAREN") + # def keyblob_contents(self, token): + # l = token.keyblob_contents + + # # Append only non-empty options lists to simplify further processing + # if len(token.keyblob_options_list) != 0: + # l.append(token.keyblob_options_list) + # return l + + # @_("empty") + # def keyblob_contents(self, token): + # return [] + + # @_("keyblob_options") + # def keyblob_options_list(self, token): + # return token.keyblob_options + + # @_("empty") + # def keyblob_options_list(self, token): + # # After discussion internal discussion, we will ignore empty definitions in keyblob + # # It's not clear, whether this has some effect on the final sb file or not. + # # C++ elftosb implementation is able to parse the file even without empty + # # parenthesis + # return token.empty + + # @_("IDENT ASSIGN const_expr COMMA keyblob_options") + # def keyblob_options(self, token): + # d = {} + # d[token.IDENT] = token.const_expr + # d.update(token.keyblob_options) + # return d + + # @_("IDENT ASSIGN const_expr") + # def keyblob_options(self, token): + # d = {} + # d[token.IDENT] = token.const_expr + # return d + + # New keyblob grammar! + @_("LPAREN keyblob_options RPAREN") # type: ignore + def keyblob_contents(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list containing options of each keyblob. + """ + list_ = [token.keyblob_options] + + return list_ + + @_("IDENT ASSIGN const_expr COMMA keyblob_options") # type: ignore + def keyblob_options(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of keyblob options. + """ + dictionary = {} + dictionary[token.IDENT] = token.const_expr + dictionary.update(token.keyblob_options) + return dictionary + + @_("IDENT ASSIGN const_expr") # type: ignore + def keyblob_options(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the keyblob option. + """ + dictionary = {} + dictionary[token.IDENT] = token.const_expr + return dictionary + + @_("section_block SECTION LPAREN int_const_expr section_options RPAREN section_contents") # type: ignore + def section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a section. + """ + self._sections.append( + { + "section_id": token.int_const_expr, + "options": token.section_options, + "commands": token.section_contents, + } + ) + token.section_block["sections"] += [ + { + "section_id": token.int_const_expr, + "options": token.section_options, + "commands": token.section_contents, + } + ] + return token.section_block + + @_("empty") # type: ignore + def section_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding content of empty section. + """ + token.empty["sections"] = [] + return token.empty + + @_("SEMI section_option_list") # type: ignore + def section_options(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of section options. + """ + return token.section_option_list + + @_("SEMI") # type: ignore + def section_options(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of empty section options. + """ + dictionary = {} + return dictionary + + @_("empty") # type: ignore + def section_options(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of empty section options. + """ + return token.empty + + @_("section_option_list COMMA section_option") # type: ignore + def section_option_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of section options. + """ + options = {} + options.update(token.section_option) + if token.section_option_list: + token.section_option_list.append(options) + return token.section_option_list + + @_("section_option") # type: ignore + def section_option_list(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding a section option. + """ + return [token.section_option] + + @_("IDENT ASSIGN const_expr") # type: ignore + def section_option(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a section option. + """ + return {token.IDENT: token.const_expr} + + @_("LBRACE statement RBRACE") # type: ignore + def section_contents(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the section statements. + """ + return token.statement + + @_("LE SOURCE_NAME SEMI") # type: ignore + def section_contents(self, token: YaccProduction) -> None: + """Parser rule. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": <= syntax is not supported right now.") + + @_("statement basic_stmt SEMI") # type: ignore + def statement(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list holding section statements. + """ + list_ = [] + token.statement + list_.append(token.basic_stmt) + return list_ + + @_("statement from_stmt") # type: ignore + def statement(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support from_stmt for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of from_stmt. + """ + dictionary = {} + return dictionary + + @_("statement if_stmt") # type: ignore + def statement(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support if statements for now. + + :param token: object holding the content defined in decorator. + """ + # return token.statement + token.if_stmt + + @_("statement encrypt_block") # type: ignore + def statement(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list containing the encrypt statement. + """ + list_ = [] + token.statement + list_.append(token.encrypt_block) + return list_ + + @_("statement keywrap_block") # type: ignore + def statement(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list containing the keywrap statement. + """ + list_ = [] + token.statement + list_.append(token.keywrap_block) + return list_ + + @_("empty") # type: ignore + def statement(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: empty list. + """ + # return empty statement list + return [] + + @_("KEYWRAP LPAREN int_const_expr RPAREN LBRACE LOAD BINARY_BLOB GT int_const_expr SEMI RBRACE") # type: ignore + def keywrap_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the keywrap block content. + """ + dictionary = {"keywrap": {"keyblob_id": token.int_const_expr0}} + load_cmd = {"address": token.int_const_expr1, "values": token.BINARY_BLOB} + dictionary["keywrap"].update(load_cmd) + return dictionary + + @_("ENCRYPT LPAREN int_const_expr RPAREN LBRACE load_stmt SEMI RBRACE") # type: ignore + def encrypt_block(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the encrypt block content. + """ + dictionary = {"encrypt": {"keyblob_id": token.int_const_expr}} + dictionary["encrypt"].update(token.load_stmt.get("load")) + return dictionary + + @_( # type: ignore + "load_stmt", + "call_stmt", + "jump_sp_stmt", + "mode_stmt", + "message_stmt", + "erase_stmt", + "enable_stmt", + "reset_stmt", + "keystore_stmt", + "version_stmt", + ) + def basic_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of defined statements. + """ + return token[0] + + @_("LOAD load_opt load_data load_target") # type: ignore + def load_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a load statement. + """ + # pattern with load options means load -> program command + if token.load_data.get("pattern") is not None and token.load_opt.get("load_opt") is None: + cmd = "fill" + else: + cmd = "load" + dictionary: Dict = {cmd: {}} + dictionary[cmd].update(token.load_opt) + dictionary[cmd].update(token.load_data) + dictionary[cmd].update(token.load_target) + return dictionary + + @_("empty") # type: ignore + def load_opt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load options. + """ + return token.empty + + @_("'@' int_const_expr") # type: ignore + def load_opt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load options. + """ + return {"load_opt": token.int_const_expr} + + @_("IDENT") # type: ignore + def load_opt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load options. + """ + return {"load_opt": token.IDENT} + + @_("int_const_expr") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + if isinstance(token.int_const_expr, str): + self.error(token, f": identifier '{token.int_const_expr}' is not a source identifier.") + retval = {"N/A": "N/A"} + else: + retval = {"pattern": token.int_const_expr} + + return retval + + @_("STRING_LITERAL") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + return {"file": token.STRING_LITERAL[1:-1]} + + @_("SOURCE_NAME") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + for source in self._lexer._sources: + if token.SOURCE_NAME == source.name: + return {"file": source.value} + + # with current implementation, this code won't be ever reached. In case + # a not defined source file is used as `load_data`, the parser detects + # it as a different rule: + # + # load_data ::= int_const_expr + # + # which evaluates as false... however, this fragment is left just in + # in case something changes. + self.error(token, ": source file not defined") + return {"file": "N/A"} + + @_("section_list") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + self.error(token, ": section list is not supported") + dictionary = {} + return dictionary + + @_("section_list FROM SOURCE_NAME") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + self.error(token, "section list using from is not supported") + dictionary = {} + return dictionary + + @_("BINARY_BLOB") # type: ignore + def load_data(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load data. + """ + # no_spaces = "".join(token.BINARY_BLOB.split()) + + return {"values": token.BINARY_BLOB} + + @_("GT PERIOD") # type: ignore + def load_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the load target. + """ + self.error(token, ": '.' as load destination is not supported right now") + dictionary = {} + return dictionary + + @_("GT address_or_range") # type: ignore + def load_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of load target. + """ + return token.address_or_range + + @_("empty") # type: ignore + def load_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: empty dictionary. + """ + self.error(token, ": empty load target is not supported right now.") + return token.empty + + @_("ERASE mem_opt address_or_range") # type: ignore + def erase_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of erase statement. + """ + dictionary: Dict = {token.ERASE: {}} + dictionary[token.ERASE].update(token.address_or_range) + dictionary[token.ERASE].update(token.mem_opt) + return dictionary + + @_("ERASE mem_opt ALL") # type: ignore + def erase_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of erase statement. + """ + dictionary: Dict = {token.ERASE: {"address": 0x00, "flags": 0x01}} + dictionary[token.ERASE].update(token.mem_opt) + return dictionary + + @_("ERASE UNSECURE ALL") # type: ignore + def erase_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of erase statement. + """ + return {"erase": {"address": 0x00, "flags": 0x02}} + + @_("ENABLE mem_opt int_const_expr") # type: ignore + def enable_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of enable statement. + """ + dictionary: Dict = {token.ENABLE: {}} + dictionary[token.ENABLE].update(token.mem_opt) + dictionary[token.ENABLE]["address"] = token.int_const_expr + return dictionary + + @_("section_list COMMA section_ref") # type: ignore + def section_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the section list content. + """ + dictionary = {} + return dictionary + + @_("section_ref") # type: ignore + def section_list(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a section reference. + """ + return token.section_ref + + @_("NOT SECTION_NAME") # type: ignore + def section_ref(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a section reference. + """ + self.error(token, ": section reference is not supported.") + dictionary = {} + return dictionary + + @_("SECTION_NAME") # type: ignore + def section_ref(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a section reference. + """ + self.error(token, ": section reference is not supported.") + return {token.SECTION_NAME} + + @_("int_const_expr") # type: ignore + def address_or_range(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of an address. + """ + address_start = token.int_const_expr + return {"address": address_start} + + @_("int_const_expr RANGE int_const_expr") # type: ignore + def address_or_range(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of an address range. + """ + address_start = token.int_const_expr0 + length = token.int_const_expr1 - address_start + return {"address": address_start, "length": length} + + @_("SOURCE_NAME QUESTIONMARK COLON IDENT") # type: ignore + def symbol_ref(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": symbol reference is not supported.") + + @_("call_type call_target call_arg") # type: ignore + def call_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a call statement. + """ + dictionary: Dict = {token.call_type: {}} + dictionary[token.call_type].update(token.call_target) + dictionary[token.call_type].update(token.call_arg) + return dictionary + + @_("CALL", "JUMP") # type: ignore + def call_type(self, token: YaccProduction) -> str: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: string representing 'call' or 'jump' + """ + return token[0] + + @_("int_const_expr") # type: ignore + def call_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a call_target. + """ + return {"address": token.int_const_expr} + + @_("SOURCE_NAME") # type: ignore + def call_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a call target. + """ + self.error(token, ": source name as call target is not supported.") + dictionary = {} + return dictionary + + @_("symbol_ref") # type: ignore + def call_target(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a call target. + """ + self.error(token, ": symbol reference as call target is not supported.") + dictionary = {} + return dictionary + + @_("LPAREN RPAREN") # type: ignore + def call_arg(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding an empty call argument. + """ + dictionary = {} + return dictionary + + @_("LPAREN int_const_expr RPAREN") # type: ignore + def call_arg(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding a call argument. + """ + return {"argument": token.int_const_expr} + + @_("empty") # type: ignore + def call_arg(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding an empty call argument. + """ + return token.empty + + @_("JUMP_SP int_const_expr call_target call_arg") # type: ignore + def jump_sp_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content jump statement. + """ + dictionary: Dict = {"jump": {}} + dictionary["jump"]["spreg"] = token.int_const_expr + dictionary["jump"].update(token.call_target) + dictionary["jump"].update(token.call_arg) + return dictionary + + @_("RESET") # type: ignore + def reset_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of a reset statement. + """ + return {"reset": {}} + + @_("FROM SOURCE_NAME LBRACE in_from_stmt RBRACE") # type: ignore + def from_stmt(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": from statement not supported.") + + @_("basic_stmt SEMI") # type: ignore + def in_from_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list of statements. + """ + return token.basic_stmt + + @_("if_stmt") # type: ignore + def in_from_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list of statements. + """ + return token.if_stmt + + @_("empty") # type: ignore + def in_from_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: empty list. + """ + return [] + + @_("MODE int_const_expr") # type: ignore + def mode_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: + """ + self.error(token, ": mode statement is not supported") + dictionary: Dict = {} + return dictionary + + @_("message_type STRING_LITERAL") # type: ignore + def message_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the message statement. + """ + dictionary: Dict = {} + return dictionary + + @_("INFO", "WARNING", "ERROR") # type: ignore + def message_type(self, token: YaccProduction) -> Dict: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: empty dictionary. + """ + self.error(token, ": info/warning/error messages are not supported.") + dictionary: Dict = {} + return dictionary + + @_("KEYSTORE_TO_NV mem_opt address_or_range") # type: ignore + def keystore_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content keystore statement. + """ + dictionary = {token.KEYSTORE_TO_NV: {}} + dictionary[token.KEYSTORE_TO_NV].update(token.mem_opt) + dictionary[token.KEYSTORE_TO_NV].update(token.address_or_range) + return dictionary + + @_("KEYSTORE_FROM_NV mem_opt address_or_range") # type: ignore + def keystore_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content keystore statement. + """ + dictionary = {token.KEYSTORE_FROM_NV: {}} + dictionary[token.KEYSTORE_FROM_NV].update(token.mem_opt) + dictionary[token.KEYSTORE_FROM_NV].update(token.address_or_range) + return dictionary + + @_("IDENT") # type: ignore + def mem_opt(self, token: YaccProduction) -> None: + """Parser rule. + + Unsupported syntax right now. + + :param token: object holding the content defined in decorator. + """ + # search in variables for token.IDENT variable and get it's value + return {"mem_opt": token.IDENT} + + @_("'@' int_const_expr") # type: ignore + def mem_opt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of memory type. + """ + dictionary = {"mem_opt": token.int_const_expr} + return dictionary + + @_("empty") # type: ignore + def mem_opt(self, token: YaccProduction) -> None: + """Parser rule. + + Unsupported syntax right now. + + :param token: object holding the content defined in decorator. + """ + return token.empty + + @_("VERSION_CHECK sec_or_nsec fw_version") # type: ignore + def version_stmt(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of version check statement. + """ + dictionary: Dict = {token.VERSION_CHECK: {}} + dictionary[token.VERSION_CHECK].update(token.sec_or_nsec) + dictionary[token.VERSION_CHECK].update(token.fw_version) + return dictionary + + @_("SEC") # type: ignore + def sec_or_nsec(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of version check type. + """ + dictionary = {"ver_type": 0} + return dictionary + + @_("NSEC") # type: ignore + def sec_or_nsec(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of version check type. + """ + dictionary = {"ver_type": 1} + return dictionary + + @_("int_const_expr") # type: ignore + def fw_version(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: dictionary holding the content of fw version. + """ + dictionary = {"fw_version": token.int_const_expr} + return dictionary + + @_("IF bool_expr LBRACE statement RBRACE else_stmt") # type: ignore + def if_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: list of if statements. + """ + self.error(token, ": if & if-else statement is not supported.") + if token.bool_expr: + return token.statement + + return token.else_stmt + + @_("ELSE LBRACE statement RBRACE") # type: ignore + def else_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list of else statements. + """ + return token.statement + + @_("ELSE if_stmt") # type: ignore + def else_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: list of else if statements. + """ + return token.if_stmt + + @_("empty") # type: ignore + def else_stmt(self, token: YaccProduction) -> List: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: empty list. + """ + list_ = [] + return list_ + + @_("STRING_LITERAL") # type: ignore + def const_expr(self, token: YaccProduction) -> str: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: string. + """ + return token.STRING_LITERAL[1:-1] + + @_("bool_expr") # type: ignore + def const_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: boolean value as a result of constant expression. + """ + return token.bool_expr + + @_("expr") # type: ignore + def int_const_expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: Number as a result of an expression. + """ + return token.expr + + @_("DEFINED LPAREN IDENT RPAREN") # type: ignore + def bool_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: boolean value as a result if some identifier is defined. + """ + return token.IDENT in self._variables + + @_( # type: ignore + "bool_expr LT bool_expr", + "bool_expr LE bool_expr", + "bool_expr GT bool_expr", + "bool_expr GE bool_expr", + "bool_expr EQ bool_expr", + "bool_expr NE bool_expr", + "bool_expr LAND bool_expr", + "bool_expr LOR bool_expr", + "LPAREN bool_expr RPAREN", + ) + def bool_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: boolean value as a result of boolean expression. + """ + operator = token[1] + if operator == "<": + return token.bool_expr0 < token.bool_expr1 + if operator == "<=": + return token.bool_expr0 <= token.bool_expr1 + if operator == ">": + return token.bool_expr0 > token.bool_expr1 + if operator == ">=": + return token.bool_expr0 >= token.bool_expr1 + if operator == "==": + return token.bool_expr0 == token.bool_expr1 + if operator == "!=": + return token.bool_expr0 != token.bool_expr1 + if operator == "&&": + return token.bool_expr0 and token.bool_expr1 + if operator == "||": + return token.bool_expr0 or token.bool_expr1 + + return token[1] + + @_("int_const_expr") # type: ignore + def bool_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: boolean value as a result of a boolean expression. + """ + return token.int_const_expr + + @_("LNOT bool_expr") # type: ignore + def bool_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: boolean value as a result of logical not expression. + """ + return not token.bool_expr + + @_("IDENT LPAREN SOURCE_NAME RPAREN") # type: ignore + def bool_expr(self, token: YaccProduction) -> bool: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + :return: boolean value (at the moment always False, as not supported). + """ + # I've absolutely no clue, what this rule can mean or be for??? + self.error(token, ": IDENT ( SOURCE_NAME ) is not supported.") + return False + + @_( # type: ignore + "expr PLUS expr", + "expr MINUS expr", + "expr TIMES expr", + "expr DIVIDE expr", + "expr MOD expr", + "expr LSHIFT expr", + "expr RSHIFT expr", + "expr AND expr", + "expr OR expr", + "expr XOR expr", + "expr PERIOD INT_SIZE", + "LPAREN expr RPAREN", + ) + def expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: number as a result of an expression. + """ + operator = token[1] + if operator == "+": + return token.expr0 + token.expr1 + if operator == "-": + return token.expr0 - token.expr1 + if operator == "*": + return token.expr0 - token.expr1 + if operator == "/": + return token.expr0 // token.expr1 + if operator == "%": + return token.expr0 % token.expr1 + if operator == "<<": + return token.expr0 << token.expr1 + if operator == ">>": + return token.expr0 >> token.expr1 + if operator == "&": + return token.expr0 & token.expr1 + if operator == "|": + return token.expr0 | token.expr1 + if operator == "^": + return token.expr0 ^ token.expr1 + if operator == ".": + char = token.INT_SIZE + if char == "w": + return token[0] & 0xFFFF + if char == "h": + return token[0] & 0xFF + if char == "b": + return token[0] & 0xF + # LPAREN expr RPAREN + return token[1] + + @_("INT_LITERAL") # type: ignore + def expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: integer number as a terminal. + """ + return token.INT_LITERAL + + @_("IDENT") # type: ignore + def expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: number stored under identifier. + """ + # we need to convert the IDENT into a value stored under that identifier + # search the variables and check, whether there is a name of IDENT + for var in self._variables: + if var.name == token.IDENT: + return var.value + + return token.IDENT + + @_("symbol_ref") # type: ignore + def expr(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": symbol reference is not supported.") + + @_("unary_expr") # type: ignore + def expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: number as a result of unary expression. + """ + return token.unary_expr + + @_("SIZEOF LPAREN symbol_ref RPAREN") # type: ignore + def expr(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": sizeof operator is not supported") + + @_("SIZEOF LPAREN IDENT RPAREN") # type: ignore + def expr(self, token: YaccProduction) -> None: + """Parser rule. + + We don't support this rule for now. + + :param token: object holding the content defined in decorator. + """ + self.error(token, ": sizeof operator is not supported") + + @_("PLUS expr", "MINUS expr") # type: ignore + def unary_expr(self, token: YaccProduction) -> Number: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: number as a result of unary expression. + """ + sign = token[0] + number = token.expr + if sign == "-": + number = -number + + return number + + @_("") # type: ignore + def empty(self, token: YaccProduction) -> Dict: + """Parser rule. + + :param token: object holding the content defined in decorator. + :return: empty dictionary. + """ + dictionary: Dict = {} + return dictionary + + @staticmethod + def _find_column(text: str, token: YaccProduction) -> int: + """Finds the column of token in input. + + :param text: input file being parsed + :param token: object holding the content defined in decorator. + :return: column based on token index. + """ + last_cr = text.rfind("\n", 0, token.index) + if last_cr < 0: + last_cr = 0 + else: + last_cr += 1 + column = (token.index - last_cr) + 1 + return column + + @staticmethod + def _find_line(text: str, line_num: int) -> str: + """Finds the line in text based on line number. + + :param text: text to return required line. + :param line_num: line number to return. + :return: line 'line_num" in 'text'. + """ + lines = text.split("\n") + + return lines[line_num] + + def error( + self, token: YaccProduction, msg: str = "" + ) -> YaccProduction: # pylint: disable=arguments-differ + """Syntax error handler. + + On syntax error, we set an error flag and read the rest of input file + until end to terminate the process of parsing. + + :param token: object holding the content defined in decorator. + :param msg: error message to use. + + :raises SPSDKError: Raises error with 'msg' message. + """ + self._parse_error = True + + if token: + lineno = getattr(token, "lineno", -1) + if lineno != -1: + column = BDParser._find_column(self._input, token) + error_line = BDParser._find_line(self._input, lineno - 1) + raise SPSDKError( + f"bdcompiler:{lineno}:{column}: error{msg}\n\n{error_line}\n" + + (column - 1) * " " + + "^\n" + ) + + raise SPSDKError(f"bdcompiler: error{msg}\n") + + raise SPSDKError("bdcompiler: unspecified error.") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py new file mode 100644 index 00000000..580978fc --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Uboot device.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py new file mode 100644 index 00000000..18221b1c --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Simple Uboot serial console implementation.""" + +import logging + +from crcmod.predefined import mkPredefinedCrcFun +from hexdump import restore +from serial import Serial + +from spsdk.exceptions import SPSDKError +from spsdk.utils.misc import align, change_endianness, split_data + +logger = logging.getLogger(__name__) + + +class Uboot: + """Class for encapsulation of Uboot CLI interface.""" + + LINE_FEED = "\n" + ENCODING = "ascii" + READ_ALIGNMENT = 16 + DATA_BYTES_SPLIT = 4 + PROMPT = b"u-boot=> " + + def __init__( + self, port: str, timeout: int = 1, baudrate: int = 115200, crc: bool = True + ) -> None: + """Uboot constructor. + + :param port: TTY port + :param timeout: timeout in seconds, defaults to 1 + :param baudrate: baudrate, defaults to 115200 + :param crc: True if crc will be calculated, defaults to True + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.is_opened = False + self.open() + self.crc = crc + + def calc_crc(self, data: bytes, address: int, count: int) -> None: + """Calculate CRC from the data. + + :param data: data to calculate CRC from + :param address: address from where the data should be calculated + :param count: count of bytes + :raises SPSDKError: Invalid CRC of data + """ + if not self.crc: + return + crc_command = f"crc32 {hex(address)} {hex(count)}" + self.write(crc_command) + hexdump_str = self.LINE_FEED.join(self.read_output().splitlines()[1:-1]) + crc_obtained = "0x" + hexdump_str[-8:] + logger.debug(f"CRC command:\n{crc_command}\n{crc_obtained}") + crc_function = mkPredefinedCrcFun("crc-32") + calculated_crc = hex(crc_function(data)) + logger.debug(f"Calculated CRC {calculated_crc}") + if calculated_crc != crc_obtained: + raise SPSDKError(f"Invalid CRC of data {calculated_crc} != {crc_obtained}") + + def open(self) -> None: + """Open uboot device.""" + self._device = Serial(port=self.port, timeout=self.timeout, baudrate=self.baudrate) + self.is_opened = True + + def close(self) -> None: + """Close uboot device.""" + self._device.close() + self.is_opened = False + + def read(self, length: int) -> str: + """Read specified number of charactrs from uboot CLI. + + :param length: count of read characters + :return: encoded string + """ + output = self._device.read(length) + return output.decode(encoding=self.ENCODING) + + def read_output(self) -> str: + """Read CLI output until prompt. + + :return: ASCII encoded output + """ + return self._device.read_until(expected=self.PROMPT).decode(self.ENCODING) + + def write(self, data: str) -> None: + """Write ASCII decoded data to CLI. Append LINE FEED if not present. + + :param data: ASCII decoded data + """ + if self.LINE_FEED not in data: + data += self.LINE_FEED + data_bytes = bytes(data, encoding=self.ENCODING) + self._device.write(data_bytes) + + def read_memory(self, address: int, count: int) -> bytes: + """Read memory using the md command. Optionally calculate CRC. + + :param address: Address in memory + :param count: Count of bytes + :return: data as bytes + """ + count = align(count, self.READ_ALIGNMENT) + md_command = f"md.b {hex(address)} {hex(count)}" + self.write(md_command) + hexdump_str = self.LINE_FEED.join(self.read_output().splitlines()[1:-1]) + logger.debug(f"read_memory:\n{md_command}\n{hexdump_str}") + data = restore(hexdump_str) + self.calc_crc(data, address, count) + + return data + + def write_memory(self, address: int, data: bytes) -> None: + """Write memory and optionally calculate CRC. + + :param address: Address in memory + :param data: data as bytes + """ + start_address = address + for splitted_data in split_data(data, self.DATA_BYTES_SPLIT): + mw_command = f"mw.l {hex(address)} {change_endianness(splitted_data).hex()}" + logger.debug(f"write_memory: {mw_command}") + self.write(mw_command) + address += len(splitted_data) + self.read_output() + + self.calc_crc(data, start_address, len(data)) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/__init__.py new file mode 100644 index 00000000..99ff7fd3 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module containing various functions/modules used throughout the SPSDK.""" + +from .exceptions import ( + SPSDKRegsError, + SPSDKRegsErrorBitfieldNotFound, + SPSDKRegsErrorEnumNotFound, + SPSDKRegsErrorRegisterGroupMishmash, + SPSDKRegsErrorRegisterNotFound, +) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/abstract.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/abstract.py new file mode 100644 index 00000000..3c8131c5 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/abstract.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for base abstract classes.""" + +from abc import ABC, abstractmethod +from typing import Any + +from typing_extensions import Self + + +######################################################################################################################## +# Abstract Class for Data Classes +######################################################################################################################## +class BaseClass(ABC): + """Abstract Class for Data Classes.""" + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return isinstance(obj, self.__class__) and vars(obj) == vars(self) + + def __ne__(self, obj: Any) -> bool: + return not self.__eq__(obj) + + @abstractmethod + def __repr__(self) -> str: + """Object representation in string format.""" + + @abstractmethod + def __str__(self) -> str: + """Object description in string format.""" + + @abstractmethod + def export(self) -> bytes: + """Serialize object into bytes array.""" + + @classmethod + @abstractmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/__init__.py new file mode 100644 index 00000000..e9525d40 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for cryptographic utilities.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py new file mode 100644 index 00000000..ac6e7b7a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py @@ -0,0 +1,1745 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for handling Certificate block.""" + +import datetime +import logging +import os +import re +from abc import abstractmethod +from struct import calcsize, pack, unpack_from +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union + +from typing_extensions import Self + +from spsdk import version as spsdk_version +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash +from spsdk.crypto.keys import PrivateKeyRsa, PublicKeyEcc +from spsdk.crypto.signature_provider import SignatureProvider, get_signature_provider +from spsdk.crypto.types import SPSDKEncoding +from spsdk.crypto.utils import extract_public_key, extract_public_key_from_data, get_matching_key_id +from spsdk.exceptions import ( + SPSDKError, + SPSDKNotImplementedError, + SPSDKTypeError, + SPSDKUnsupportedOperation, + SPSDKValueError, +) +from spsdk.utils.abstract import BaseClass +from spsdk.utils.crypto.rkht import RKHTv1, RKHTv21 +from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file +from spsdk.utils.misc import ( + Endianness, + align, + align_block, + change_endianness, + find_file, + load_binary, + load_configuration, + split_data, + value_to_int, + write_file, +) +from spsdk.utils.schema_validator import CommentedConfig + +logger = logging.getLogger(__name__) + + +class CertBlock(BaseClass): + """Common general class for various CertBlocks.""" + + @classmethod + @abstractmethod + def get_supported_families(cls) -> List[str]: + """Get supported families for certification block.""" + + @classmethod + @abstractmethod + def get_validation_schemas(cls) -> List[Dict[str, Any]]: + """Create the list of validation schemas. + + :return: List of validation schemas. + """ + + @staticmethod + @abstractmethod + def generate_config_template(family: Optional[str] = None) -> str: + """Generate configuration for certification block.""" + + @classmethod + @abstractmethod + def from_config( + cls, + config: Dict[str, Any], + search_paths: Optional[List[str]] = None, + ) -> Self: + """Creates an instance of cert block from configuration.""" + + @abstractmethod + def create_config(self, data_path: str) -> str: + """Create configuration of the Certification block Image.""" + + @classmethod + def get_cert_block_class(cls, family: str) -> Type["CertBlock"]: + """Get certification block class by family name. + + :param family: Chip family + :raises SPSDKError: No certification block class found for given family + """ + for cert_block_class in cls.get_cert_block_classes(): + if family in cert_block_class.get_supported_families(): + return cert_block_class + raise SPSDKError(f"Family '{family}' is not supported in any certification block.") + + @classmethod + def get_all_supported_families(cls) -> List[str]: + """Get supported families for all certification blocks except for SRK.""" + families = get_families(DatabaseManager.CERT_BLOCK) + + return [ + family + for family in families + if "srk" not in get_db(family, "latest").get_str(DatabaseManager.CERT_BLOCK, "rot_type") + ] + + @classmethod + def get_cert_block_classes(cls) -> List[Type["CertBlock"]]: + """Get list of all cert block classes.""" + return CertBlock.__subclasses__() + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return bytes() + + @classmethod + def _get_supported_families(cls, cert_block_type: str) -> List[str]: + """Get list of supported families. + + :param cert_block_type: Type of certification block to look for + :return: List of devices that supports this cert block + """ + families = cls.get_all_supported_families() + + return [ + family + for family in families + if get_db(family, "latest").get_str(DatabaseManager.CERT_BLOCK, "rot_type") + == cert_block_type + ] + + @classmethod + def get_root_private_key_file(cls, config: Dict[str, Any]) -> Optional[str]: + """Get main root private key file from config. + + :param config: Configuration to be searched. + :return: Root private key file path. + """ + private_key_file = config.get("signPrivateKey", config.get("mainRootCertPrivateKeyFile")) + if private_key_file and not isinstance(private_key_file, str): + raise SPSDKTypeError("Root private key file must be a string type") + return private_key_file + + @classmethod + def find_main_cert_index( + cls, config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> Optional[int]: + """Go through all certificates and find the index matching to private key. + + :param config: Configuration to be searched. + :param search_paths: List of paths where to search for the file, defaults to None + :return: List of root certificates. + """ + try: + signature_provider = get_signature_provider( + sp_cfg=config.get("signProvider"), + local_file_key=cls.get_root_private_key_file(config), + search_paths=search_paths, + ) + except SPSDKError as exc: + logger.debug(f"A signature provider could not be created: {exc}") + return None + root_certificates = find_root_certificates(config) + public_keys = [] + for root_crt_file in root_certificates: + try: + public_key = extract_public_key(root_crt_file, search_paths=search_paths) + public_keys.append(public_key) + except SPSDKError: + continue + try: + idx = get_matching_key_id(public_keys, signature_provider) + return idx + except (SPSDKValueError, SPSDKUnsupportedOperation) as exc: + logger.debug(f"Main cert index could not be found: {exc}") + return None + + @classmethod + def get_main_cert_index( + cls, config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> int: + """Gets main certificate index from configuration. + + :param config: Input standard configuration. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Certificate index + :raises SPSDKError: If invalid configuration is provided. + :raises SPSDKError: If correct certificate could not be identified. + :raises SPSDKValueError: If certificate is not of correct type. + """ + root_cert_id = config.get("mainRootCertId") + cert_chain_id = config.get("mainCertChainId") + if root_cert_id is not None and cert_chain_id is not None and root_cert_id != cert_chain_id: + raise SPSDKError( + "The mainRootCertId and mainRootCertId are specified and have different values." + ) + found_cert_id = cls.find_main_cert_index(config=config, search_paths=search_paths) + if root_cert_id is None and cert_chain_id is None: + if found_cert_id is not None: + return found_cert_id + raise SPSDKError("Certificate could not be found") + # root_cert_id may be 0 which is falsy value, therefore 'or' cannot be used + cert_id = root_cert_id if root_cert_id is not None else cert_chain_id + try: + cert_id = int(cert_id) + except ValueError as exc: + raise SPSDKValueError(f"A certificate index is not a number: {cert_id}") from exc + if found_cert_id is not None and found_cert_id != cert_id: + logger.warning("Defined certificate does not match the private key.") + return cert_id + + +######################################################################################################################## +# Certificate Block Header Class +######################################################################################################################## +class CertBlockHeader(BaseClass): + """Certificate block header.""" + + FORMAT = "<4s2H6I" + SIZE = calcsize(FORMAT) + SIGNATURE = b"cert" + + def __init__(self, version: str = "1.0", flags: int = 0, build_number: int = 0) -> None: + """Constructor. + + :param version: Version of the certificate in format n.n + :param flags: Flags for the Certificate Header + :param build_number: of the certificate + :raises SPSDKError: When there is invalid version + """ + if not re.match(r"[0-9]+\.[0-9]+", version): # check format of the version: N.N + raise SPSDKError("Invalid version") + self.version = version + self.flags = flags + self.build_number = build_number + self.image_length = 0 + self.cert_count = 0 + self.cert_table_length = 0 + + def __repr__(self) -> str: + nfo = f"CertBlockHeader: V={self.version}, F={self.flags}, BN={self.build_number}, IL={self.image_length}, " + nfo += f"CC={self.cert_count}, CTL={self.cert_table_length}" + return nfo + + def __str__(self) -> str: + """Info of the certificate header in text form.""" + nfo = str() + nfo += f" CB Version: {self.version}\n" + nfo += f" CB Flags: {self.flags}\n" + nfo += f" CB Build Number: {self.build_number}\n" + nfo += f" CB Image Length: {self.image_length}\n" + nfo += f" CB Cert. Count: {self.cert_count}\n" + nfo += f" CB Cert. Length: {self.cert_table_length}\n" + return nfo + + def export(self) -> bytes: + """Certificate block in binary form.""" + major_version, minor_version = [int(v) for v in self.version.split(".")] + return pack( + self.FORMAT, + self.SIGNATURE, + major_version, + minor_version, + self.SIZE, + self.flags, + self.build_number, + self.image_length, + self.cert_count, + self.cert_table_length, + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Deserialize object from bytes array. + + :param data: Input data as bytes + :return: Certificate Header instance + :raises SPSDKError: Unexpected size or signature of data + """ + if cls.SIZE > len(data): + raise SPSDKError("Incorrect size") + ( + signature, + major_version, + minor_version, + length, + flags, + build_number, + image_length, + cert_count, + cert_table_length, + ) = unpack_from(cls.FORMAT, data) + if signature != cls.SIGNATURE: + raise SPSDKError("Incorrect signature") + if length != cls.SIZE: + raise SPSDKError("Incorrect length") + obj = cls( + version=f"{major_version}.{minor_version}", + flags=flags, + build_number=build_number, + ) + obj.image_length = image_length + obj.cert_count = cert_count + obj.cert_table_length = cert_table_length + return obj + + +######################################################################################################################## +# Certificate Block Class +######################################################################################################################## +class CertBlockV1(CertBlock): + """Certificate block. + + Shared for SB file 2.1 and for MasterBootImage using RSA keys. + """ + + # default size alignment + DEFAULT_ALIGNMENT = 16 + + @property + def header(self) -> CertBlockHeader: + """Certificate block header.""" + return self._header + + @property + def rkh(self) -> List[bytes]: + """List of root keys hashes (SHA-256), each hash as 32 bytes.""" + return self._rkht.rkh_list + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return self._rkht.rkth() + + @property + def rkth_fuses(self) -> List[int]: + """List of RKHT fuses, ordered from highest bit to lowest. + + Note: Returned values are in format that should be passed for blhost + """ + result = [] + rkht = self.rkth + while rkht: + fuse = int.from_bytes(rkht[:4], byteorder=Endianness.LITTLE.value) + result.append(fuse) + rkht = rkht[4:] + return result + + @property + def certificates(self) -> List[Certificate]: + """List of certificates in header. + + First certificate is root certificate and followed by optional chain certificates + """ + return self._cert + + @property + def signature_size(self) -> int: + """Size of the signature in bytes.""" + return len( + self.certificates[0].signature + ) # The certificate is self signed, return size of its signature + + @property + def rkh_index(self) -> Optional[int]: + """Index of the Root Key Hash that matches the certificate; None if does not match.""" + if self._cert: + rkh = self._cert[0].public_key_hash() + for index, value in enumerate(self.rkh): + if rkh == value: + return index + return None + + @property + def alignment(self) -> int: + """Alignment of the binary output, by default it is DEFAULT_ALIGNMENT but can be customized.""" + return self._alignment + + @alignment.setter + def alignment(self, value: int) -> None: + """Setter. + + :param value: new alignment + :raises SPSDKError: When there is invalid alignment + """ + if value <= 0: + raise SPSDKError("Invalid alignment") + self._alignment = value + + @property + def raw_size(self) -> int: + """Aligned size of the certificate block.""" + size = CertBlockHeader.SIZE + size += self._header.cert_table_length + size += self._rkht.RKH_SIZE * self._rkht.RKHT_SIZE + return align(size, self.alignment) + + @property + def expected_size(self) -> int: + """Expected size of binary block.""" + return self.raw_size + + @property + def image_length(self) -> int: + """Image length in bytes.""" + return self._header.image_length + + @image_length.setter + def image_length(self, value: int) -> None: + """Setter. + + :param value: new image length + :raises SPSDKError: When there is invalid image length + """ + if value <= 0: + raise SPSDKError("Invalid image length") + self._header.image_length = value + + def __init__(self, version: str = "1.0", flags: int = 0, build_number: int = 0) -> None: + """Constructor. + + :param version: of the certificate in format n.n + :param flags: Flags for the Certificate Block Header + :param build_number: of the certificate + """ + self._header = CertBlockHeader(version, flags, build_number) + self._rkht: RKHTv1 = RKHTv1([]) + self._cert: List[Certificate] = [] + self._alignment = self.DEFAULT_ALIGNMENT + + def __len__(self) -> int: + return len(self._cert) + + def set_root_key_hash(self, index: int, key_hash: Union[bytes, bytearray, Certificate]) -> None: + """Add Root Key Hash into RKHT. + + Note: Multiple root public keys are supported to allow for key revocation. + + :param index: The index of Root Key Hash in the table + :param key_hash: The Root Key Hash value (32 bytes, SHA-256); + or Certificate where the hash can be created from public key + :raises SPSDKError: When there is invalid index of root key hash in the table + :raises SPSDKError: When there is invalid length of key hash + """ + if isinstance(key_hash, Certificate): + key_hash = get_hash(key_hash.get_public_key().export()) + assert isinstance(key_hash, (bytes, bytearray)) + if len(key_hash) != self._rkht.RKH_SIZE: + raise SPSDKError("Invalid length of key hash") + self._rkht.set_rkh(index, bytes(key_hash)) + + def add_certificate(self, cert: Union[bytes, Certificate]) -> None: + """Add certificate. + + First call adds root certificate. Additional calls add chain certificates. + + :param cert: The certificate itself in DER format + :raises SPSDKError: If certificate cannot be added + """ + if isinstance(cert, bytes): + cert_obj = Certificate.parse(cert) + elif isinstance(cert, Certificate): + cert_obj = cert + else: + raise SPSDKError("Invalid parameter type (cert)") + if cert_obj.version.name != "v3": + raise SPSDKError("Expected certificate v3 but received: " + cert_obj.version.name) + if self._cert: # chain certificate? + last_cert = self._cert[-1] # verify that it is signed by parent key + if not cert_obj.validate(last_cert): + raise SPSDKError("Chain certificate cannot be verified using parent public key") + else: # root certificate + if not cert_obj.self_signed: + raise SPSDKError(f"Root certificate must be self-signed.\n{str(cert_obj)}") + self._cert.append(cert_obj) + self._header.cert_count += 1 + self._header.cert_table_length += cert_obj.raw_size + 4 + + def __repr__(self) -> str: + return str(self._header) + + def __str__(self) -> str: + """Text info about certificate block.""" + nfo = str(self.header) + nfo += " Public Root Keys Hash e.g. RKH (SHA256):\n" + rkh_index = self.rkh_index + for index, root_key in enumerate(self._rkht.rkh_list): + nfo += ( + f" {index}) {root_key.hex().upper()} {'<- Used' if index == rkh_index else ''}\n" + ) + rkth = self.rkth + nfo += f" RKTH (SHA256): {rkth.hex().upper()}\n" + for index, fuse in enumerate(self.rkth_fuses): + bit_ofs = (len(rkth) - 4 * index) * 8 + nfo += f" - RKTH fuse [{bit_ofs:03}:{bit_ofs - 31:03}]: {fuse:08X}\n" + for index, cert in enumerate(self._cert): + nfo += " Root Certificate:\n" if index == 0 else f" Certificate {index}:\n" + nfo += str(cert) + return nfo + + def verify_data(self, signature: bytes, data: bytes) -> bool: + """Signature verification. + + :param signature: to be verified + :param data: that has been signed + :return: True if the data signature can be confirmed using the certificate; False otherwise + """ + cert = self._cert[-1] + pub_key = cert.get_public_key() + return pub_key.verify_signature(signature=signature, data=data) + + def verify_private_key(self, private_key: PrivateKeyRsa) -> bool: + """Verify that given private key matches the public certificate. + + :param private_key: to be tested + :return: True if yes; False otherwise + """ + cert = self.certificates[-1] # last certificate + pub_key = cert.get_public_key() + return private_key.verify_public_key(pub_key) + + def export(self) -> bytes: + """Serialize Certificate Block V1 object.""" + # At least one certificate must be used + if not self._cert: + raise SPSDKError("At least one certificate must be used") + # The hast of root key certificate must be in RKHT + if self.rkh_index is None: + raise SPSDKError("The HASH of used Root Key must be in RKHT") + # CA: Using a single certificate is allowed. In this case, the sole certificate must be self-signed and must not + # be a CA. If multiple certificates are used, the root must be self-signed and all but the last must be CAs. + if self._cert[-1].ca: + raise SPSDKError("The last chain certificate must not be CA.") + if not all(cert.ca for cert in self._cert[:-1]): + raise SPSDKError("All certificates except the last chain certificate must be CA") + # Export + data = self.header.export() + for cert in self._cert: + data += pack(" Self: + """Deserialize CertBlockV1 from binary file. + + :param data: Binary data + :return: Certificate Block instance + :raises SPSDKError: Length of the data doesn't match Certificate Block length + """ + header = CertBlockHeader.parse(data) + offset = CertBlockHeader.SIZE + if len(data) < (header.cert_table_length + (RKHTv1.RKHT_SIZE * RKHTv1.RKH_SIZE)): + raise SPSDKError("Length of the data doesn't match Certificate Block length") + obj = cls(version=header.version, flags=header.flags, build_number=header.build_number) + for _ in range(header.cert_count): + cert_len = unpack_from(" List[Dict[str, Any]]: + """Create the list of validation schemas. + + :return: List of validation schemas. + """ + sch_cfg = get_schema_file(DatabaseManager.CERT_BLOCK) + return [ + sch_cfg["certificate_v1"], + sch_cfg["certificate_root_keys"], + ] + + @staticmethod + def generate_config_template(_family: Optional[str] = None) -> str: + """Generate configuration for certification block v1.""" + val_schemas = CertBlockV1.get_validation_schemas() + val_schemas.append( + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + ) + return CommentedConfig("Certification Block V1 template", val_schemas).get_template() + + def create_config(self, data_path: str) -> str: + """Create configuration of the Certification block Image.""" + cfg = self.get_config(data_path) + val_schemas = CertBlockV1.get_validation_schemas() + + return CommentedConfig( + main_title=( + "Certification block v1 recreated configuration from :" + f"{datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}." + ), + schemas=val_schemas, + ).get_config(cfg) + + @classmethod + def get_root_private_key_file(cls, config: Dict[str, Any]) -> Optional[str]: + """Get main root private key file from config. + + :param config: Configuration to be searched. + :return: Root private key file path. + """ + private_key_file = config.get("mainCertPrivateKeyFile") + if private_key_file and not isinstance(private_key_file, str): + raise SPSDKTypeError("Root private key file must be a string type") + return private_key_file + + @classmethod + def from_config( + cls, + config: Dict[str, Any], + search_paths: Optional[List[str]] = None, + ) -> "CertBlockV1": + """Creates an instance of CertBlockV1 from configuration. + + :param config: Input standard configuration. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Instance of CertBlockV1 + :raises SPSDKError: Invalid certificates detected, Invalid configuration. + """ + if not isinstance(config, Dict): + raise SPSDKError("Configuration cannot be parsed") + cert_block = config.get("certBlock") + if cert_block: + try: + return cls.parse(load_binary(cert_block, search_paths)) + except (SPSDKError, TypeError): + if search_paths: # append path to cert block + search_paths.append(os.path.dirname(cert_block)) + else: + search_paths = [os.path.dirname(cert_block)] + return cls.from_config(load_configuration(cert_block, search_paths), search_paths) + + image_build_number = value_to_int(config.get("imageBuildNumber", 0)) + root_certificates: List[List[str]] = [[] for _ in range(4)] + # TODO we need to read the whole chain from the dict for a given + # selection based on mainCertPrivateKeyFile!!! + root_certificates[0].append(config.get("rootCertificate0File", None)) + root_certificates[1].append(config.get("rootCertificate1File", None)) + root_certificates[2].append(config.get("rootCertificate2File", None)) + root_certificates[3].append(config.get("rootCertificate3File", None)) + main_cert_chain_id = cls.get_main_cert_index(config, search_paths=search_paths) + if root_certificates[main_cert_chain_id][0] is None: + raise SPSDKError(f"A key rootCertificate{main_cert_chain_id}File must be defined") + + # get all certificate chain related keys from config + pattern = f"chainCertificate{main_cert_chain_id}File[0-3]" + keys = [key for key in config.keys() if re.fullmatch(pattern, key)] + # just in case, sort the chain certificate keys in order + keys.sort() + for key in keys: + root_certificates[main_cert_chain_id].append(config[key]) + + cert_block = CertBlockV1(build_number=image_build_number) + + # add whole certificate chain used for image signing + for cert_path in root_certificates[main_cert_chain_id]: + cert_data = Certificate.load( + find_file(str(cert_path), search_paths=search_paths) + ).export(SPSDKEncoding.DER) + cert_block.add_certificate(cert_data) + # set root key hash of each root certificate + empty_rec = False + for cert_idx, cert_path_list in enumerate(root_certificates): + if cert_path_list[0]: + if empty_rec: + raise SPSDKError("There are gaps in rootCertificateXFile definition") + cert_data = Certificate.load( + find_file(str(cert_path_list[0]), search_paths=search_paths) + ).export(SPSDKEncoding.DER) + cert_block.set_root_key_hash(cert_idx, Certificate.parse(cert_data)) + else: + empty_rec = True + + return cert_block + + def get_config(self, output_folder: str) -> Dict[str, Any]: + """Create configuration of Certificate V2 from object. + + :param output_folder: Output folder to store possible files. + :return: Configuration dictionary. + """ + + def create_certificate_cfg(root_id: int, chain_id: int) -> Optional[str]: + if len(self._cert) <= chain_id: + return None + + file_name = f"certificate{root_id}_depth{chain_id}.der" + self._cert[chain_id].save(os.path.join(output_folder, file_name)) + return file_name + + cfg: Dict[str, Optional[Union[str, int]]] = {} + cfg["imageBuildNumber"] = self.header.build_number + used_cert_id = self.rkh_index + assert used_cert_id is not None + cfg["mainRootCertId"] = used_cert_id + + cfg[f"rootCertificate{used_cert_id}File"] = create_certificate_cfg(used_cert_id, 0) + for chain_ix in range(4): + cfg[f"chainCertificate{used_cert_id}File{chain_ix}"] = create_certificate_cfg( + used_cert_id, chain_ix + 1 + ) + + return cfg + + @classmethod + def get_supported_families(cls) -> List[str]: + """Get list of supported families.""" + return super()._get_supported_families("cert_block_1") + + +######################################################################################################################## +# Certificate Block Class for SB 3.1 +######################################################################################################################## + + +def convert_to_ecc_key(key: Union[PublicKeyEcc, bytes]) -> PublicKeyEcc: + """Convert key into EccKey instance.""" + if isinstance(key, PublicKeyEcc): + return key + try: + pub_key = extract_public_key_from_data(key) + if not isinstance(pub_key, PublicKeyEcc): + raise SPSDKError("Not ECC key") + return pub_key + except Exception: + pass + # Just recreate public key from the parsed data + return PublicKeyEcc.parse(key) + + +class CertificateBlockHeader(BaseClass): + """Create Certificate block header.""" + + FORMAT = "<4s2HL" + SIZE = calcsize(FORMAT) + MAGIC = b"chdr" + + def __init__(self, format_version: str = "2.1") -> None: + """Constructor for Certificate block header version 2.1. + + :param format_version: Major = 2, minor = 1 + """ + self.format_version = format_version + self.cert_block_size = 0 + + def export(self) -> bytes: + """Export Certificate block header as bytes array.""" + major_format_version, minor_format_version = [ + int(v) for v in self.format_version.split(".") + ] + + return pack( + self.FORMAT, + self.MAGIC, + minor_format_version, + major_format_version, + self.cert_block_size, + ) + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse Certificate block header from bytes array. + + :param data: Input data as bytes + :raises SPSDKError: Raised when SIZE is bigger than length of the data without offset + :raises SPSDKError: Raised when magic is not equal MAGIC + :return: CertificateBlockHeader + """ + if cls.SIZE > len(data): + raise SPSDKError("SIZE is bigger than length of the data without offset") + ( + magic, + minor_format_version, + major_format_version, + cert_block_size, + ) = unpack_from(cls.FORMAT, data) + + if magic != cls.MAGIC: + raise SPSDKError("Magic is not same!") + + obj = cls(format_version=f"{major_format_version}.{minor_format_version}") + obj.cert_block_size = cert_block_size + return obj + + def __len__(self) -> int: + """Length of the Certificate block header.""" + return calcsize(self.FORMAT) + + def __repr__(self) -> str: + return f"Cert block header {self.format_version}" + + def __str__(self) -> str: + """Get info of Certificate block header.""" + info = f"Format version: {self.format_version}\n" + info += f"Certificate block size: {self.cert_block_size}\n" + return info + + +class RootKeyRecord(BaseClass): + """Create Root key record.""" + + # P-256 + + def __init__( + self, + ca_flag: bool, + root_certs: Optional[Union[Sequence[PublicKeyEcc], Sequence[bytes]]] = None, + used_root_cert: int = 0, + ) -> None: + """Constructor for Root key record. + + :param ca_flag: CA flag + :param root_certs: Root cert used to ISK/image signature + :param used_root_cert: Used root cert number 0-3 + """ + self.ca_flag = ca_flag + self.root_certs_input = root_certs + self.root_certs: List[PublicKeyEcc] = [] + self.used_root_cert = used_root_cert + self.flags = 0 + self._rkht = RKHTv21([]) + self.root_public_key = b"" + + @property + def number_of_certificates(self) -> int: + """Get number of included certificates.""" + return (self.flags & 0xF0) >> 4 + + @property + def expected_size(self) -> int: + """Get expected binary block size.""" + # the '4' means 4 bytes for flags + return 4 + len(self._rkht.export()) + len(self.root_public_key) + + def __repr__(self) -> str: + cert_type = {0x1: "secp256r1", 0x2: "secp384r1"}[self.flags & 0xF] + return f"Cert Block: Root Key Record - ({cert_type})" + + def __str__(self) -> str: + """Get info of Root key record.""" + cert_type = {0x1: "secp256r1", 0x2: "secp384r1"}[self.flags & 0xF] + info = "" + info += f"Flags: {hex(self.flags)}\n" + info += f" - CA: {bool(self.ca_flag)}, ISK Certificate is {'not ' if self.ca_flag else ''}mandatory\n" + info += f" - Used Root c.:{self.used_root_cert}\n" + info += f" - Number of c.:{self.number_of_certificates}\n" + info += f" - Cert. type: {cert_type}\n" + if self.root_certs: + info += f"Root certs: {self.root_certs}\n" + if self._rkht.rkh_list: + info += f"CTRK Hash table: {self._rkht.export().hex()}\n" + if self.root_public_key: + info += f"Root public key: {str(convert_to_ecc_key(self.root_public_key))}\n" + + return info + + def _calculate_flags(self) -> int: + """Function to calculate parameter flags.""" + flags = 0 + if self.ca_flag is True: + flags |= 1 << 31 + if self.used_root_cert: + flags |= self.used_root_cert << 8 + flags |= len(self.root_certs) << 4 + if self.root_certs[0].curve in ["NIST P-256", "p256", "secp256r1"]: + flags |= 1 << 0 + if self.root_certs[0].curve in ["NIST P-384", "p384", "secp384r1"]: + flags |= 1 << 1 + return flags + + def _create_root_public_key(self) -> bytes: + """Function to create root public key.""" + root_key = self.root_certs[self.used_root_cert] + root_key_data = root_key.export() + return root_key_data + + def calculate(self) -> None: + """Calculate all internal members. + + :raises SPSDKError: The RKHT certificates inputs are missing. + """ + # pylint: disable=invalid-name + if not self.root_certs_input: + raise SPSDKError("Root Key Record: The root of trust certificates are not specified.") + self.root_certs = [convert_to_ecc_key(cert) for cert in self.root_certs_input] + self.flags = self._calculate_flags() + self._rkht = RKHTv21.from_keys(keys=self.root_certs) + if self._rkht.hash_algorithm != self.get_hash_algorithm(self.flags): + raise SPSDKError("Hash algorithm does not match the key size.") + self.root_public_key = self._create_root_public_key() + + def export(self) -> bytes: + """Export Root key record as bytes array.""" + data = bytes() + data += pack(" EnumHashAlgorithm: + """Get CTRK table hash algorithm. + + :param flags: Root Key Record flags + :return: Name of hash algorithm + """ + return {1: EnumHashAlgorithm.SHA256, 2: EnumHashAlgorithm.SHA384}[flags & 0xF] + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse Root key record from bytes array. + + :param data: Input data as bytes array + :return: Root key record object + """ + (flags,) = unpack_from("> 8 + number_of_hashes = (flags & 0xF0) >> 4 + rotkh_len = {0x0: 32, 0x1: 32, 0x2: 48}[flags & 0xF] + root_key_record = cls(ca_flag=ca_flag, root_certs=[], used_root_cert=used_rot_ix) + root_key_record.flags = flags + offset = 4 # move offset just after FLAGS + if number_of_hashes > 1: + rkht_len = rotkh_len * number_of_hashes + rkht = data[offset : offset + rkht_len] + offset += rkht_len + root_key_record.root_public_key = data[offset : offset + rotkh_len * 2] + root_key_record._rkht = ( + RKHTv21.parse(rkht, cls.get_hash_algorithm(flags)) + if number_of_hashes > 1 + else RKHTv21([get_hash(root_key_record.root_public_key, cls.get_hash_algorithm(flags))]) + ) + return root_key_record + + +class IskCertificate(BaseClass): + """Create ISK certificate.""" + + def __init__( + self, + constraints: int = 0, + signature_provider: Optional[SignatureProvider] = None, + isk_cert: Optional[Union[PublicKeyEcc, bytes]] = None, + user_data: Optional[bytes] = None, + offset_present: bool = True, + family: Optional[str] = None, + ) -> None: + """Constructor for ISK certificate. + + :param constraints: Certificate version + :param signature_provider: ISK Signature Provider + :param isk_cert: ISK certificate + :param user_data: User data + """ + self.flags = 0 + self.offset_present = offset_present + self.constraints = constraints + self.signature_provider = signature_provider + self.isk_cert = convert_to_ecc_key(isk_cert) if isk_cert else None + self.user_data = user_data or bytes() + if family: + db = get_db(device=family) + isk_data_limit = db.get_int(DatabaseManager.CERT_BLOCK, "isk_data_limit") + if len(self.user_data) > isk_data_limit: + raise SPSDKError( + f"ISK user data is too big ({len(self.user_data)} B). Max size is: {isk_data_limit} B." + ) + isk_data_alignment = db.get_int(DatabaseManager.CERT_BLOCK, "isk_data_alignment") + if len(self.user_data) % isk_data_alignment: + raise SPSDKError(f"ISK user data is not aligned to {isk_data_alignment} B.") + self.signature = bytes() + self.coordinate_length = ( + self.signature_provider.signature_length // 2 if self.signature_provider else 0 + ) + self.isk_public_key_data = self.isk_cert.export() if self.isk_cert else bytes() + + self._calculate_flags() + + @property + def signature_offset(self) -> int: + """Signature offset inside the ISK Certificate.""" + offset = calcsize("<3L") if self.offset_present else calcsize("<2L") + signature_offset = offset + len(self.user_data) + if self.isk_cert: + signature_offset += 2 * self.isk_cert.coordinate_size + + return signature_offset + + @property + def expected_size(self) -> int: + """Binary block expected size.""" + sign_len = len(self.signature) or ( + self.signature_provider.signature_length if self.signature_provider else 0 + ) + pub_key_len = ( + self.isk_cert.coordinate_size * 2 if self.isk_cert else len(self.isk_public_key_data) + ) + + offset = 4 if self.offset_present else 0 + return ( + offset # signature offset + + 4 # constraints + + 4 # flags + + pub_key_len # isk public key coordinates + + len(self.user_data) # user data + + sign_len # isk blob signature + ) + + def __repr__(self) -> str: + isk_type = {0: "secp256r1", 1: "secp256r1", 2: "secp384r1"}[self.flags & 0xF] + return f"ISK Certificate, {isk_type}" + + def __str__(self) -> str: + """Get info about ISK certificate.""" + isk_type = {0: "secp256r1", 1: "secp256r1", 2: "secp384r1"}[self.flags & 0xF] + info = "" + info += f"Constraints: {self.constraints}\n" + info += f"Flags: {self.flags}\n" + if self.user_data: + info += f"User data: {self.user_data.hex()}\n" + else: + info += "User data: Not included\n" + info += f"Type: {isk_type}\n" + info += f"Public Key: {str(self.isk_cert)}\n" + return info + + def _calculate_flags(self) -> None: + """Function to calculate parameter flags.""" + self.flags = 0 + if self.user_data: + self.flags |= 1 << 31 + assert self.isk_cert + if self.isk_cert.curve == "secp256r1": + self.flags |= 1 << 0 + if self.isk_cert.curve == "secp384r1": + self.flags |= 1 << 1 + + def create_isk_signature(self, key_record_data: bytes, force: bool = False) -> None: + """Function to create ISK signature. + + :raises SPSDKError: Signature provider is not specified. + """ + # pylint: disable=invalid-name + if self.signature and not force: + return + if not self.signature_provider: + raise SPSDKError("ISK Certificate: The signature provider is not specified.") + if self.offset_present: + data = key_record_data + pack( + "<3L", self.signature_offset, self.constraints, self.flags + ) + else: + data = key_record_data + pack("<2L", self.constraints, self.flags) + data += self.isk_public_key_data + self.user_data + self.signature = self.signature_provider.get_signature(data) + + def export(self) -> bytes: + """Export ISK certificate as bytes array.""" + if not self.signature: + raise SPSDKError("Signature is not set.") + if self.offset_present: + data = pack("<3L", self.signature_offset, self.constraints, self.flags) + else: + data = pack("<2L", self.constraints, self.flags) + data += self.isk_public_key_data + if self.user_data: + data += self.user_data + data += self.signature + + assert len(data) == self.expected_size + return data + + @classmethod + def parse(cls, data: bytes, signature_size: int) -> Self: # type: ignore # pylint: disable=arguments-differ + """Parse ISK certificate from bytes array.This operation is not supported. + + :param data: Input data as bytes array + :param signature_size: The signature size of ISK block + :raises NotImplementedError: This operation is not supported + """ + (signature_offset, constraints, isk_flags) = unpack_from("<3L", data) + header_word_cnt = 3 + if signature_offset & 0xFFFF == 0x4D43: # This means that certificate has no offset + (constraints, isk_flags) = unpack_from("<2L", data) + signature_offset = 72 + header_word_cnt = 2 + user_data_flag = bool(isk_flags & 0x80000000) + isk_pub_key_length = {0x0: 32, 0x1: 32, 0x2: 48}[isk_flags & 0xF] + offset = header_word_cnt * 4 + isk_pub_key_bytes = data[offset : offset + isk_pub_key_length * 2] + offset += isk_pub_key_length * 2 + user_data = data[offset:signature_offset] if user_data_flag else None + signature = data[signature_offset : signature_offset + signature_size] + offset_present = header_word_cnt == 3 + certificate = cls( + constraints=constraints, + isk_cert=isk_pub_key_bytes, + user_data=user_data, + offset_present=offset_present, + ) + certificate.signature = signature + return certificate + + +class IskCertificateLite(BaseClass): + """ISK certificate lite.""" + + MAGIC = 0x4D43 + VERSION = 1 + HEADER_FORMAT = " None: + """Constructor for ISK certificate. + + :param pub_key: ISK public key + :param constraints: 1 = self signed, 0 = nxp signed + :param user_data: User data + """ + self.constraints = constraints + self.pub_key = convert_to_ecc_key(pub_key) + self.signature = bytes() + self.isk_public_key_data = self.pub_key.export() + + @property + def expected_size(self) -> int: + """Binary block expected size.""" + return ( + +4 # magic + version + + 4 # constraints + + self.ISK_PUB_KEY_LENGTH # isk public key coordinates + + self.ISK_SIGNATURE_SIZE # isk blob signature + ) + + def __repr__(self) -> str: + return "ISK Certificate lite" + + def __str__(self) -> str: + """Get info about ISK certificate.""" + info = "ISK Certificate lite\n" + info += f"Constraints: {self.constraints}\n" + info += f"Public Key: {str(self.pub_key)}\n" + return info + + def create_isk_signature( + self, signature_provider: Optional[SignatureProvider], force: bool = False + ) -> None: + """Function to create ISK signature. + + :param signature_provider: Signature Provider + :param force: Force resign. + :raises SPSDKError: Signature provider is not specified. + """ + # pylint: disable=invalid-name + if self.signature and not force: + return + if not signature_provider: + raise SPSDKError("ISK Certificate: The signature provider is not specified.") + + data = pack(self.HEADER_FORMAT, self.MAGIC, self.VERSION, self.constraints) + data += self.isk_public_key_data + self.signature = signature_provider.get_signature(data) + + def export(self) -> bytes: + """Export ISK certificate as bytes array.""" + if not self.signature: + raise SPSDKError("Signature is not set.") + + data = pack(self.HEADER_FORMAT, self.MAGIC, self.VERSION, self.constraints) + data += self.isk_public_key_data + data += self.signature + + assert len(data) == self.expected_size, "ISK Cert data size does not match" + return data + + @classmethod + def parse(cls, data: bytes) -> Self: # pylint: disable=arguments-differ + """Parse ISK certificate from bytes array. + + :param data: Input data as bytes array + :raises NotImplementedError: This operation is not supported + """ + (_, _, constraints) = unpack_from(cls.HEADER_FORMAT, data) + offset = calcsize(cls.HEADER_FORMAT) + isk_pub_key_bytes = data[offset : offset + cls.ISK_PUB_KEY_LENGTH] + offset += cls.ISK_PUB_KEY_LENGTH + signature = data[offset : offset + cls.ISK_SIGNATURE_SIZE] + certificate = cls( + constraints=constraints, + pub_key=isk_pub_key_bytes, + ) + certificate.signature = signature + return certificate + + +class CertBlockV21(CertBlock): + """Create Certificate block version 2.1. + + Used for SB 3.1 and MBI using ECC keys. + """ + + MAGIC = b"chdr" + FORMAT_VERSION = "2.1" + + def __init__( + self, + root_certs: Optional[Union[Sequence[PublicKeyEcc], Sequence[bytes]]] = None, + ca_flag: bool = False, + version: str = "2.1", + used_root_cert: int = 0, + constraints: int = 0, + signature_provider: Optional[SignatureProvider] = None, + isk_cert: Optional[Union[PublicKeyEcc, bytes]] = None, + user_data: Optional[bytes] = None, + family: Optional[str] = None, + ) -> None: + """The Constructor for Certificate block.""" + self.header = CertificateBlockHeader(version) + self.root_key_record = RootKeyRecord( + ca_flag=ca_flag, used_root_cert=used_root_cert, root_certs=root_certs + ) + + self.isk_certificate = None + if not ca_flag and signature_provider and isk_cert: + self.isk_certificate = IskCertificate( + constraints=constraints, + signature_provider=signature_provider, + isk_cert=isk_cert, + user_data=user_data, + family=family, + ) + + def _set_ca_flag(self, value: bool) -> None: + self.root_key_record.ca_flag = value + + def calculate(self) -> None: + """Calculate all internal members.""" + self.root_key_record.calculate() + + @property + def signature_size(self) -> int: + """Size of the signature in bytes.""" + # signature size is same as public key data + if self.isk_certificate: + return len(self.isk_certificate.isk_public_key_data) + + return len(self.root_key_record.root_public_key) + + @property + def expected_size(self) -> int: + """Expected size of binary block.""" + expected_size = self.header.SIZE + expected_size += self.root_key_record.expected_size + if self.isk_certificate: + expected_size += self.isk_certificate.expected_size + return expected_size + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return self.root_key_record._rkht.rkth() + + def __repr__(self) -> str: + return f"Cert block 2.1, Size:{self.expected_size}B" + + def __str__(self) -> str: + """Get info of Certificate block.""" + msg = f"HEADER:\n{str(self.header)}\n" + msg += f"ROOT KEY RECORD:\n{str(self.root_key_record)}\n" + if self.isk_certificate: + msg += f"ISK Certificate:\n{str(self.isk_certificate)}\n" + return msg + + def export(self) -> bytes: + """Export Certificate block as bytes array.""" + key_record_data = self.root_key_record.export() + self.header.cert_block_size = self.header.SIZE + len(key_record_data) + isk_cert_data = bytes() + if self.isk_certificate: + self.isk_certificate.create_isk_signature(key_record_data) + isk_cert_data = self.isk_certificate.export() + self.header.cert_block_size += len(isk_cert_data) + header_data = self.header.export() + return header_data + key_record_data + isk_cert_data + + @classmethod + def parse(cls, data: bytes) -> Self: + """Parse Certificate block from bytes array.This operation is not supported. + + :param data: Input data as bytes array + :raises SPSDKError: Magic do not match + """ + # CertificateBlockHeader + cert_header = CertificateBlockHeader.parse(data) + offset = len(cert_header) + # RootKeyRecord + root_key_record = RootKeyRecord.parse(data[offset:]) + offset += root_key_record.expected_size + # IskCertificate + isk_certificate = None + if root_key_record.ca_flag == 0: + isk_certificate = IskCertificate.parse( + data[offset:], len(root_key_record.root_public_key) + ) + # Certification Block V2.1 + cert_block = cls() + cert_block.header = cert_header + cert_block.root_key_record = root_key_record + cert_block.isk_certificate = isk_certificate + return cert_block + + @classmethod + def get_validation_schemas(cls) -> List[Dict[str, Any]]: + """Create the list of validation schemas. + + :return: List of validation schemas. + """ + sch_cfg = get_schema_file(DatabaseManager.CERT_BLOCK) + return [sch_cfg["certificate_v21"], sch_cfg["certificate_root_keys"]] + + @classmethod + def from_config( + cls, config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "CertBlockV21": + """Creates an instance of CertBlockV21 from configuration. + + :param config: Input standard configuration. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Instance of CertBlockV21 + :raises SPSDKError: If found gap in certificates from config file. Invalid configuration. + """ + if not isinstance(config, Dict): + raise SPSDKError("Configuration cannot be parsed") + cert_block = config.get("certBlock") + if cert_block: + try: + return cls.parse(load_binary(cert_block, search_paths)) + except (SPSDKError, TypeError): + if search_paths: # append path to cert block + search_paths.append(os.path.dirname(cert_block)) + else: + search_paths = [os.path.dirname(cert_block)] + cert_block_data = load_configuration(cert_block, search_paths) + # temporarily pass-down family to cert-block config data + cert_block_data["family"] = config["family"] + return cls.from_config(cert_block_data, search_paths) + + root_certificates = find_root_certificates(config) + main_root_cert_id = cls.get_main_cert_index(config, search_paths=search_paths) + + try: + root_certificates[main_root_cert_id] + except IndexError as e: + raise SPSDKError( + f"Main root certificate with id {main_root_cert_id} does not exist" + ) from e + + root_certs = [ + load_binary(cert_file, search_paths=search_paths) for cert_file in root_certificates + ] + + user_data = None + signature_provider = None + isk_cert = None + + use_isk = config.get("useIsk", False) + if use_isk: + signature_provider_config = config.get("signProvider") + signature_provider = get_signature_provider( + signature_provider_config, + cls.get_root_private_key_file(config), + search_paths=search_paths, + ) + + isk_public_key = config.get("iskPublicKey", config.get("signingCertificateFile")) + isk_cert = load_binary(isk_public_key, search_paths=search_paths) + + isk_sign_data_path = config.get("iskCertData", config.get("signCertData")) + if isk_sign_data_path: + user_data = load_binary(isk_sign_data_path, search_paths=search_paths) + + isk_constraint = value_to_int( + config.get("iskCertificateConstraint", config.get("signingCertificateConstraint", "0")) + ) + family = config.get("family") + cert_block = cls( + root_certs=root_certs, + used_root_cert=main_root_cert_id, + user_data=user_data, + constraints=isk_constraint, + isk_cert=isk_cert, + ca_flag=not use_isk, + signature_provider=signature_provider, + family=family, + ) + cert_block.calculate() + + return cert_block + + def validate(self) -> None: + """Validate the settings of class members. + + :raises SPSDKError: Invalid configuration of certification block class members. + """ + self.header.parse(self.header.export()) + if self.isk_certificate and not self.isk_certificate.signature: + if not isinstance(self.isk_certificate.signature_provider, SignatureProvider): + raise SPSDKError("Invalid ISK certificate.") + + @staticmethod + def generate_config_template(family: Optional[str] = None) -> str: + """Generate configuration for certification block v21.""" + val_schemas = CertBlockV21.get_validation_schemas() + val_schemas.append( + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + ) + + if family: + # find family + for schema in val_schemas: + if "properties" in schema and "family" in schema["properties"]: + schema["properties"]["family"]["template_value"] = family + break + return CommentedConfig("Certification Block V21 template", val_schemas).get_template() + + def get_config(self, output_folder: str) -> Dict[str, Any]: + """Create configuration dictionary of the Certification block Image. + + :param output_folder: Path to store the data files of configuration. + :return: Configuration dictionary. + """ + cfg: Dict[str, Optional[Union[str, int]]] = {} + cfg["mainRootCertPrivateKeyFile"] = "N/A" + cfg["signingCertificatePrivateKeyFile"] = "N/A" + for i in range(self.root_key_record.number_of_certificates): + key: Optional[PublicKeyEcc] = None + if i == self.root_key_record.used_root_cert: + key = convert_to_ecc_key(self.root_key_record.root_public_key) + else: + if i < len(self.root_key_record.root_certs) and self.root_key_record.root_certs[i]: + key = convert_to_ecc_key(self.root_key_record.root_certs[i]) + if key: + key_file_name = os.path.join(output_folder, f"rootCertificate{i}File.pub") + key.save(key_file_name) + cfg[f"rootCertificate{i}File"] = f"rootCertificate{i}File.pub" + else: + cfg[ + f"rootCertificate{i}File" + ] = "The public key is not possible reconstruct from the key hash" + + cfg["mainRootCertId"] = self.root_key_record.used_root_cert + if self.isk_certificate and self.root_key_record.ca_flag == 0: + cfg["useIsk"] = True + assert self.isk_certificate.isk_cert + key = self.isk_certificate.isk_cert + key_file_name = os.path.join(output_folder, "signingCertificateFile.pub") + key.save(key_file_name) + cfg["signingCertificateFile"] = "signingCertificateFile.pub" + cfg["signingCertificateConstraint"] = self.isk_certificate.constraints + if self.isk_certificate.user_data: + key_file_name = os.path.join(output_folder, "isk_user_data.bin") + write_file(self.isk_certificate.user_data, key_file_name, mode="wb") + cfg["signCertData"] = "isk_user_data.bin" + + else: + cfg["useIsk"] = False + + return cfg + + def create_config(self, data_path: str) -> str: + """Create configuration of the Certification block Image. + + :param data_path: Path to store the data files of configuration. + :return: Configuration in string. + """ + cfg = self.get_config(data_path) + val_schemas = CertBlockV21.get_validation_schemas() + + return CommentedConfig( + main_title=( + "Certification block v2.1 recreated configuration from :" + f"{datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}." + ), + schemas=val_schemas, + ).get_config(cfg) + + @classmethod + def get_supported_families(cls) -> List[str]: + """Get list of supported families.""" + return super()._get_supported_families("cert_block_21") + + +######################################################################################################################## +# Certificate Block Class for SB X +######################################################################################################################## + + +######################################################################################################################## +# Certificate Block Class for SB X +######################################################################################################################## + + +class CertBlockVx(CertBlock): + """Create Certificate block for MC56xx.""" + + ISK_CERT_LENGTH = 136 + ISK_CERT_HASH_LENGTH = 16 # [0:127] + + def __init__( + self, + isk_cert: Union[PublicKeyEcc, bytes], + signature_provider: Optional[SignatureProvider] = None, + self_signed: bool = True, + ) -> None: + """The Constructor for Certificate block.""" + self.isk_cert_hash = bytes(self.ISK_CERT_HASH_LENGTH) + self.isk_certificate = IskCertificateLite(pub_key=isk_cert, constraints=int(self_signed)) + self.signature_provider = signature_provider + + @property + def expected_size(self) -> int: + """Expected size of binary block.""" + return self.isk_certificate.expected_size + + @property + def cert_hash(self) -> bytes: + """Calculate first half [:127] of certificate hash.""" + isk_cert_data = self.isk_certificate.export() + return get_hash(isk_cert_data)[: self.ISK_CERT_HASH_LENGTH] + + def __repr__(self) -> str: + return "CertificateBlockVx" + + def __str__(self) -> str: + """Get info about Certificate block.""" + msg = "Certificate block version x\n" + msg += f"ISK Certificate:\n{str(self.isk_certificate)}\n" + msg += f"Certificate hash: {self.cert_hash.hex()}" + return msg + + def export(self) -> bytes: + """Export Certificate block as bytes array.""" + isk_cert_data = bytes() + self.isk_certificate.create_isk_signature(self.signature_provider) + isk_cert_data = self.isk_certificate.export() + return isk_cert_data + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse Certificate block from bytes array.This operation is not supported. + + :param data: Input data as bytes array + :raises SPSDKValueError: In case of inval + """ + # IskCertificate + isk_certificate = IskCertificateLite.parse(data) + cert_block = cls( + isk_cert=isk_certificate.isk_public_key_data, + self_signed=bool(isk_certificate.constraints), + ) + cert_block.isk_certificate.signature = isk_certificate.signature + return cert_block + + @classmethod + def get_validation_schemas(cls) -> List[Dict[str, Any]]: + """Create the list of validation schemas. + + :return: List of validation schemas. + """ + sch_cfg = get_schema_file(DatabaseManager.CERT_BLOCK) + return [sch_cfg["certificate_vx"]] + + def create_config(self, data_path: str) -> str: + """Create configuration of the Certification block Image.""" + raise SPSDKNotImplementedError("Parsing of Cert Block Vx is not supported") + + @classmethod + def from_config( + cls, config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "CertBlockVx": + """Creates an instance of CertBlockVx from configuration. + + :param config: Input standard configuration. + :param search_paths: List of paths where to search for the file, defaults to None + :return: CertBlockVx + :raises SPSDKError: If found gap in certificates from config file. Invalid configuration. + """ + if not isinstance(config, Dict): + raise SPSDKError("Configuration cannot be parsed") + cert_block = config.get("certBlock") + if cert_block: + try: + return cls.parse(load_binary(cert_block, search_paths)) + except Exception: + return cls.from_config(load_configuration(cert_block, search_paths), search_paths) + + main_root_private_key_file = cls.get_root_private_key_file(config) + signature_provider = config.get("signProvider", config.get("iskSignProvider")) + isk_certificate = config.get("iskPublicKey", config.get("signingCertificateFile")) + + signature_provider = get_signature_provider( + signature_provider, + main_root_private_key_file, + search_paths=search_paths, + ) + isk_cert = load_binary(isk_certificate, search_paths=search_paths) + self_signed = config.get("selfSigned", True) + cert_block = cls( + signature_provider=signature_provider, + isk_cert=isk_cert, + self_signed=self_signed, + ) + + return cert_block + + def validate(self) -> None: + """Validate the settings of class members. + + :raises SPSDKError: Invalid configuration of certification block class members. + """ + if self.isk_certificate and not self.isk_certificate.signature: + if not isinstance(self.signature_provider, SignatureProvider): + raise SPSDKError("Invalid ISK certificate.") + + @staticmethod + def generate_config_template(_family: Optional[str] = None) -> str: + """Generate configuration for certification block vX.""" + val_schemas = CertBlockVx.get_validation_schemas() + val_schemas.append( + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + ) + return CommentedConfig("Certification Block Vx template", val_schemas).get_template() + + @classmethod + def get_supported_families(cls) -> List[str]: + """Get list of supported families.""" + return super()._get_supported_families("cert_block_x") + + def get_otp_script(self) -> str: + """Return script for writing certificate hash to OTP. + + :return: string value of blhost script + """ + ret = ( + "# BLHOST Cert Block Vx fuses programming script\n" + f"# Generated by SPSDK {spsdk_version}\n" + f"# ISK Cert hash [0:127]: {self.cert_hash.hex()} \n\n" + ) + + fuse_value = change_endianness(self.cert_hash) + fuse_idx = 12 # Fuse start IDX + for fuse_data in split_data(fuse_value, 4): + ret += f"flash-program-once {hex(fuse_idx)} 4 {fuse_data.hex()}\n" + fuse_idx += 1 + + return ret + + +def find_root_certificates(config: Dict[str, Any]) -> List[str]: + """Find all root certificates in configuration. + + :param config: Configuration to be searched. + :raises SPSDKError: If invalid configuration is provided. + :return: List of root certificates. + """ + root_certificates_loaded: List[Optional[str]] = [ + config.get(f"rootCertificate{idx}File") for idx in range(4) + ] + # filter out None and empty values + root_certificates = list(filter(None, root_certificates_loaded)) + for org, filtered in zip(root_certificates_loaded, root_certificates): + if org != filtered: + raise SPSDKError("There are gaps in rootCertificateXFile definition") + return root_certificates + + +def get_keys_or_rotkh_from_certblock_config( + rot: Optional[str], family: Optional[str] +) -> Tuple[Optional[Iterable[str]], Optional[bytes]]: + """Get keys or ROTKH value from ROT config. + + ROT config might be cert block config or MBI config. + There are four cases how cert block might be configured. + + 1. MBI with certBlock property pointing to YAML file + 2. MBI with certBlock property pointing to BIN file + 3. YAML configuration of cert block + 4. Binary cert block + + :param rot: Path to ROT configuration (MBI or cert block) + or path to binary cert block + :param family: MCU family + :raises SPSDKError: In case the ROTKH or keys cannot be parsed + :return: Tuple containing root of trust (list of paths to keys) + or ROTKH in case of binary cert block + """ + root_of_trust = None + rotkh = None + if rot and family: + logger.info("Loading configuration from cert block/MBI file...") + config_dir = os.path.dirname(rot) + try: + config_data = load_configuration(rot, search_paths=[config_dir]) + if "certBlock" in config_data: + try: + config_data = load_configuration( + config_data["certBlock"], search_paths=[config_dir] + ) + except SPSDKError: + cert_block = load_binary(config_data["certBlock"], search_paths=[config_dir]) + parsed_cert_block = CertBlock.get_cert_block_class(family).parse(cert_block) + rotkh = parsed_cert_block.rkth + public_keys = find_root_certificates(config_data) + root_of_trust = tuple((find_file(x, search_paths=[config_dir]) for x in public_keys)) + except SPSDKError: + logger.debug("Parsing ROT from config did not succeed, trying it as binary") + try: + cert_block = load_binary(rot, search_paths=[config_dir]) + parsed_cert_block = CertBlock.get_cert_block_class(family).parse(cert_block) + rotkh = parsed_cert_block.rkth + except SPSDKError as e: + raise SPSDKError(f"Parsing of binary cert block failed with {e}") from e + + return root_of_trust, rotkh diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py new file mode 100644 index 00000000..197bf5ed --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py @@ -0,0 +1,803 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""The module provides support for IEE for RTxxxx devices.""" + +import logging +from copy import deepcopy +from struct import pack +from typing import Any, Dict, List, Optional, Union + +from crcmod.predefined import mkPredefinedCrcFun + +from spsdk import version as spsdk_version +from spsdk.apps.utils.utils import filepath_from_config +from spsdk.crypto.rng import random_bytes +from spsdk.crypto.symmetric import Counter, aes_ctr_encrypt, aes_xts_encrypt +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file +from spsdk.utils.images import BinaryImage +from spsdk.utils.misc import ( + Endianness, + align_block, + load_hex_string, + reverse_bytes_in_longs, + split_data, + value_to_bytes, + value_to_int, +) +from spsdk.utils.registers import Registers +from spsdk.utils.schema_validator import CommentedConfig +from spsdk.utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + + +class IeeKeyBlobLockAttributes(SpsdkEnum): + """IEE keyblob lock attributes.""" + + LOCK = (0x95, "LOCK") # IEE region lock. + UNLOCK = (0x59, "UNLOCK") # IEE region unlock. + + +class IeeKeyBlobKeyAttributes(SpsdkEnum): + """IEE keyblob key attributes.""" + + CTR128XTS256 = (0x5A, "CTR128XTS256") # AES 128 bits (CTR), 256 bits (XTS) + CTR256XTS512 = (0xA5, "CTR256XTS512") # AES 256 bits (CTR), 512 bits (XTS) + + +class IeeKeyBlobModeAttributes(SpsdkEnum): + """IEE Keyblob mode attributes.""" + + Bypass = (0x6A, "Bypass") # AES encryption/decryption bypass + AesXTS = (0xA6, "AesXTS") # AES XTS mode + AesCTRWAddress = (0x66, "AesCTRWAddress") # AES CTR w address binding mode + AesCTRWOAddress = (0xAA, "AesCTRWOAddress") # AES CTR w/o address binding mode + AesCTRkeystream = (0x19, "AesCTRkeystream") # AES CTR keystream only + + +class IeeKeyBlobWritePmsnAttributes(SpsdkEnum): + """IEE keblob write permission attributes.""" + + ENABLE = (0x99, "ENABLE") # Enable write permission in APC IEE + DISABLE = (0x11, "DISABLE") # Disable write permission in APC IEE + + +class IeeKeyBlobAttribute: + """IEE Keyblob Attribute. + + | typedef struct _iee_keyblob_attribute + | { + | uint8_t lock; # IEE Region Lock control flag. + | uint8_t keySize; # IEE AES key size. + | uint8_t aesMode; # IEE AES mode. + | uint8_t reserved; # Reserved. + | } iee_keyblob_attribute_t; + """ + + _FORMAT = " None: + """IEE keyblob constructor. + + :param lock: IeeKeyBlobLockAttributes + :param key_attribute: IeeKeyBlobKeyAttributes + :param aes_mode: IeeKeyBlobModeAttributes + """ + self.lock = lock + self.key_attribute = key_attribute + self.aes_mode = aes_mode + + @property + def ctr_mode(self) -> bool: + """Return true if AES mode is CTR. + + :return: True if AES-CTR, false otherwise + """ + if self.aes_mode in [ + IeeKeyBlobModeAttributes.AesCTRWAddress, + IeeKeyBlobModeAttributes.AesCTRWOAddress, + IeeKeyBlobModeAttributes.AesCTRkeystream, + ]: + return True + return False + + @property + def key1_size(self) -> int: + """Return IEE key size based on selected mode. + + :return: Key size in bytes + """ + if self.key_attribute == IeeKeyBlobKeyAttributes.CTR128XTS256: + return 16 + return 32 + + @property + def key2_size(self) -> int: + """Return IEE key size based on selected mode. + + :return: Key size in bytes + """ + if self.key_attribute == IeeKeyBlobKeyAttributes.CTR128XTS256: + return 16 + if self.ctr_mode: + return 16 + return 32 + + def export(self) -> bytes: + """Export binary representation of KeyBlobAttribute. + + :return: serialized binary data + """ + return pack(self._FORMAT, self.lock.tag, self.key_attribute.tag, self.aes_mode.tag, 0) + + +class IeeKeyBlob: + """IEE KeyBlob. + + | typedef struct _iee_keyblob_ + | { + | uint32_t header; # IEE Key Blob header tag. + | uint32_t version; # IEE Key Blob version, upward compatible. + | iee_keyblob_attribute_t attribute; # IEE configuration attribute. + | uint32_t pageOffset; # IEE page offset. + | uint32_t key1[IEE_MAX_AES_KEY_SIZE_IN_BYTE / + | sizeof(uint32_t)]; # Encryption key1 for XTS-AES mode, encryption key for AES-CTR mode. + | uint32_t key2[IEE_MAX_AES_KEY_SIZE_IN_BYTE / + | sizeof(uint32_t)]; # Encryption key2 for XTS-AES mode, initial counter for AES-CTR mode. + | uint32_t startAddr; # Physical address of encryption region. + | uint32_t endAddr; # Physical address of encryption region. + | uint32_t reserved; # Reserved word. + | uint32_t crc32; # Entire IEE Key Blob CRC32 value. Must be the last struct member. + | } iee_keyblob_t + """ + + _FORMAT = "LL4BL8L8LLLLL96B" + + HEADER_TAG = 0x49454542 + # Tag used in keyblob header + # (('I' << 24) | ('E' << 16) | ('E' << 8) | ('B' << 0)) + KEYBLOB_VERSION = 0x56010000 + # Identifier of IEE keyblob version + # (('V' << 24) | (1 << 16) | (0 << 8) | (0 << 0)) + KEYBLOB_OFFSET = 0x1000 + + _IEE_ENCR_BLOCK_SIZE_XTS = 0x1000 + + _ENCRYPTION_BLOCK_SIZE = 0x10 + + _START_ADDR_MASK = 0x400 - 1 + # Region addresses are modulo 1024 + + _END_ADDR_MASK = 0x3F8 + + def __init__( + self, + attributes: IeeKeyBlobAttribute, + start_addr: int, + end_addr: int, + key1: Optional[bytes] = None, + key2: Optional[bytes] = None, + page_offset: int = 0, + crc: Optional[bytes] = None, + ): + """Constructor. + + :param attributes: IEE keyblob attributes + :param start_addr: start address of the region + :param end_addr: end address of the region + :param key1: Encryption key1 for XTS-AES mode, encryption key for AES-CTR mode. + :param key2: Encryption key2 for XTS-AES mode, initial_counter for AES-CTR mode. + :param crc: optional value for unused CRC fill (for testing only); None to use calculated value + :raises SPSDKError: Start or end address are not aligned + :raises SPSDKError: When there is invalid key + :raises SPSDKError: When there is invalid start/end address + """ + self.attributes = attributes + + if key1 is None: + key1 = random_bytes(self.attributes.key1_size) + if key2 is None: + key2 = random_bytes(self.attributes.key2_size) + + key1 = value_to_bytes(key1, byte_cnt=self.attributes.key1_size) + key2 = value_to_bytes(key2, byte_cnt=self.attributes.key2_size) + + if start_addr < 0 or start_addr > end_addr or end_addr > 0xFFFFFFFF: + raise SPSDKError("Invalid start/end address") + + if (start_addr & self._START_ADDR_MASK) != 0: + raise SPSDKError( + f"Start address must be aligned to {hex(self._START_ADDR_MASK + 1)} boundary" + ) + + self.start_addr = start_addr + self.end_addr = end_addr + + self.key1 = key1 + self.key2 = key2 + self.page_offset = page_offset + + self.crc_fill = crc + + def __str__(self) -> str: + """Text info about the instance.""" + msg = "" + msg += f"KEY 1: {self.key1.hex()}\n" + msg += f"KEY 2: {self.key2.hex()}\n" + msg += f"Start Addr: {hex(self.start_addr)}\n" + msg += f"End Addr: {hex(self.end_addr)}\n" + return msg + + def plain_data(self) -> bytes: + """Plain data for selected key range. + + :return: key blob exported into binary form (serialization) + """ + result = bytes() + result += pack(" bool: + """Whether key blob contains specified address. + + :param addr: to be tested + :return: True if yes, False otherwise + """ + return self.start_addr <= addr <= self.end_addr + + def matches_range(self, image_start: int, image_end: int) -> bool: + """Whether key blob matches address range of the image to be encrypted. + + :param image_start: start address of the image + :param image_end: last address of the image + :return: True if yes, False otherwise + """ + return self.contains_addr(image_start) and self.contains_addr(image_end) + + def encrypt_image_xts(self, base_address: int, data: bytes) -> bytes: + """Encrypt specified data using AES-XTS. + + :param base_address: of the data in target memory; must be >= self.start_addr + :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr + :return: encrypted data + """ + encrypted_data = bytes() + current_start = base_address + key1 = reverse_bytes_in_longs(self.key1) + key2 = reverse_bytes_in_longs(self.key2) + + for block in split_data(bytearray(data), self._IEE_ENCR_BLOCK_SIZE_XTS): + tweak = self.calculate_tweak(current_start) + + encrypted_block = aes_xts_encrypt( + key1 + key2, + block, + tweak, + ) + encrypted_data += encrypted_block + current_start += len(block) + + return encrypted_data + + def encrypt_image_ctr(self, base_address: int, data: bytes) -> bytes: + """Encrypt specified data using AES-CTR. + + :param base_address: of the data in target memory; must be >= self.start_addr + :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr + :return: encrypted data + """ + encrypted_data = bytes() + key = reverse_bytes_in_longs(self.key1) + nonce = reverse_bytes_in_longs(self.key2) + + counter = Counter(nonce, ctr_value=base_address >> 4, ctr_byteorder_encoding=Endianness.BIG) + + for block in split_data(bytearray(data), self._ENCRYPTION_BLOCK_SIZE): + encrypted_block = aes_ctr_encrypt( + key, + block, + counter.value, + ) + encrypted_data += encrypted_block + counter.increment(self._ENCRYPTION_BLOCK_SIZE >> 4) + + return encrypted_data + + def encrypt_image(self, base_address: int, data: bytes) -> bytes: + """Encrypt specified data. + + :param base_address: of the data in target memory; must be >= self.start_addr + :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr + :return: encrypted data + :raises SPSDKError: If start address is not valid + :raises NotImplementedError: AES-CTR is not implemented yet + """ + if base_address % 16 != 0: + raise SPSDKError("Invalid start address") # Start address has to be 16 byte aligned + data = align_block(data, self._ENCRYPTION_BLOCK_SIZE) # align data length + data_len = len(data) + + # check start and end addresses + if not self.matches_range(base_address, base_address + data_len - 1): + logger.warning( + f"Image address range is not within key blob: {hex(self.start_addr)}-{hex(self.end_addr)}." + ) + + if self.attributes.ctr_mode: + return self.encrypt_image_ctr(base_address, data) + return self.encrypt_image_xts(base_address, data) + + @staticmethod + def calculate_tweak(address: int) -> bytes: + """Calculate tweak value for AES-XTS encryption based on the address value. + + :param address: start address of encryption + :return: 16 byte tweak values + """ + sector = address >> 12 + tweak = bytearray(16) + for n in range(16): + tweak[n] = sector & 0xFF + sector = sector >> 8 + return bytes(tweak) + + +class Iee: + """IEE: Inline Encryption Engine.""" + + IEE_DATA_UNIT = 0x1000 + IEE_KEY_BLOBS_SIZE = 384 + + def __init__(self) -> None: + """Constructor.""" + self._key_blobs: List[IeeKeyBlob] = [] + + def __getitem__(self, index: int) -> IeeKeyBlob: + return self._key_blobs[index] + + def __setitem__(self, index: int, value: IeeKeyBlob) -> None: + self._key_blobs.remove(self._key_blobs[index]) + self._key_blobs.insert(index, value) + + def add_key_blob(self, key_blob: IeeKeyBlob) -> None: + """Add key for specified address range. + + :param key_blob: to be added + """ + self._key_blobs.append(key_blob) + + def encrypt_image(self, image: bytes, base_addr: int) -> bytes: + """Encrypt image with all available keyblobs. + + :param image: plain image to be encrypted + :param base_addr: where the image will be located in target processor + :return: encrypted image + """ + encrypted_data = bytearray(image) + addr = base_addr + for block in split_data(image, self.IEE_DATA_UNIT): + for key_blob in self._key_blobs: + if key_blob.matches_range(addr, addr + len(block)): + logger.debug( + f"Encrypting {hex(addr)}:{hex(len(block) + addr)}" + f" with keyblob: \n {str(key_blob)}" + ) + encrypted_data[ + addr - base_addr : len(block) + addr - base_addr + ] = key_blob.encrypt_image(addr, block) + addr += len(block) + + return bytes(encrypted_data) + + def get_key_blobs(self) -> bytes: + """Get key blobs. + + :return: Binary key blobs joined together + """ + result = bytes() + for key_blob in self._key_blobs: + result += key_blob.plain_data() + + # return result + return align_block(result, self.IEE_KEY_BLOBS_SIZE) + + def encrypt_key_blobs( + self, + ibkek1: Union[bytes, str], + ibkek2: Union[bytes, str], + keyblob_address: int, + ) -> bytes: + """Encrypt keyblobs and export them as binary. + + :param ibkek1: key encryption key AES-XTS 256 bit + :param ibkek2: key encryption key AES-XTS 256 bit + :param keyblob_address: keyblob base address + :return: encrypted keyblobs + """ + plain_key_blobs = self.get_key_blobs() + + ibkek1 = reverse_bytes_in_longs(value_to_bytes(ibkek1, byte_cnt=32)) + logger.debug(f"IBKEK1: {' '.join(f'{b:02x}' for b in ibkek1)}") + ibkek2 = reverse_bytes_in_longs(value_to_bytes(ibkek2, byte_cnt=32)) + logger.debug(f"IBKEK2 {' '.join(f'{b:02x}' for b in ibkek2)}") + + tweak = IeeKeyBlob.calculate_tweak(keyblob_address) + return aes_xts_encrypt( + ibkek1 + ibkek2, + plain_key_blobs, + tweak, + ) + + +class IeeNxp(Iee): + """IEE: Inline Encryption Engine.""" + + def __init__( + self, + family: str, + keyblob_address: int, + ibkek1: Union[bytes, str], + ibkek2: Union[bytes, str], + key_blobs: Optional[List[IeeKeyBlob]] = None, + binaries: Optional[BinaryImage] = None, + ) -> None: + """Constructor. + + :param family: Device family + :param ibkek1: 256 bit key to encrypt IEE keyblob + :param ibkek2: 256 bit key to encrypt IEE keyblob + :param key_blobs: Optional Key blobs to add to IEE, defaults to None + :raises SPSDKValueError: Unsupported family + """ + super().__init__() + + if family not in self.get_supported_families(): + raise SPSDKValueError(f"Unsupported family{family} by IEE") + + self.family = family + self.ibkek1 = bytes.fromhex(ibkek1) if isinstance(ibkek1, str) else ibkek1 + self.ibkek2 = bytes.fromhex(ibkek2) if isinstance(ibkek2, str) else ibkek2 + self.keyblob_address = keyblob_address + self.binaries = binaries + + self.db = get_db(family, "latest") + self.blobs_min_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_min_cnt") + self.blobs_max_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_max_cnt") + self.generate_keyblob = self.db.get_bool(DatabaseManager.IEE, "generate_keyblob") + + if key_blobs: + for key_blob in key_blobs: + self.add_key_blob(key_blob) + + def export_key_blobs(self) -> bytes: + """Export encrypted keyblobs in binary. + + :return: Encrypted keyblobs + """ + return self.encrypt_key_blobs(self.ibkek1, self.ibkek2, self.keyblob_address) + + def export_image(self) -> Optional[BinaryImage]: + """Export encrypted image. + + :return: Encrypted image + """ + if self.binaries is None: + return None + self.binaries.validate() + + binaries: BinaryImage = deepcopy(self.binaries) + + for binary in binaries.sub_images: + if binary.binary: + binary.binary = self.encrypt_image( + binary.binary, binary.absolute_address + self.keyblob_address + ) + for segment in binary.sub_images: + if segment.binary: + segment.binary = self.encrypt_image( + segment.binary, + segment.absolute_address + self.keyblob_address, + ) + + binaries.validate() + return binaries + + def get_blhost_script_otp_kek(self) -> str: + """Create BLHOST script to load fuses needed to run IEE with OTP fuses. + + :return: BLHOST script that loads the keys into fuses. + """ + if not self.db.get_bool(DatabaseManager.IEE, "has_kek_fuses", default=False): + logger.debug(f"The {self.family} has no IEE KEK fuses") + return "" + + xml_fuses = self.db.get_file_path(DatabaseManager.IEE, "reg_fuses", default=None) + if not xml_fuses: + logger.debug(f"The {self.family} has no IEE fuses definition") + return "" + + fuses = Registers(self.family, base_endianness=Endianness.LITTLE) + grouped_regs = self.db.get_list(DatabaseManager.IEE, "grouped_registers", default=None) + + fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) + fuses.find_reg("USER_KEY1").set_value(self.ibkek1) + fuses.find_reg("USER_KEY2").set_value(self.ibkek2) + + load_iee = fuses.find_reg("LOAD_IEE_KEY") + load_iee.find_bitfield("LOAD_IEE_KEY_BITFIELD").set_value(1) + + encrypt_engine = fuses.find_reg("ENCRYPT_XIP_ENGINE") + encrypt_engine.find_bitfield("ENCRYPT_XIP_ENGINE_BITFIELD").set_value(1) + + boot_cfg = fuses.find_reg("BOOT_CFG") + boot_cfg.find_bitfield("ENCRYPT_XIP_EN_BITFIELD").set_value(1) + + ibkek_lock = fuses.find_reg("USER_KEY_RLOCK") + ibkek_lock.find_bitfield("USER_KEY1_RLOCK").set_value(1) + ibkek_lock.find_bitfield("USER_KEY2_RLOCK").set_value(1) + + ret = ( + "# BLHOST IEE fuses programming script\n" + f"# Generated by SPSDK {spsdk_version}\n" + f"# Chip: {self.family} \n\n" + ) + + ret += f"# OTP IBKEK1: {self.ibkek1.hex()}\n\n" + for reg in fuses.find_reg("USER_KEY1").sub_regs: + ret += f"# {reg.name} fuse.\n" + ret += f"efuse-program-once {hex(reg.offset)} 0x{reg.get_hex_value(raw=True)} --no-verify\n" + + ret += f"\n\n# OTP IBKEK2: {self.ibkek2.hex()}\n\n" + for reg in fuses.find_reg("USER_KEY2").sub_regs: + ret += f"# {reg.name} fuse.\n" + ret += f"efuse-program-once {hex(reg.offset)} 0x{reg.get_hex_value(raw=True)} --no-verify\n" + + ret += f"\n\n# {load_iee.name} fuse.\n" + for bitfield in load_iee.get_bitfields(): + ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" + ret += f"efuse-program-once {hex(load_iee.offset)} 0x{load_iee.get_hex_value(raw=True)} --no-verify\n" + + ret += f"\n\n# {encrypt_engine.name} fuse.\n" + for bitfield in encrypt_engine.get_bitfields(): + ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" + ret += ( + f"efuse-program-once {hex(encrypt_engine.offset)} " + f"0x{encrypt_engine.get_hex_value(raw=True)} --no-verify\n" + ) + + ret += f"\n\n# {ibkek_lock.name} fuse.\n" + for bitfield in ibkek_lock.get_bitfields(): + ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" + ret += f"efuse-program-once {hex(ibkek_lock.offset)} 0x{ibkek_lock.get_hex_value(raw=True)} --no-verify\n" + + ret += f"\n\n# {boot_cfg.name} fuse.\n" + ret += "WARNING!! Check SRM and set all desired bitfields for boot configuration" + for bitfield in boot_cfg.get_bitfields(): + ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" + ret += ( + f"# efuse-program-once {hex(boot_cfg.offset)} " + f"0x{boot_cfg.get_hex_value(raw=True)} --no-verify\n" + ) + + return ret + + def binary_image( + self, + plain_data: bool = False, + data_alignment: int = 16, + keyblob_name: str = "iee_keyblob.bin", + image_name: str = "encrypted.bin", + ) -> BinaryImage: + """Get the IEE Binary Image representation. + + :param plain_data: Binary representation in plain format, defaults to False + :param data_alignment: Alignment of data part key blobs. + :param keyblob_name: Filename of the IEE keyblob + :param image_name: Filename of the IEE image + :return: IEE in BinaryImage. + """ + iee = BinaryImage(image_name, offset=self.keyblob_address) + if self.generate_keyblob: + # Add mandatory IEE keyblob + iee_keyblobs = self.get_key_blobs() if plain_data else self.export_key_blobs() + iee.add_image( + BinaryImage( + keyblob_name, + offset=0, + description=f"IEE keyblobs {self.family}", + binary=iee_keyblobs, + ) + ) + binaries = self.export_image() + + if binaries: + binaries.alignment = data_alignment + binaries.validate() + iee.add_image(binaries) + + return iee + + @staticmethod + def get_supported_families() -> List[str]: + """Get all supported families for AHAB container. + + :return: List of supported families. + """ + return get_families(DatabaseManager.IEE) + + @staticmethod + def get_validation_schemas(family: str) -> List[Dict[str, Any]]: + """Get list of validation schemas. + + :param family: Family for which the template should be generated. + :return: Validation list of schemas. + """ + if family not in IeeNxp.get_supported_families(): + return [] + + database = get_db(family, "latest") + schemas = get_schema_file(DatabaseManager.IEE) + family_sch = schemas["iee_family"] + family_sch["properties"]["family"]["enum"] = IeeNxp.get_supported_families() + family_sch["properties"]["family"]["template_value"] = family + ret = [family_sch, schemas["iee_output"], schemas["iee"]] + additional_schemes = database.get_list( + DatabaseManager.IEE, "additional_template", default=[] + ) + ret.extend([schemas[x] for x in additional_schemes]) + return ret + + @staticmethod + def get_validation_schemas_family() -> List[Dict[str, Any]]: + """Get list of validation schemas for family key. + + :return: Validation list of schemas. + """ + schemas = get_schema_file(DatabaseManager.IEE) + family_sch = schemas["iee_family"] + family_sch["properties"]["family"]["enum"] = IeeNxp.get_supported_families() + return [family_sch] + + @staticmethod + def generate_config_template(family: str) -> Dict[str, Any]: + """Generate IEE configuration template. + + :param family: Family for which the template should be generated. + :return: Dictionary of individual templates (key is name of template, value is template itself). + """ + val_schemas = IeeNxp.get_validation_schemas(family) + database = get_db(family, "latest") + + if val_schemas: + template_note = database.get_str( + DatabaseManager.IEE, "additional_template_text", default="" + ) + title = f"IEE: Inline Encryption Engine Configuration template for {family}." + + yaml_data = CommentedConfig(title, val_schemas, note=template_note).get_template() + + return {f"{family}_iee": yaml_data} + + return {} + + @staticmethod + def load_from_config( + config: Dict[str, Any], config_dir: str, search_paths: Optional[List[str]] = None + ) -> "IeeNxp": + """Converts the configuration option into an IEE image object. + + "config" content array of containers configurations. + + :param config: array of IEE configuration dictionaries. + :param config_dir: directory where the config is located + :param search_paths: List of paths where to search for the file, defaults to None + :return: initialized IEE object. + """ + iee_config: List[Dict[str, Any]] = config.get("key_blobs", [config.get("key_blob")]) + family = config["family"] + ibkek1 = load_hex_string( + config.get( + "ibkek1", + "0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + ), + 32, + ) + ibkek2 = load_hex_string( + config.get( + "ibkek2", + "0x202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", + ), + 32, + ) + + logger.debug(f"Loaded IBKEK1: {ibkek1.hex()}") + logger.debug(f"Loaded IBKEK2: {ibkek2.hex()}") + + keyblob_address = value_to_int(config["keyblob_address"]) + start_address = min( + [value_to_int(addr.get("start_address", 0xFFFFFFFF)) for addr in iee_config] + ) + + data_blobs: Optional[List[Dict]] = config.get("data_blobs") + binaries = None + if data_blobs: + # start address to calculate offset from keyblob, min from keyblob or data blob address + # pylint: disable-next=nested-min-max + start_address = min( + min([value_to_int(addr.get("address", 0xFFFFFFFF)) for addr in data_blobs]), + start_address, + ) + binaries = BinaryImage( + filepath_from_config( + config, "encrypted_name", "encrypted_blobs", config_dir, config["output_folder"] + ), + offset=start_address - keyblob_address, + alignment=IeeKeyBlob._ENCRYPTION_BLOCK_SIZE, + ) + for data_blob in data_blobs: + address = value_to_int( + data_blob.get("address", 0), keyblob_address + binaries.offset + ) + + binary = BinaryImage.load_binary_image( + path=data_blob["data"], + search_paths=search_paths, + offset=address - keyblob_address - binaries.offset, + alignment=IeeKeyBlob._ENCRYPTION_BLOCK_SIZE, + size=0, + ) + + binaries.add_image(binary) + + iee = IeeNxp(family, keyblob_address, ibkek1, ibkek2, binaries=binaries) + + for key_blob_cfg in iee_config: + aes_mode = key_blob_cfg["aes_mode"] + region_lock = "LOCK" if key_blob_cfg.get("region_lock") else "UNLOCK" + key_size = key_blob_cfg["key_size"] + + attributes = IeeKeyBlobAttribute( + IeeKeyBlobLockAttributes.from_label(region_lock), + IeeKeyBlobKeyAttributes.from_label(key_size), + IeeKeyBlobModeAttributes.from_label(aes_mode), + ) + + key1 = load_hex_string(key_blob_cfg["key1"], attributes.key1_size) + key2 = load_hex_string(key_blob_cfg["key2"], attributes.key2_size) + + start_addr = value_to_int(key_blob_cfg.get("start_address", start_address)) + end_addr = value_to_int(key_blob_cfg.get("end_address", 0xFFFFFFFF)) + page_offset = value_to_int(key_blob_cfg.get("page_offset", 0)) + + iee.add_key_blob( + IeeKeyBlob( + attributes=attributes, + start_addr=start_addr, + end_addr=end_addr, + key1=key1, + key2=key2, + page_offset=page_offset, + ) + ) + + return iee diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py new file mode 100644 index 00000000..38e4cc50 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py @@ -0,0 +1,940 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""The module provides support for On-The-Fly encoding for RTxxx devices.""" + +import logging +import os +from copy import deepcopy +from struct import pack +from typing import Any, Dict, List, Optional, Union + +from crcmod.predefined import mkPredefinedCrcFun + +from spsdk import version as spsdk_version +from spsdk.apps.utils.utils import filepath_from_config +from spsdk.crypto.rng import random_bytes +from spsdk.crypto.symmetric import Counter, aes_ctr_encrypt, aes_key_wrap +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file +from spsdk.utils.exceptions import SPSDKRegsErrorBitfieldNotFound +from spsdk.utils.images import BinaryImage +from spsdk.utils.misc import ( + Endianness, + align_block, + load_binary, + load_hex_string, + reverse_bits_in_bytes, + split_data, + value_to_bytes, + value_to_int, +) +from spsdk.utils.registers import Registers +from spsdk.utils.schema_validator import CommentedConfig + +logger = logging.getLogger(__name__) + + +class KeyBlob: + """OTFAD KeyBlob: The class specifies AES key and counter initial value for specified address range. + + | typedef struct KeyBlob + | { + | unsigned char key[kAesKeySizeBytes]; // 16 bytes, 128-bits, KEY[A15...A00] + | unsigned char ctr[kCtrSizeBytes]; // 8 bytes, 64-bits, CTR[C7...C0] + | unsigned int srtaddr; // region start, SRTADDR[31 - 10] + | unsigned int endaddr; // region end, ENDADDR[31 - 10]; lowest three bits are used as flags + | // end of 32-byte area covered by CRC + | unsigned int zero_fill; // zeros + | unsigned int key_blob_crc32; // crc32 over 1st 32-bytes + | // end of 40 byte (5*64-bit) key blob data + | unsigned char expanded_wrap_data[8]; // 8 bytes, used for wrap expanded data + | // end of 48 byte (6*64-bit) wrap data + | unsigned char unused_filler[16]; // unused fill to 64 bytes + | } keyblob_t; + """ + + _START_ADDR_MASK = 0x400 - 1 + # Region addresses are modulo 1024 + # The address ends with RO, ADE, VLD bits. From this perspective, only + # bits [9:3] must be set to 1. The rest is configurable. + _END_ADDR_MASK = 0x3F8 + + # Key flags mask: RO, ADE, VLD + _KEY_FLAG_MASK = 0x07 + # This field signals that the entire set of context registers (CTXn_KEY[0-3], CTXn_CTR[0-1], + # CTXn_RGD_W[0-1] are read-only and cannot be modified. This field is sticky and remains + # asserted until the next system reset. SR[RRAM] provides another level of register access + # control and is independent of the RO indicator. + KEY_FLAG_READ_ONLY = 0x4 + # AES Decryption Enable: For accesses hitting in a valid context, this bit indicates if the fetched data is to be + # decrypted or simply bypassed. + KEY_FLAG_ADE = 0x2 + # Valid: This field signals if the context is valid or not. + KEY_FLAG_VLD = 0x1 + + # key length in bytes + KEY_SIZE = 16 + # counter length in bytes + CTR_SIZE = 8 + # len of counter init value for export + _EXPORT_CTR_IV_SIZE = 8 + # this constant seems to be fixed for SB2.1 + _EXPORT_NBLOCKS_5 = 5 + # binary export size + _EXPORT_KEY_BLOB_SIZE = 64 + # QSPI image alignment length, 512 is supposed to be the safe alignment level for any QSPI device + # this means that all QSPI images generated by this tool will be sizes of multiple 512 + _IMAGE_ALIGNMENT = 512 + # Encryption block size + _ENCRYPTION_BLOCK_SIZE = 16 + + def __init__( + self, + start_addr: int, + end_addr: int, + key: Optional[bytes] = None, + counter_iv: Optional[bytes] = None, + key_flags: int = KEY_FLAG_VLD | KEY_FLAG_ADE, + # for testing + zero_fill: Optional[bytes] = None, + crc: Optional[bytes] = None, + ): + """Constructor. + + :param start_addr: start address of the region + :param end_addr: end address of the region + :param key_flags: see KEY_FLAG_xxx constants; default flags: RO = 0, ADE = 1, VLD = 1 + :param key: optional AES key; None to use random value + :param counter_iv: optional counter init value for AES; None to use random value + :param binaries: optional data chunks of this key blob + :param zero_fill: optional value for zero_fill (for testing only); None to use random value (recommended) + :param crc: optional value for unused CRC fill (for testing only); None to use random value (recommended) + :raises SPSDKError: Start or end address are not aligned + :raises SPSDKError: When there is invalid key + :raises SPSDKError: When there is invalid start/end address + :raises SPSDKError: When key_flags exceeds mask + """ + if key is None: + key = random_bytes(self.KEY_SIZE) + if counter_iv is None: + counter_iv = random_bytes(self.CTR_SIZE) + if (len(key) != self.KEY_SIZE) and (len(counter_iv) != self.CTR_SIZE): + raise SPSDKError("Invalid key") + if start_addr < 0 or start_addr > end_addr or end_addr > 0xFFFFFFFF: + raise SPSDKError("Invalid start/end address") + if key_flags & ~self._KEY_FLAG_MASK != 0: + raise SPSDKError(f"key_flags exceeds mask {hex(self._KEY_FLAG_MASK)}") + if (start_addr & self._START_ADDR_MASK) != 0: + raise SPSDKError( + f"Start address must be aligned to {hex(self._START_ADDR_MASK + 1)} boundary" + ) + # if (end_addr & self._END_ADDR_MASK) != self._END_ADDR_MASK: + # raise SPSDKError(f"End address must be aligned to {hex(self._END_ADDR_MASK)} boundary") + self.key = key + self.ctr_init_vector = counter_iv + self.start_addr = start_addr + self.end_addr = end_addr + self.key_flags = key_flags + self.zero_fill = zero_fill + self.crc_fill = crc + + def __str__(self) -> str: + """Text info about the instance.""" + msg = "" + msg += f"Key: {self.key.hex()}\n" + msg += f"Counter IV: {self.ctr_init_vector.hex()}\n" + msg += f"Start Addr: {hex(self.start_addr)}\n" + msg += f"End Addr: {hex(self.end_addr)}\n" + return msg + + def plain_data(self) -> bytes: + """Plain data for selected key range. + + :return: key blob exported into binary form (serialization) + :raises SPSDKError: Invalid value of zero fill parameter + :raises SPSDKError: Invalid value crc + :raises SPSDKError: Invalid length binary data + """ + result = bytes() + result += self.key + result += self.ctr_init_vector + result += pack(" bytes: + """Creates key wrap for the key blob. + + :param kek: key to encode; 16 bytes long + :param iv: counter initialization vector; 8 bytes; optional, OTFAD uses empty init value + :param byte_swap_cnt: Encrypted keyblob reverse byte count, 0 means NO reversing is enabled + :return: Serialized key blob + :raises SPSDKError: If any parameter is not valid + :raises SPSDKError: If length of kek is not valid + :raises SPSDKError: If length of data is not valid + """ + if isinstance(kek, str): + kek = bytes.fromhex(kek) + if len(kek) != 16: + raise SPSDKError("Invalid length of kek") + if len(iv) != self._EXPORT_CTR_IV_SIZE: + raise SPSDKError("Invalid length of initialization vector") + n = self._EXPORT_NBLOCKS_5 + plaintext = self.plain_data() # input data to be encrypted + if len(plaintext) < n * 8: + raise SPSDKError("Invalid length of data to be encrypted") + + blobs = bytes() + wrap = aes_key_wrap(kek, plaintext[:40]) + if byte_swap_cnt > 0: + for i in range(0, len(wrap), byte_swap_cnt): + blobs += wrap[i : i + byte_swap_cnt][::-1] + else: + blobs += wrap + + return align_block( + blobs, self._EXPORT_KEY_BLOB_SIZE, padding=0 + ) # align to 64 bytes (0 padding) + + def _get_ctr_nonce(self) -> bytes: + """Get the counter initial value for image encryption. + + :return: counter bytes + :raises SPSDKError: If length of counter is not valid + """ + # CTRn_x[127-0] = {CTR_W0_x[C0...C3], // 32 bits of pre-programmed CTR + # CTR_W1_x[C4...C7], // another 32 bits of CTR + # CTR_W0_x[C0...C3] ^ CTR_W1_x[C4...C7], // exclusive-OR of CTR values + # systemAddress[31-4], 0000b // 0-modulo-16 system address */ + + if len(self.ctr_init_vector) != 8: + raise SPSDKError("Invalid length of counter init") + + result = bytearray(16) + result[:4] = self.ctr_init_vector[:4] + result[4:8] = self.ctr_init_vector[4:] + for i in range(0, 4): + result[8 + i] = self.ctr_init_vector[0 + i] ^ self.ctr_init_vector[4 + i] + + # result[15:12] = start_addr as a counter; nonce has these bytes zero and value passes as counter init value + + return bytes(result) + + def contains_addr(self, addr: int) -> bool: + """Whether key blob contains specified address. + + :param addr: to be tested + :return: True if yes, False otherwise + """ + return self.start_addr <= addr <= self.end_addr + + def matches_range(self, image_start: int, image_end: int) -> bool: + """Whether key blob matches address range of the image to be encrypted. + + :param image_start: start address of the image + :param image_end: last address of the image + :return: True if yes, False otherwise + """ + return self.contains_addr(image_start) and self.contains_addr(image_end) + + def encrypt_image( + self, + base_address: int, + data: bytes, + byte_swap: bool, + counter_value: Optional[int] = None, + ) -> bytes: + """Encrypt specified data. + + :param base_address: of the data in target memory; must be >= self.start_addr + :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr + :param byte_swap: this probably depends on the flash device, how bytes are organized there + :param counter_value: Optional counter value, if not specified start address of keyblob will be used + :return: encrypted data + :raises SPSDKError: If start address is not valid + """ + if base_address % 16 != 0: + raise SPSDKError("Invalid start address") # Start address has to be 16 byte aligned + data = align_block(data, self._ENCRYPTION_BLOCK_SIZE) # align data length + data_len = len(data) + + # check start and end addresses + # Support dual image boot, do not raise exception + if not self.matches_range(base_address, base_address + data_len - 1): + logger.warning( + f"Image address range is not within key blob: " + f"{hex(self.start_addr)}-{hex(self.end_addr)}." + " Ignore this if flash remap feature is used" + ) + result = bytes() + + if not counter_value: + counter_value = self.start_addr + + counter = Counter( + self._get_ctr_nonce(), ctr_value=counter_value, ctr_byteorder_encoding=Endianness.BIG + ) + + for index in range(0, data_len, 16): + # prepare data in byte order + if byte_swap: + # swap 8 bytes + swap 8 bytes + data_2_encr = ( + data[-data_len + index + 7 : -data_len + index - 1 : -1] + + data[-data_len + index + 15 : -data_len + index + 7 : -1] + ) + else: + data_2_encr = data[index : index + 16] + # encrypt + encr_data = aes_ctr_encrypt(self.key, data_2_encr, counter.value) + # fix byte order in result + if byte_swap: + result += encr_data[-9:-17:-1] + encr_data[-1:-9:-1] # swap 8 bytes + swap 8 bytes + else: + result += encr_data + # update counter for encryption + counter.increment(16) + + if len(result) != data_len: + raise SPSDKError("Invalid length of encrypted data") + return bytes(result) + + @property + def is_encrypted(self) -> bool: + """Get the required encryption or not. + + :return: True if blob is encrypted, False otherwise. + """ + return (bool)( + (self.key_flags & (self.KEY_FLAG_ADE | self.KEY_FLAG_VLD)) + == (self.KEY_FLAG_ADE | self.KEY_FLAG_VLD) + ) + + +class Otfad: + """OTFAD: On-the-Fly AES Decryption Module.""" + + OTFAD_DATA_UNIT = 0x400 + + def __init__(self) -> None: + """Constructor.""" + self._key_blobs: List[KeyBlob] = [] + + def __getitem__(self, index: int) -> KeyBlob: + return self._key_blobs[index] + + def __setitem__(self, index: int, value: KeyBlob) -> None: + self._key_blobs.remove(self._key_blobs[index]) + self._key_blobs.insert(index, value) + + def __len__(self) -> int: + """Count of keyblobs.""" + return len(self._key_blobs) + + def add_key_blob(self, key_blob: KeyBlob) -> None: + """Add key for specified address range. + + :param key_blob: to be added + """ + self._key_blobs.append(key_blob) + + def encrypt_image(self, image: bytes, base_addr: int, byte_swap: bool) -> bytes: + """Encrypt image with all available keyblobs. + + :param image: plain image to be encrypted + :param base_addr: where the image will be located in target processor + :param byte_swap: this probably depends on the flash device, how bytes are organized there + :return: encrypted image + """ + encrypted_data = bytearray(image) + addr = base_addr + for block in split_data(image, self.OTFAD_DATA_UNIT): + for key_blob in self._key_blobs: + if key_blob.matches_range(addr, addr + len(block)): + logger.debug( + f"Encrypting {hex(addr)}:{hex(len(block) + addr)}" + f" with keyblob: \n {str(key_blob)}" + ) + encrypted_data[ + addr - base_addr : len(block) + addr - base_addr + ] = key_blob.encrypt_image(addr, block, byte_swap, counter_value=addr) + addr += len(block) + + return bytes(encrypted_data) + + def get_key_blobs(self) -> bytes: + """Get key blobs. + + :return: Binary key blobs joined together + """ + result = bytes() + for key_blob in self._key_blobs: + result += key_blob.plain_data() + return align_block( + result, 256 + ) # this is for compatibility with elftosb, probably need FLASH sector size + + def encrypt_key_blobs( + self, + kek: Union[bytes, str], + key_scramble_mask: Optional[int] = None, + key_scramble_align: Optional[int] = None, + byte_swap_cnt: int = 0, + ) -> bytes: + """Encrypt key blobs with specified key. + + :param kek: key to encode key blobs + :param key_scramble_mask: 32-bit scramble key, if KEK scrambling is desired. + :param key_scramble_align: 8-bit scramble align, if KEK scrambling is desired. + :param byte_swap_cnt: Encrypted keyblob reverse byte count, 0 means NO reversing is enabled + :raises SPSDKValueError: Invalid input value. + :return: encrypted binary key blobs joined together + """ + if isinstance(kek, str): + kek = bytes.fromhex(kek) + scramble_enabled = key_scramble_mask is not None and key_scramble_align is not None + if scramble_enabled: + assert key_scramble_mask and key_scramble_align + if key_scramble_mask >= 1 << 32: + raise SPSDKValueError("OTFAD Key scramble mask has invalid length") + if key_scramble_align >= 1 << 8: + raise SPSDKValueError("OTFAD Key scramble align has invalid length") + + logger.debug("The scrambling of keys is enabled.") + key_scramble_mask_inv = reverse_bits_in_bytes( + key_scramble_mask.to_bytes(4, byteorder=Endianness.BIG.value) + ) + logger.debug(f"The inverted scramble key is: {key_scramble_mask_inv.hex()}") + result = bytes() + scrambled = bytes() + for i, key_blob in enumerate(self._key_blobs): + if scramble_enabled: + assert key_scramble_mask and key_scramble_align + scrambled = bytearray(kek) + long_ix = (key_scramble_align >> (i * 2)) & 0x03 + for j in range(4): + scrambled[(long_ix * 4) + j] ^= key_scramble_mask_inv[j] + + logger.debug( + f"Used KEK for keyblob{i} encryption is: {scrambled.hex() if scramble_enabled else kek.hex()}" + ) + + result += key_blob.export( + scrambled if scramble_enabled else kek, byte_swap_cnt=byte_swap_cnt + ) + return align_block( + result, 256 + ) # this is for compatibility with elftosb, probably need FLASH sector size + + def __str__(self) -> str: + """Text info about the instance.""" + msg = "Key-Blob\n" + for index, key_blob in enumerate(self._key_blobs): + msg += f"Key-Blob {str(index)}:\n" + msg += str(key_blob) + return msg + + +class OtfadNxp(Otfad): + """OTFAD: On-the-Fly AES Decryption Module with reflecting of NXP parts.""" + + def __init__( + self, + family: str, + kek: Union[bytes, str], + table_address: int = 0, + key_blobs: Optional[List[KeyBlob]] = None, + key_scramble_mask: Optional[int] = None, + key_scramble_align: Optional[int] = None, + binaries: Optional[BinaryImage] = None, + ) -> None: + """Constructor. + + :param family: Device family + :param kek: KEK to encrypt OTFAD table + :param table_address: Absolute address of OTFAD table. + :param key_blobs: Optional Key blobs to add to OTFAD, defaults to None + :param key_scramble_mask: If defined, the key scrambling algorithm will be applied. + ('key_scramble_align' must be defined also) + :param key_scramble_align: If defined, the key scrambling algorithm will be applied. + ('key_scramble_mask' must be defined also) + :raises SPSDKValueError: Unsupported family + """ + super().__init__() + + if family not in self.get_supported_families(): + raise SPSDKValueError(f"Unsupported family{family} by OTFAD") + + if (key_scramble_align is None and key_scramble_mask) or ( + key_scramble_align and key_scramble_mask is None + ): + raise SPSDKValueError("Key Scrambling is not fully defined") + + self.family = family + self.kek = bytes.fromhex(kek) if isinstance(kek, str) else kek + self.key_scramble_mask = key_scramble_mask + self.key_scramble_align = key_scramble_align + self.table_address = table_address + self.db = get_db(family, "latest") + self.blobs_min_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_min_cnt") + self.blobs_max_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_max_cnt") + self.byte_swap = self.db.get_bool(DatabaseManager.OTFAD, "byte_swap") + self.key_blob_rec_size = self.db.get_int(DatabaseManager.OTFAD, "key_blob_rec_size") + self.keyblob_byte_swap_cnt = self.db.get_int(DatabaseManager.OTFAD, "keyblob_byte_swap_cnt") + assert self.keyblob_byte_swap_cnt in [0, 2, 4, 8, 16] + self.binaries = binaries + + if key_blobs: + for key_blob in key_blobs: + self.add_key_blob(key_blob) + + # Just fill up the minimum count of key blobs + while len(self._key_blobs) < self.blobs_min_cnt: + self.add_key_blob( + KeyBlob( + start_addr=0, + end_addr=0, + key=bytes([0] * KeyBlob.KEY_SIZE), + counter_iv=bytes([0] * KeyBlob.CTR_SIZE), + key_flags=0, + zero_fill=bytes([0] * 4), + ) + ) + + @staticmethod + def get_blhost_script_otp_keys( + family: str, otp_master_key: bytes, otfad_key_seed: bytes + ) -> str: + """Create BLHOST script to load fuses needed to run OTFAD with OTP fuses. + + :param family: Device family. + :param otp_master_key: OTP Master Key. + :param otfad_key_seed: OTFAD Key Seed. + :return: BLHOST script that loads the keys into fuses. + """ + database = get_db(family, "latest") + xml_fuses = database.get_file_path(DatabaseManager.OTFAD, "reg_fuses", default=None) + if not xml_fuses: + logger.debug(f"The {family} has no OTFAD fuses definition") + return "" + + fuses = Registers(family, base_endianness=Endianness.LITTLE) + grouped_regs = database.get_list(DatabaseManager.OTFAD, "grouped_registers", default=None) + fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) + reg_omk = fuses.find_reg("OTP_MASTER_KEY") + reg_oks = fuses.find_reg("OTFAD_KEK_SEED") + reg_omk.set_value(otp_master_key) + reg_oks.set_value(otfad_key_seed) + ret = ( + "# BLHOST OTFAD keys fuse programming script\n" + f"# Generated by SPSDK {spsdk_version}\n" + f"# Chip: {family}\n\n" + ) + + ret += f"# OTP MASTER KEY(Big Endian): 0x{reg_omk.get_bytes_value(raw=False).hex()}\n\n" + for reg in reg_omk.sub_regs: + ret += f"# {reg.name} fuse.\n" + ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" + + ret += f"\n# OTFAD KEK SEED (Big Endian): 0x{reg_oks.get_bytes_value(raw=True).hex()}\n\n" + for reg in reg_oks.sub_regs: + ret += f"# {reg.name} fuse.\n" + ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" + + return ret + + @staticmethod + def _replace_idx_value(value: str, index: int) -> str: + """Replace index value if provided in the database. + + :param value: value to be replaced f-string containing index + :param index: Index of record to be replaced + :return: value with replaced index + """ + return value.replace("{index}", str(index)) + + def get_blhost_script_otp_kek(self, index: int = 1) -> str: + """Create BLHOST script to load fuses needed to run OTFAD with OTP fuses just for OTFAD key. + + :param index: Index of OTFAD peripheral [1, 2, ..., n]. + :return: BLHOST script that loads the keys into fuses. + """ + if not self.db.get_bool(DatabaseManager.OTFAD, "has_kek_fuses", default=False): + logger.debug(f"The {self.family} has no OTFAD KEK fuses") + return "" + + peripheral_list = self.db.get_list(DatabaseManager.OTFAD, "peripheral_list") + if str(index) not in peripheral_list: + logger.debug(f"The {self.family} has no OTFAD{index} peripheral") + return "" + + filter_out_list = [f"OTFAD{i}" for i in peripheral_list if str(index) != i] + xml_fuses = self.db.get_file_path(DatabaseManager.OTFAD, "reg_fuses", default=None) + if not xml_fuses: + logger.debug(f"The {self.family} has no OTFAD fuses definition") + return "" + + fuses = Registers(self.family, base_endianness=Endianness.LITTLE) + + grouped_regs = self.db.get_list(DatabaseManager.OTFAD, "grouped_registers", default=None) + + fuses.load_registers_from_xml(xml_fuses, filter_out_list, grouped_regs) + + scramble_enabled = ( + self.key_scramble_mask is not None and self.key_scramble_align is not None + ) + + otfad_key_fuse = self._replace_idx_value( + self.db.get_str(DatabaseManager.OTFAD, "otfad_key_fuse"), index + ) + otfad_cfg_fuse = self._replace_idx_value( + self.db.get_str(DatabaseManager.OTFAD, "otfad_cfg_fuse"), index + ) + + fuses.find_reg(otfad_key_fuse).set_value(self.kek) + otfad_cfg = fuses.find_reg(otfad_cfg_fuse) + + try: + otfad_cfg.find_bitfield( + self.db.get_str(DatabaseManager.OTFAD, "otfad_enable_bitfield") + ).set_value(1) + except SPSDKRegsErrorBitfieldNotFound: + logger.debug(f"Bitfield for OTFAD ENABLE not found for {self.family}") + + if scramble_enabled: + scramble_key = self._replace_idx_value( + self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_key"), index + ) + scramble_align = self._replace_idx_value( + self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_align_bitfield"), + index, + ) + scramble_align_standalone = self.db.get_bool( + DatabaseManager.OTFAD, "otfad_scramble_align_fuse_standalone" + ) + otfad_cfg.find_bitfield( + self._replace_idx_value( + self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_enable_bitfield"), + index, + ) + ).set_value(1) + if scramble_align_standalone: + fuses.find_reg(scramble_align).set_value(self.key_scramble_align) + else: + otfad_cfg.find_bitfield(scramble_align).set_value(self.key_scramble_align) + fuses.find_reg(scramble_key).set_value(self.key_scramble_mask) + + ret = ( + f"# BLHOST OTFAD{index} KEK fuses programming script\n" + f"# Generated by SPSDK {spsdk_version}\n" + f"# Chip: {self.family}, peripheral: OTFAD{index} !\n\n" + ) + + ret += f"# OTP KEK (Big Endian): {self.kek.hex()}\n\n" + for reg in fuses.find_reg(otfad_key_fuse).sub_regs: + ret += f"# {reg.name} fuse.\n" + ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" + + ret += f"\n\n# {otfad_cfg.name} fuse.\n" + for bitfield in otfad_cfg.get_bitfields(): + ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" + ret += f"efuse-program-once {otfad_cfg.offset} 0x{otfad_cfg.get_bytes_value(raw=True).hex()} --no-verify\n" + + if scramble_enabled: + scramble = fuses.find_reg(scramble_key) + ret += f"\n# {scramble.name} fuse.\n" + ret += f"efuse-program-once {scramble.offset} 0x{scramble.get_bytes_value(raw=True).hex()} --no-verify\n" + if scramble_align_standalone: + scramble_align_reg = fuses.find_reg(scramble_align) + ret += f"\n# {scramble_align_reg.name} fuse.\n" + ret += ( + f"efuse-program-once {scramble_align_reg.offset}" + f" 0x{scramble_align_reg.get_bytes_value(raw=True).hex()} --no-verify\n" + ) + + return ret + + def export_image( + self, + plain_data: bool = False, + swap_bytes: bool = False, + join_sub_images: bool = True, + table_address: int = 0, + ) -> Optional[BinaryImage]: + """Get the OTFAD Key Blob Binary Image representation. + + :param plain_data: Binary representation in plain data format, defaults to False + :param swap_bytes: For some platforms the swap bytes is needed in encrypted format, defaults to False. + :param join_sub_images: If it's True, all the binary sub-images are joined into one, defaults to True. + :param table_address: Absolute address of OTFAD table. + :return: OTFAD key blob data in BinaryImage. + """ + if self.binaries is None: + return None + binaries: BinaryImage = deepcopy(self.binaries) + for binary in binaries.sub_images: + if binary.binary: + binary.binary = align_block(binary.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE) + for segment in binary.sub_images: + if segment.binary: + segment.binary = align_block(segment.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE) + + binaries.validate() + + if not plain_data: + for binary in binaries.sub_images: + if binary.binary: + binary.binary = self.encrypt_image( + binary.binary, + table_address + binary.absolute_address, + swap_bytes, + ) + for segment in binary.sub_images: + if segment.binary: + segment.binary = self.encrypt_image( + segment.binary, + segment.absolute_address + table_address, + swap_bytes, + ) + + if join_sub_images: + binaries.join_images() + binaries.validate() + + return binaries + + def binary_image( + self, + plain_data: bool = False, + data_alignment: int = 16, + otfad_table_name: str = "OTFAD_Table", + ) -> BinaryImage: + """Get the OTFAD Binary Image representation. + + :param plain_data: Binary representation in plain format, defaults to False + :param data_alignment: Alignment of data part key blobs. + :param otfad_table_name: name of the output file that contains OTFAD table + :return: OTFAD in BinaryImage. + """ + otfad = BinaryImage("OTFAD", offset=self.table_address) + # Add mandatory OTFAD table + otfad_table = ( + self.get_key_blobs() + if plain_data + else self.encrypt_key_blobs( + self.kek, + self.key_scramble_mask, + self.key_scramble_align, + self.keyblob_byte_swap_cnt, + ) + ) + otfad.add_image( + BinaryImage( + otfad_table_name, + size=self.key_blob_rec_size * self.blobs_max_cnt, + offset=0, + description=f"OTFAD description table for {self.family}", + binary=otfad_table, + alignment=256, + ) + ) + binaries = self.export_image(table_address=self.table_address) + + if binaries: + binaries.alignment = data_alignment + binaries.validate() + otfad.add_image(binaries) + return otfad + + @staticmethod + def get_supported_families() -> List[str]: + """Get all supported families for AHAB container. + + :return: List of supported families. + """ + return get_families(DatabaseManager.OTFAD) + + @staticmethod + def get_validation_schemas(family: str) -> List[Dict[str, Any]]: + """Get list of validation schemas. + + :param family: Family for which the template should be generated. + :return: Validation list of schemas. + """ + if family not in OtfadNxp.get_supported_families(): + return [] + + database = get_db(family, "latest") + schemas = get_schema_file(DatabaseManager.OTFAD) + family_sch = schemas["otfad_family"] + family_sch["properties"]["family"]["enum"] = OtfadNxp.get_supported_families() + family_sch["properties"]["family"]["template_value"] = family + ret = [family_sch, schemas["otfad_output"], schemas["otfad"]] + additional_schemes = database.get_list( + DatabaseManager.OTFAD, "additional_template", default=[] + ) + ret.extend([schemas[x] for x in additional_schemes]) + return ret + + @staticmethod + def get_validation_schemas_family() -> List[Dict[str, Any]]: + """Get list of validation schemas for family key. + + :return: Validation list of schemas. + """ + schemas = get_schema_file(DatabaseManager.OTFAD) + family_sch = schemas["otfad_family"] + family_sch["properties"]["family"]["enum"] = OtfadNxp.get_supported_families() + return [family_sch] + + @staticmethod + def generate_config_template(family: str) -> Dict[str, Any]: + """Generate OTFAD configuration template. + + :param family: Family for which the template should be generated. + :return: Dictionary of individual templates (key is name of template, value is template itself). + """ + val_schemas = OtfadNxp.get_validation_schemas(family) + database = get_db(family, "latest") + + if val_schemas: + template_note = database.get_str( + DatabaseManager.OTFAD, "additional_template_text", default="" + ) + title = f"On-The-Fly AES decryption Configuration template for {family}." + + yaml_data = CommentedConfig(title, val_schemas, note=template_note).get_template() + + return {f"{family}_otfad": yaml_data} + + return {} + + @staticmethod + def load_from_config( + config: Dict[str, Any], config_dir: str, search_paths: Optional[List[str]] = None + ) -> "OtfadNxp": + """Converts the configuration option into an OTFAD image object. + + "config" content array of containers configurations. + + :param config: array of OTFAD configuration dictionaries. + :param config_dir: directory where the config is located + :param search_paths: List of paths where to search for the file, defaults to None + :return: initialized OTFAD object. + """ + otfad_config: List[Dict[str, Any]] = config["key_blobs"] + family = config["family"] + database = get_db(family, "latest") + kek = load_hex_string(config["kek"], expected_size=16, search_paths=search_paths) + logger.debug(f"Loaded KEK: {kek.hex()}") + table_address = value_to_int(config["otfad_table_address"]) + start_address = min([value_to_int(addr["start_address"]) for addr in otfad_config]) + + key_scramble_mask = None + key_scramble_align = None + if database.get_bool(DatabaseManager.OTFAD, "supports_key_scrambling", default=False): + if "key_scramble" in config.keys(): + key_scramble = config["key_scramble"] + key_scramble_mask = value_to_int(key_scramble["key_scramble_mask"]) + key_scramble_align = value_to_int(key_scramble["key_scramble_align"]) + + data_blobs: Optional[List[Dict]] = config.get("data_blobs") + binaries = None + if data_blobs: + # pylint: disable-next=nested-min-max + start_address = min( + min([value_to_int(addr["address"]) for addr in data_blobs]), + start_address, + ) + binaries = BinaryImage( + filepath_from_config( + config, "encrypted_name", "encrypted_blobs", config_dir, config["output_folder"] + ), + offset=start_address - table_address, + ) + for data_blob in data_blobs: + data = load_binary(data_blob["data"], search_paths=search_paths) + address = value_to_int(data_blob["address"]) + + binary = BinaryImage( + os.path.basename(data_blob["data"]), + offset=address - table_address - binaries.offset, + binary=data, + ) + binaries.add_image(binary) + else: + logger.warning("The OTFAD configuration has NOT any data blobs records!") + + otfad = OtfadNxp( + family=family, + kek=kek, + table_address=table_address, + key_scramble_align=key_scramble_align, + key_scramble_mask=key_scramble_mask, + binaries=binaries, + ) + + for i, key_blob_cfg in enumerate(otfad_config): + aes_key = value_to_bytes(key_blob_cfg["aes_key"], byte_cnt=KeyBlob.KEY_SIZE) + aes_ctr = value_to_bytes(key_blob_cfg["aes_ctr"], byte_cnt=KeyBlob.CTR_SIZE) + start_addr = value_to_int(key_blob_cfg["start_address"]) + end_addr = value_to_int(key_blob_cfg["end_address"]) + aes_decryption_enable = key_blob_cfg.get("aes_decryption_enable", True) + valid = key_blob_cfg.get("valid", True) + read_only = key_blob_cfg.get("read_only", True) + flags = 0 + if aes_decryption_enable: + flags |= KeyBlob.KEY_FLAG_ADE + if valid: + flags |= KeyBlob.KEY_FLAG_VLD + if read_only: + flags |= KeyBlob.KEY_FLAG_READ_ONLY + + otfad[i] = KeyBlob( + start_addr=start_addr, + end_addr=end_addr, + key=aes_key, + counter_iv=aes_ctr, + key_flags=flags, + zero_fill=bytes([0] * 4), + ) + + return otfad diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py new file mode 100644 index 00000000..5afd446a --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""The module provides support for Root Key Hash table.""" + +import logging +import math +from abc import abstractmethod +from typing import List, Optional, Sequence, Union + +from typing_extensions import Self + +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.hash import EnumHashAlgorithm, get_hash, get_hash_length +from spsdk.crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from spsdk.crypto.utils import extract_public_key, extract_public_key_from_data +from spsdk.exceptions import SPSDKError +from spsdk.utils.misc import Endianness + +logger = logging.getLogger(__name__) + + +class RKHT: + """Root Key Hash Table class.""" + + def __init__(self, rkh_list: List[bytes]) -> None: + """Initialization of Root Key Hash Table class. + + :param rkh_list: List of Root Key Hashes + """ + if len(rkh_list) > 4: + raise SPSDKError("Number of Root Key Hashes can not be larger than 4.") + self.rkh_list = rkh_list + + @classmethod + def from_keys( + cls, + keys: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> Self: + """Create RKHT from list of keys. + + :param keys: List of public keys/certificates/private keys/bytes + :param password: Optional password to open secured private keys, defaults to None + :param search_paths: List of paths where to search for the file, defaults to None + """ + public_keys = ( + [cls.convert_key(x, password, search_paths=search_paths) for x in keys] if keys else [] + ) + if not all(isinstance(x, type(public_keys[0])) for x in public_keys): + raise SPSDKError("RKHT must contains all keys of a same instances.") + if not all( + cls._get_hash_algorithm(x) == cls._get_hash_algorithm(public_keys[0]) + for x in public_keys + ): + raise SPSDKError("RKHT must have same hash algorithm for all keys.") + + rotk_hashes = [cls._calc_key_hash(key) for key in public_keys] + return cls(rotk_hashes) + + @abstractmethod + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of hashes of public keys. + """ + + @staticmethod + def _get_hash_algorithm(key: PublicKey) -> EnumHashAlgorithm: + """Get hash algorithm output size for the key. + + :param key: Key to get hash. + :raises SPSDKError: Invalid kye type. + :return: Size in bits of hash. + """ + if isinstance(key, PublicKeyEcc): + return EnumHashAlgorithm.from_label(f"sha{key.key_size}") + + if isinstance(key, PublicKeyRsa): + # In case of RSA keys, hash is always SHA-256, regardless of the key length + return EnumHashAlgorithm.SHA256 + + raise SPSDKError("Unsupported key type to load.") + + @property + def hash_algorithm(self) -> EnumHashAlgorithm: + """Used hash algorithm name.""" + if not len(self.rkh_list) > 0: + raise SPSDKError("Unknown hash algorighm name. No root key hashes.") + return EnumHashAlgorithm.from_label(f"sha{self.hash_algorithm_size}") + + @property + def hash_algorithm_size(self) -> int: + """Used hash algorithm size in bites.""" + if not len(self.rkh_list) > 0: + raise SPSDKError("Unknown hash algorithm size. No public keys provided.") + return len(self.rkh_list[0]) * 8 + + @staticmethod + def _calc_key_hash( + public_key: PublicKey, + algorithm: Optional[EnumHashAlgorithm] = None, + ) -> bytes: + """Calculate a hash out of public key's exponent and modulus in RSA case, X/Y in EC. + + :param public_key: List of public keys to compute hash from. + :param sha_width: Used hash algorithm. + :raises SPSDKError: Unsupported public key type + :return: Computed hash. + """ + n_1 = 0 + n_2 = 0 + if isinstance(public_key, PublicKeyRsa): + n_1 = public_key.e + n1_len = math.ceil(n_1.bit_length() / 8) + n_2 = public_key.n + n2_len = math.ceil(n_2.bit_length() / 8) + elif isinstance(public_key, PublicKeyEcc): + n_1 = public_key.y + n_2 = public_key.x + n1_len = n2_len = public_key.coordinate_size + else: + raise SPSDKError(f"Unsupported key type: {type(public_key)}") + + n1_bytes = n_1.to_bytes(n1_len, Endianness.BIG.value) + n2_bytes = n_2.to_bytes(n2_len, Endianness.BIG.value) + + algorithm = algorithm or RKHT._get_hash_algorithm(public_key) + return get_hash(n2_bytes + n1_bytes, algorithm=algorithm) + + @staticmethod + def convert_key( + key: Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> PublicKey: + """Convert practically whole input that could hold Public key into public key. + + :param key: Public key in Certificate/Private key, Public key as a path to file, + loaded bytes or supported class. + :param password: Optional password to open secured private keys, defaults to None. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid kye type. + :return: Public Key object. + """ + if isinstance(key, PublicKey): + return key + + if isinstance(key, PrivateKey): + return key.get_public_key() + + if isinstance(key, Certificate): + return key.get_public_key() + + if isinstance(key, str): + return extract_public_key(key, password, search_paths=search_paths) + + if isinstance(key, (bytes, bytearray)): + return extract_public_key_from_data(key, password) + + raise SPSDKError("RKHT: Unsupported key to load.") + + +class RKHTv1(RKHT): + """Root Key Hash Table class for cert block v1.""" + + RKHT_SIZE = 4 + RKH_SIZE = 32 + + def __init__( + self, + rkh_list: List[bytes], + ) -> None: + """Initialization of Root Key Hash Table class. + + :param rkh_list: List of Root Key Hashes + """ + for key_hash in rkh_list: + if len(key_hash) != self.RKH_SIZE: + raise SPSDKError(f"Invalid key hash size: {len(key_hash)}") + super().__init__(rkh_list) + + @property + def hash_algorithm(self) -> EnumHashAlgorithm: + """Used Hash algorithm name.""" + return EnumHashAlgorithm.SHA256 + + def export(self) -> bytes: + """Export RKHT as bytes.""" + rotk_table = b"" + for i in range(self.RKHT_SIZE): + if i < len(self.rkh_list) and self.rkh_list[i]: + rotk_table += self.rkh_list[i] + else: + rotk_table += bytes(self.RKH_SIZE) + if len(rotk_table) != self.RKH_SIZE * self.RKHT_SIZE: + raise SPSDKError("Invalid length of data.") + return rotk_table + + @classmethod + def parse(cls, rkht: bytes) -> Self: + """Parse Root Key Hash Table into RKHTv1 object. + + :param rkht: Valid RKHT table + """ + rotkh_len = len(rkht) // cls.RKHT_SIZE + offset = 0 + key_hashes = [] + for _ in range(cls.RKHT_SIZE): + key_hashes.append(rkht[offset : offset + rotkh_len]) + offset += rotkh_len + return cls(key_hashes) + + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of Hashes of public key. + """ + rotkh = get_hash(self.export(), self.hash_algorithm) + return rotkh + + def set_rkh(self, index: int, rkh: bytes) -> None: + """Set Root Key Hash with index. + + :param index: Index in the hash table + :param rkh: Root Key Hash to be set + """ + if index > 3: + raise SPSDKError("Key hash can not be larger than 3.") + if self.rkh_list and len(rkh) != len(self.rkh_list[0]): + raise SPSDKError("Root Key Hash must be the same size as other hashes.") + # fill the gap with zeros if the keys are not consecutive + for idx in range(index + 1): + if len(self.rkh_list) < idx + 1: + self.rkh_list.append(bytes(self.RKH_SIZE)) + assert len(self.rkh_list) <= 4 + self.rkh_list[index] = rkh + + +class RKHTv21(RKHT): + """Root Key Hash Table class for cert block v2.1.""" + + def export(self) -> bytes: + """Export RKHT as bytes.""" + hash_table = bytes() + if len(self.rkh_list) > 1: + hash_table = bytearray().join(self.rkh_list) + return hash_table + + @classmethod + def parse(cls, rkht: bytes, hash_algorithm: EnumHashAlgorithm) -> Self: + """Parse Root Key Hash Table into RKHTv21 object. + + :param rkht: Valid RKHT table + :param hash_algorithm: Hash algorithm to be used + """ + rkh_len = get_hash_length(hash_algorithm) + if len(rkht) % rkh_len != 0: + raise SPSDKError( + f"The length of Root Key Hash Table does not match the hash algorithm {hash_algorithm}" + ) + offset = 0 + rkh_list = [] + rkht_size = len(rkht) // rkh_len + for _ in range(rkht_size): + rkh_list.append(rkht[offset : offset + rkh_len]) + offset += rkh_len + return cls(rkh_list) + + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of Hashes of public key. + """ + if not self.rkh_list: + logger.debug("RKHT has no records.") + return bytes() + if len(self.rkh_list) == 1: + rotkh = self.rkh_list[0] + else: + rotkh = get_hash(self.export(), self.hash_algorithm) + return rotkh diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py new file mode 100644 index 00000000..ac912295 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""The module provides support for RoT hash calculation .""" + + +from abc import abstractmethod +from typing import List, Optional, Sequence, Type, Union + +from spsdk.crypto.certificate import Certificate +from spsdk.crypto.keys import PrivateKey, PublicKey +from spsdk.exceptions import SPSDKError +from spsdk.image.ahab.ahab_container import SRKRecord +from spsdk.image.ahab.ahab_container import SRKTable as AhabSrkTable +from spsdk.image.secret import SrkItem +from spsdk.image.secret import SrkTable as HabSrkTable +from spsdk.utils.crypto.rkht import RKHT, RKHTv1, RKHTv21 +from spsdk.utils.database import DatabaseManager, get_db, get_families +from spsdk.utils.misc import load_binary + + +class Rot: + """Root of Trust object providing an abstraction over the RoT hash calculation for multiple device families.""" + + def __init__( + self, + family: str, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Root of Trust initialization.""" + self.rot_obj = self.get_rot_class(family)( + keys_or_certs=keys_or_certs, password=password, search_paths=search_paths + ) + + def calculate_hash(self) -> bytes: + """Calculate RoT hash.""" + return self.rot_obj.calculate_hash() + + def export(self) -> bytes: + """Export RoT.""" + return self.rot_obj.export() + + @classmethod + def get_supported_families(cls) -> List[str]: + """Get all supported families.""" + return get_families(DatabaseManager.CERT_BLOCK) + + @classmethod + def get_rot_class(cls, family: str) -> Type["RotBase"]: + """Get RoT class.""" + db = get_db(family, "latest") + rot_type = db.get_str(DatabaseManager.CERT_BLOCK, "rot_type") + for subclass in RotBase.__subclasses__(): + if subclass.rot_type == rot_type: + return subclass + raise SPSDKError(f"A ROT type {rot_type} does not exist.") + + +class RotBase: + """Root of Trust base class.""" + + rot_type: Optional[str] = None + + def __init__( + self, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Rot initialization.""" + self.keys_or_certs = keys_or_certs + self.password = password + self.search_paths = search_paths + + @abstractmethod + def calculate_hash( + self, + ) -> bytes: + """Calculate ROT hash.""" + + @abstractmethod + def export(self) -> bytes: + """Calculate ROT table.""" + + +class RotCertBlockv1(RotBase): + """Root of Trust for certificate block v1 class.""" + + rot_type = "cert_block_1" + + def __init__( + self, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Rot cert block v1 initialization.""" + super().__init__(keys_or_certs, password, search_paths) + self.rkht = RKHTv1.from_keys(self.keys_or_certs, self.password, self.search_paths) + + def calculate_hash( + self, + ) -> bytes: + """Calculate RoT hash.""" + return self.rkht.rkth() + + def export(self) -> bytes: + """Export RoT.""" + return self.rkht.export() + + +class RotCertBlockv21(RotBase): + """Root of Trust for certificate block v21 class.""" + + rot_type = "cert_block_21" + + def __init__( + self, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """Rot cert block v21 initialization.""" + super().__init__(keys_or_certs, password, search_paths) + self.rkht = RKHTv21.from_keys(self.keys_or_certs, self.password, self.search_paths) + + def calculate_hash( + self, + ) -> bytes: + """Calculate ROT hash.""" + return self.rkht.rkth() + + def export(self) -> bytes: + """Export RoT.""" + return self.rkht.export() + + +class RotSrkTableAhab(RotBase): + """Root of Trust for AHAB SrkTable class.""" + + rot_type = "srk_table_ahab" + + def __init__( + self, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """AHAB SRK table initialization.""" + super().__init__(keys_or_certs, password, search_paths) + self.srk = AhabSrkTable( + [SRKRecord(RKHT.convert_key(key, password, search_paths)) for key in keys_or_certs] + ) + self.srk.update_fields() + + def calculate_hash(self) -> bytes: + """Calculate ROT hash.""" + return self.srk.compute_srk_hash() + + def export(self) -> bytes: + """Export RoT.""" + return self.srk.export() + + +class RotSrkTableHab(RotBase): + """Root of Trust for HAB SrkTable class.""" + + rot_type = "srk_table_hab" + + def __init__( + self, + keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> None: + """HAB SRK table initialization.""" + super().__init__(keys_or_certs, password, search_paths) + self.srk = HabSrkTable() + for certificate in keys_or_certs: + if isinstance(certificate, (str, bytes, bytearray)): + try: + certificate = self._load_certificate(certificate, search_paths) + except SPSDKError as exc: + raise SPSDKError( + "Unable to load certificate. Certificate must be provided for HAB RoT calculation." + ) from exc + if not isinstance(certificate, Certificate): + raise SPSDKError("Certificate must be provided for HAB RoT calculation.") + item = SrkItem.from_certificate(certificate) + self.srk.append(item) + + def calculate_hash(self) -> bytes: + """Calculate ROT hash.""" + return self.srk.export_fuses() + + def export(self) -> bytes: + """Export RoT.""" + return self.srk.export() + + @classmethod + def _load_certificate( + cls, + certificate: Union[str, bytes, bytearray], + search_paths: Optional[List[str]] = None, + ) -> Certificate: + """Load certificate if certificate provided, or extract public key if private/public key is provided.""" + if isinstance(certificate, str): + certificate = load_binary(certificate, search_paths) + try: + return Certificate.parse(certificate) + except SPSDKError as exc: + raise SPSDKError("Unable to load certificate.") from exc diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py new file mode 100644 index 00000000..3d5baa69 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py @@ -0,0 +1,833 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module to manage used databases in SPSDK.""" + +import atexit +import logging +import os +import pickle +import shutil +from copy import copy, deepcopy +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union + +import platformdirs +from typing_extensions import Self + +import spsdk +from spsdk import SPSDK_CACHE_DISABLED, SPSDK_DATA_FOLDER +from spsdk.crypto.hash import EnumHashAlgorithm, Hash, get_hash +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.misc import ( + deep_update, + find_first, + load_configuration, + value_to_bool, + value_to_int, +) + +logger = logging.getLogger(__name__) + + +class SPSDKErrorMissingDevice(SPSDKError): + """Missing device in database.""" + + +class Features: + """Features dataclass represents a single device revision.""" + + def __init__( + self, name: str, is_latest: bool, device: "Device", features: Dict[str, Dict[str, Any]] + ) -> None: + """Constructor of revision. + + :param name: Revision name + :param is_latest: Mark if this revision is latest. + :param device: Reference to its device + :param features: Features + """ + self.name = name + self.is_latest = is_latest + self.device = device + self.features = features + + def check_key(self, feature: str, key: Union[List[str], str]) -> bool: + """Check if the key exist in database. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :raises SPSDKValueError: Unsupported feature + :return: True if exist False otherwise + """ + if feature not in self.features: + raise SPSDKValueError(f"Unsupported feature: '{feature}'") + db_dict = self.features[feature] + + if isinstance(key, list): + while len(key) > 1: + act_key = key.pop(0) + if act_key not in db_dict or not isinstance(db_dict[act_key], dict): + return False + db_dict = db_dict[act_key] + key = key[0] + + assert isinstance(key, str) + return key in db_dict + + def get_value(self, feature: str, key: Union[List[str], str], default: Any = None) -> Any: + """Get value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :raises SPSDKValueError: Unsupported feature + :raises SPSDKValueError: Unavailable item in feature + :return: Value from the feature + """ + if feature not in self.features: + raise SPSDKValueError(f"Unsupported feature: '{feature}'") + db_dict = self.features[feature] + + if isinstance(key, list): + while len(key) > 1: + act_key = key.pop(0) + if act_key not in db_dict or not isinstance(db_dict[act_key], dict): + raise SPSDKValueError(f"Non-existing nested group: '{act_key}'") + db_dict = db_dict[act_key] + key = key[0] + + assert isinstance(key, str) + val = db_dict.get(key, default) + + if val is None: + raise SPSDKValueError(f"Unavailable item '{key}' in feature '{feature}'") + return val + + def get_bool( + self, feature: str, key: Union[List[str], str], default: Optional[bool] = None + ) -> bool: + """Get Boolean value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: Boolean value from the feature + """ + val = self.get_value(feature, key, default) + return value_to_bool(val) + + def get_int( + self, feature: str, key: Union[List[str], str], default: Optional[int] = None + ) -> int: + """Get Integer value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: Integer value from the feature + """ + val = self.get_value(feature, key, default) + return value_to_int(val) + + def get_str( + self, feature: str, key: Union[List[str], str], default: Optional[str] = None + ) -> str: + """Get String value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: String value from the feature + """ + val = self.get_value(feature, key, default) + assert isinstance(val, str) + return val + + def get_list( + self, feature: str, key: Union[List[str], str], default: Optional[List] = None + ) -> List[Any]: + """Get List value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: List value from the feature + """ + val = self.get_value(feature, key, default) + assert isinstance(val, list) + return val + + def get_dict( + self, feature: str, key: Union[List[str], str], default: Optional[Dict] = None + ) -> Dict: + """Get Dictionary value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: Dictionary value from the feature + """ + val = self.get_value(feature, key, default) + assert isinstance(val, dict) + return val + + def get_file_path( + self, feature: str, key: Union[List[str], str], default: Optional[str] = None + ) -> str: + """Get File path value. + + :param feature: Feature name + :param key: Item key or key path in list like ['grp1', 'grp2', 'key'] + :param default: Default value in case of missing key + :return: File path value from the feature + """ + file_name = self.get_str(feature, key, default) + return self.device.create_file_path(file_name) + + +class Revisions(List[Features]): + """List of device revisions.""" + + def revision_names(self, append_latest: bool = False) -> List[str]: + """Get list of revisions. + + :param append_latest: Add to list also "latest" string + :return: List of all supported device version. + """ + ret = [rev.name for rev in self] + if append_latest: + ret.append("latest") + return ret + + def get(self, name: Optional[str] = None) -> Features: + """Get the revision by its name. + + If name is not specified, or equal to 'latest', then the latest revision is returned. + + :param name: The revision name. + :return: The Revision object. + """ + if name is None or name == "latest": + return self.get_latest() + return self.get_by_name(name) + + def get_by_name(self, name: str) -> Features: + """Get the required revision. + + :param name: Required revision name + :raises SPSDKValueError: Incase of invalid device or revision value. + :return: The Revision object. + """ + revision = find_first(self, lambda rev: rev.name == name) + if not revision: + raise SPSDKValueError(f"Requested revision {name} is not supported.") + return revision + + def get_latest(self) -> Features: + """Get latest revision for device. + + :raises SPSDKValueError: Incase of there is no latest revision defined. + :return: The Features object. + """ + revision = find_first(self, lambda rev: rev.is_latest) + if not revision: + raise SPSDKValueError("No latest revision has been defined.") + return revision + + +class DeviceInfo: + """Device information dataclass.""" + + def __init__( + self, + purpose: str, + web: str, + memory_map: Dict[str, Dict[str, Union[int, bool]]], + isp: Dict[str, Any], + ) -> None: + """Constructor of device information class. + + :param purpose: String description of purpose of MCU (in fact the device group) + :param web: Web page with device info + :param memory_map: Basic memory map of device + :param isp: Information regarding ISP mode + """ + self.purpose = purpose + self.web = web + self.memory_map = memory_map + self.isp = isp + + @staticmethod + def load(config: Dict[str, Any], defaults: Dict[str, Any]) -> "DeviceInfo": + """Loads the device from folder. + + :param config: The name of device. + :param defaults: Device data defaults. + :return: The Device object. + """ + data = deepcopy(defaults) + deep_update(data, config) + return DeviceInfo( + purpose=data["purpose"], web=data["web"], memory_map=data["memory_map"], isp=data["isp"] + ) + + def update(self, config: Dict[str, Any]) -> None: + """Updates Device info by new configuration. + + :param config: The new Device Info configuration + """ + self.purpose = config.get("purpose", self.purpose) + self.web = config.get("web", self.web) + self.memory_map = config.get("memory_map", self.memory_map) + self.isp = config.get("isp", self.isp) + + +class Device: + """Device dataclass represents a single device.""" + + def __init__( + self, + name: str, + path: str, + latest_rev: str, + info: DeviceInfo, + device_alias: Optional["Device"] = None, + revisions: Revisions = Revisions(), + ) -> None: + """Constructor of SPSDK Device. + + :param name: Device name + :param path: Data path + :param latest_rev: latest revision name + :param device_alias: Device alias, defaults to None + :param revisions: Device revisions, defaults to Revisions() + """ + self.name = name + self.path = path + self.latest_rev = latest_rev + self.device_alias = device_alias + self.revisions = revisions + self.info = info + + @property + def features_list(self) -> List[str]: + """Get the list of device features.""" + return [str(k) for k in self.revisions.get().features.keys()] + + @staticmethod + def _load_alias( + name: str, path: str, dev_cfg: Dict[str, Any], other_devices: "Devices" + ) -> "Device": + """Loads the device from folder. + + :param name: The name of device. + :param path: Device data path. + :param dev_cfg: Already loaded configuration. + :param other_devices: Other devices used to allow aliases. + :return: The Device object. + """ + dev_cfg = load_configuration(os.path.join(path, "database.yaml")) + dev_alias_name = dev_cfg["alias"] + # Let get() function raise exception in case that device not exists in database + ret = deepcopy(other_devices.get(dev_alias_name)) + ret.name = name + ret.path = path + ret.device_alias = other_devices.get(dev_alias_name) + dev_features: Dict[str, Dict] = dev_cfg.get("features", {}) + dev_revisions: Dict[str, Dict] = dev_cfg.get("revisions", {}) + assert isinstance(dev_features, Dict) + assert isinstance(dev_revisions, Dict) + ret.latest_rev = dev_cfg.get("latest", ret.latest_rev) + # First off all update general changes in features + if dev_features: + for rev in ret.revisions: + deep_update(rev.features, dev_features) + + for rev_name, rev_updates in dev_revisions.items(): + try: + dev_rev = ret.revisions.get_by_name(rev_name) + except SPSDKValueError as exc: + # In case of newly defined revision, there must be defined alias + alias_rev = rev_updates.get("alias") + if not alias_rev: + raise SPSDKError( + f"There is missing alias key in new revision ({rev_name}) of aliased device {ret.name}" + ) from exc + dev_rev = deepcopy(ret.revisions.get_by_name(alias_rev)) + dev_rev.name = rev_name + dev_rev.is_latest = bool(ret.latest_rev == rev_name) + ret.revisions.append(dev_rev) + + # Update just same rev + rev_specific_features = rev_updates.get("features") + if rev_specific_features: + deep_update(dev_rev.features, rev_specific_features) + + if "info" in dev_cfg: + ret.info.update(dev_cfg["info"]) + + return ret + + @staticmethod + def load(name: str, path: str, defaults: Dict[str, Any], other_devices: "Devices") -> "Device": + """Loads the device from folder. + + :param name: The name of device. + :param path: Device data path. + :param defaults: Device data defaults. + :param other_devices: Other devices used to allow aliases. + :return: The Device object. + """ + dev_cfg = load_configuration(os.path.join(path, "database.yaml")) + dev_alias_name = dev_cfg.get("alias") + if dev_alias_name: + return Device._load_alias( + name=name, path=path, dev_cfg=dev_cfg, other_devices=other_devices + ) + + dev_features: Dict[str, Dict] = dev_cfg["features"] + features_defaults: Dict[str, Dict] = deepcopy(defaults["features"]) + + dev_info = DeviceInfo.load(dev_cfg["info"], defaults["info"]) + + # Get defaults and update them by device specific data set + for feature_name in dev_features: + deep_update(features_defaults[feature_name], dev_features[feature_name]) + dev_features[feature_name] = features_defaults[feature_name] + + revisions = Revisions() + dev_revisions: Dict[str, Dict] = dev_cfg["revisions"] + latest: str = dev_cfg["latest"] + if latest not in dev_revisions: + raise SPSDKError( + f"The latest revision defined in database for {name} is not in supported revisions" + ) + + ret = Device(name=name, path=path, info=dev_info, latest_rev=latest, device_alias=None) + + for rev, rev_updates in dev_revisions.items(): + features = deepcopy(dev_features) + rev_specific_features = rev_updates.get("features") + if rev_specific_features: + deep_update(features, rev_specific_features) + revisions.append( + Features(name=rev, is_latest=bool(rev == latest), features=features, device=ret) + ) + + ret.revisions = revisions + + return ret + + def create_file_path(self, file_name: str) -> str: + """Create File path value for this device. + + :param file_name: File name to be enriched by device path + :return: File path value for the device + """ + path = os.path.abspath(os.path.join(self.path, file_name)) + if not os.path.exists(path) and self.device_alias: + path = self.device_alias.create_file_path(file_name) + + if not os.path.exists(path): + raise SPSDKValueError(f"Non existing file ({file_name}) in database") + return path + + +class Devices(List[Device]): + """List of devices.""" + + def get(self, name: str) -> Device: + """Return database device structure. + + :param name: String Key with device name. + :raises SPSDKErrorMissingDevice: In case the device with given name does not exist + :return: Dictionary device configuration structure or None: + """ + dev = find_first(self, lambda dev: dev.name == name) + if not dev: + raise SPSDKErrorMissingDevice(f"The device with name {name} is not in the database.") + return dev + + @property + def devices_names(self) -> List[str]: + """Get the list of devices names.""" + return [dev.name for dev in self] + + def feature_items(self, feature: str, key: str) -> Iterator[Tuple[str, str, Any]]: + """Iter the whole database for the feature items. + + :return: Tuple of Device name, revision name and items value. + """ + for device in self: + if not feature in device.features_list: + continue + for rev in device.revisions: + value = rev.features[feature].get(key) + if value is None: + raise SPSDKValueError(f"Missing item '{key}' in feature '{feature}'!") + yield (device.name, rev.name, value) + + @staticmethod + def load(devices_path: str, defaults: Dict[str, Any]) -> "Devices": + """Loads the devices from SPSDK database path. + + :param devices_path: Devices data path. + :param defaults: Devices defaults data. + :return: The Devices object. + """ + devices = Devices() + uncompleted_aliases: List[os.DirEntry] = [] + for dev in os.scandir(devices_path): + if dev.is_dir(): + try: + try: + devices.append( + Device.load( + name=dev.name, + path=dev.path, + defaults=defaults, + other_devices=devices, + ) + ) + except SPSDKErrorMissingDevice: + uncompleted_aliases.append(dev) + except SPSDKError as exc: + logger.error( + f"Failed loading device '{dev.name}' into SPSDK database. Details:\n{str(exc)}" + ) + while uncompleted_aliases: + prev_len = len(uncompleted_aliases) + for dev in copy(uncompleted_aliases): + try: + devices.append( + Device.load( + name=dev.name, path=dev.path, defaults=defaults, other_devices=devices + ) + ) + uncompleted_aliases.remove(dev) + except SPSDKErrorMissingDevice: + pass + if prev_len == len(uncompleted_aliases): + raise SPSDKError("Cannot load all alias devices in database.") + return devices + + +class Database: + """Class that helps manage used databases in SPSDK.""" + + def __init__(self, path: str) -> None: + """Register Configuration class constructor. + + :param path: The path to configuration JSON file. + """ + self._cfg_cache: Dict[str, Dict[str, Any]] = {} + self.path = path + self.common_folder_path = os.path.join(path, "common") + self.devices_folder_path = os.path.join(path, "devices") + self._defaults = load_configuration( + os.path.join(self.common_folder_path, "database_defaults.yaml") + ) + self._devices = Devices.load(devices_path=self.devices_folder_path, defaults=self._defaults) + + # optional Database hash that could be used for identification of consistency + self.db_hash = bytes() + + @property + def devices(self) -> Devices: + """Get the list of devices stored in the database.""" + return self._devices + + def get_feature_list(self, dev_name: Optional[str] = None) -> List[str]: + """Get features list. + + If device is not used, the whole list of SPSDK features is returned + + :param dev_name: Device name, defaults to None + :returns: List of features. + """ + if dev_name: + return self.devices.get(dev_name).features_list + + default_features: Dict[str, Dict] = self._defaults["features"] + return [str(k) for k in default_features.keys()] + + def get_defaults(self, feature: str) -> Dict[str, Any]: + """Gets feature defaults. + + :param feature: Feature name + :return: Dictionary with feature defaults. + """ + features = self._defaults["features"] + if feature not in features: + raise SPSDKValueError(f"Invalid feature requested: {feature}") + + return deepcopy(features[feature]) + + def get_device_features( + self, + device: str, + revision: str = "latest", + ) -> Features: + """Get device features database. + + :param device: The device name. + :param revision: The revision of the silicon. + :raises SPSDKValueError: Unsupported feature + :return: The feature data. + """ + dev = self.devices.get(device) + return dev.revisions.get(revision) + + def get_schema_file(self, feature: str) -> Dict[str, Any]: + """Get JSON Schema file name for the requested feature. + + :param feature: Requested feature. + :return: Loaded dictionary of JSON Schema file. + """ + filename = os.path.join(SPSDK_DATA_FOLDER, "jsonschemas", f"sch_{feature}.yaml") + return self.load_db_cfg_file(filename) + + def load_db_cfg_file(self, filename: str) -> Dict[str, Any]: + """Return load database config file (JSON/YAML). Use SingleTon behavior. + + :param filename: Path to config file. + :raises SPSDKError: Invalid config file. + :return: Loaded file in dictionary. + """ + abs_path = os.path.abspath(filename) + if abs_path not in self._cfg_cache: + try: + cfg = load_configuration(abs_path) + except SPSDKError as exc: + raise SPSDKError(f"Invalid configuration file. {str(exc)}") from exc + self._cfg_cache[abs_path] = cfg + + return deepcopy(self._cfg_cache[abs_path]) + + def get_devices_with_feature( + self, feature: str, sub_keys: Optional[List[str]] = None + ) -> List[str]: + """Get the list of all device names that supports requested feature. + + :param feature: Name of feature + :param sub_keys: Optional sub keys to specify the nested dictionaries that feature needs to has to be counted + :returns: List of devices that supports requested feature. + """ + + def check_sub_keys(d: dict, sub_keys: List[str]) -> bool: + key = sub_keys.pop(0) + if not key in d: + return False + + if len(sub_keys) == 0: + return True + + nested = d[key] + if not isinstance(nested, dict): + return False + return check_sub_keys(nested, sub_keys) + + devices = [] + for device in self.devices: + if feature in device.features_list: + if sub_keys and not check_sub_keys( + device.revisions.get_latest().features[feature], copy(sub_keys) + ): + continue + devices.append(device.name) + + devices.sort() + return devices + + def __hash__(self) -> int: + """Hash function of the database.""" + return hash(len(self._cfg_cache)) + + +class DatabaseManager: + """Main SPSDK database manager.""" + + _instance = None + _db: Optional[Database] = None + _db_hash: int = 0 + _db_cache_file_name = "" + + @staticmethod + def get_cache_filename() -> Tuple[str, str]: + """Get database cache folder and file name. + + :return: Tuple of cache path and database file name. + """ + data_folder = SPSDK_DATA_FOLDER.lower() + cache_name = ( + "db_" + + get_hash(data_folder.encode(), algorithm=EnumHashAlgorithm.SHA1)[:6].hex() + + ".cache" + ) + cache_path = platformdirs.user_cache_dir(appname="spsdk", version=spsdk.version) + return (cache_path, os.path.join(cache_path, cache_name)) + + @staticmethod + def clear_cache() -> None: + """Clear SPSDK cache.""" + path, _ = DatabaseManager.get_cache_filename() + shutil.rmtree(path) + + @classmethod + def _get_database(cls) -> Database: + """Get database and count with cache.""" + if SPSDK_CACHE_DISABLED: + DatabaseManager.clear_cache() + return Database(SPSDK_DATA_FOLDER) + + db_hash = DatabaseManager.get_db_hash(SPSDK_DATA_FOLDER) + + if os.path.exists(cls._db_cache_file_name): + try: + with open(cls._db_cache_file_name, mode="rb") as f: + loaded_db = pickle.load(f) + assert isinstance(loaded_db, Database) + if db_hash == loaded_db.db_hash: + logger.debug(f"Loaded database from cache: {cls._db_cache_file_name}") + return loaded_db + # if the hash is not same clear cache and make a new one + logger.debug(f"Existing cached DB ({cls._db_cache_file_name}) has invalid hash") + DatabaseManager.clear_cache() + except Exception as exc: + logger.debug(f"Cannot load database cache: {str(exc)}") + + db = Database(SPSDK_DATA_FOLDER) + db.db_hash = db_hash + try: + os.makedirs(cls._db_cache_folder_name, exist_ok=True) + with open(cls._db_cache_file_name, mode="wb") as f: + pickle.dump(db, f, pickle.HIGHEST_PROTOCOL) + logger.debug(f"Created database cache: {cls._db_cache_file_name}") + except Exception as exc: + logger.debug(f"Cannot store database cache: {str(exc)}") + return db + + def __new__(cls) -> Self: + """Manage SPSDK Database as a singleton class. + + :return: SPSDK_Database object + """ + if cls._instance: + return cls._instance + cls._instance = super(DatabaseManager, cls).__new__(cls) + cls._db_cache_folder_name, cls._db_cache_file_name = DatabaseManager.get_cache_filename() + cls._db = cls._instance._get_database() + cls._db_hash = hash(cls._db) + return cls._instance + + @staticmethod + def get_db_hash(path: str) -> bytes: + """Get the real db hash.""" + hash_obj = Hash(EnumHashAlgorithm.SHA1) + for root, dirs, files in os.walk(path): + for _dir in dirs: + hash_obj.update(DatabaseManager.get_db_hash(os.path.join(root, _dir))) + for file in files: + if os.path.splitext(file)[1] in [".json", ".yaml"]: + stat = os.stat(os.path.join(root, file)) + hash_obj.update_int(stat.st_mtime_ns) + hash_obj.update_int(stat.st_ctime_ns) + hash_obj.update_int(stat.st_size) + + return hash_obj.finalize() + + @property + def db(self) -> Database: + """Get Database.""" + db = type(self)._db + assert isinstance(db, Database) + return db + + # """List all SPSDK supported features""" + COMM_BUFFER = "comm_buffer" + # BLHOST = "blhost" + CERT_BLOCK = "cert_block" + DAT = "dat" + MBI = "mbi" + HAB = "hab" + AHAB = "ahab" + SIGNED_MSG = "signed_msg" + PFR = "pfr" + IFR = "ifr" + BOOTABLE_IMAGE = "bootable_image" + FCB = "fcb" + XMCD = "xmcd" + BEE = "bee" + IEE = "iee" + OTFAD = "otfad" + SB21 = "sb21" + SB31 = "sb31" + SBX = "sbx" + SHADOW_REGS = "shadow_regs" + DEVHSM = "devhsm" + TP = "tp" + TZ = "tz" + ELE = "ele" + MEMCFG = "memcfg" + WPC = "wpc" + + +@atexit.register +def on_delete() -> None: + """Delete method of SPSDK database. + + The exit method is used to update cache in case it has been changed. + """ + if SPSDK_CACHE_DISABLED: + return + if DatabaseManager._db_hash != hash(DatabaseManager._db): + try: + with open(DatabaseManager._db_cache_file_name, mode="wb") as f: + logger.debug(f"Updating cache: {DatabaseManager._db_cache_file_name}") + pickle.dump(DatabaseManager().db, f, pickle.HIGHEST_PROTOCOL) + except FileNotFoundError: + pass + + +def get_db( + device: str, + revision: str = "latest", +) -> Features: + """Get device feature database. + + :param device: The device name. + :param revision: The revision of the silicon. + :return: The feature data. + """ + return DatabaseManager().db.get_device_features(device, revision) + + +def get_device(device: str) -> Device: + """Get device database object. + + :param device: The device name. + :return: The device data. + """ + return DatabaseManager().db.devices.get(device) + + +def get_families(feature: str, sub_keys: Optional[List[str]] = None) -> List[str]: + """Get the list of all family names that supports requested feature. + + :param feature: Name of feature + :param sub_keys: Optional sub keys to specify the nested dictionaries that feature needs to has to be counted + :returns: List of devices that supports requested feature. + """ + return DatabaseManager().db.get_devices_with_feature(feature, sub_keys) + + +def get_schema_file(feature: str) -> Dict[str, Any]: + """Get JSON Schema file name for the requested feature. + + :param feature: Requested feature. + :return: Loaded dictionary of JSON Schema file. + """ + return DatabaseManager().db.get_schema_file(feature) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/exceptions.py new file mode 100644 index 00000000..95ed2013 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/exceptions.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module provides exceptions for SPSDK utilities.""" +from ..exceptions import SPSDKError + + +class SPSDKRegsError(SPSDKError): + """General Error group for utilities SPSDK registers module.""" + + +class SPSDKRegsErrorRegisterGroupMishmash(SPSDKRegsError): + """Register Group inconsistency problem.""" + + +class SPSDKRegsErrorRegisterNotFound(SPSDKRegsError): + """Register has not been found.""" + + +class SPSDKRegsErrorBitfieldNotFound(SPSDKRegsError): + """Bitfield has not been found.""" + + +class SPSDKRegsErrorEnumNotFound(SPSDKRegsError): + """Enum has not been found.""" + + +class SPSDKTimeoutError(TimeoutError, SPSDKError): + """SPSDK Timeout.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py new file mode 100644 index 00000000..6b758819 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module to keep additional utilities for binary images.""" + +import logging +import math +import os +import re +import textwrap +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +import colorama + +from spsdk.exceptions import SPSDKError, SPSDKOverlapError, SPSDKValueError +from spsdk.utils.database import DatabaseManager +from spsdk.utils.misc import ( + BinaryPattern, + align, + align_block, + find_file, + format_value, + size_fmt, + write_file, +) +from spsdk.utils.schema_validator import CommentedConfig + +if TYPE_CHECKING: + # bincopy will be loaded lazily as needed, this is just to satisfy type-hint checkers + import bincopy + +logger = logging.getLogger(__name__) + + +class ColorPicker: + """Simple class to get each time when ask different color from list.""" + + COLORS = [ + colorama.Fore.LIGHTBLACK_EX, + colorama.Fore.BLUE, + colorama.Fore.GREEN, + colorama.Fore.CYAN, + colorama.Fore.YELLOW, + colorama.Fore.MAGENTA, + colorama.Fore.WHITE, + colorama.Fore.LIGHTBLUE_EX, + colorama.Fore.LIGHTCYAN_EX, + colorama.Fore.LIGHTGREEN_EX, + colorama.Fore.LIGHTMAGENTA_EX, + colorama.Fore.LIGHTWHITE_EX, + colorama.Fore.LIGHTYELLOW_EX, + ] + + def __init__(self) -> None: + """Constructor of ColorPicker.""" + self.index = len(self.COLORS) + + def get_color(self, unwanted_color: Optional[str] = None) -> str: + """Get new color from list. + + :param unwanted_color: Color that should be omitted. + :return: Color + """ + self.index += 1 + if self.index >= len(ColorPicker.COLORS): + self.index = 0 + if unwanted_color and ColorPicker.COLORS[self.index] == unwanted_color: + return self.get_color(unwanted_color) + return ColorPicker.COLORS[self.index] + + +class BinaryImage: + """Binary Image class.""" + + MINIMAL_DRAW_WIDTH = 30 + + def __init__( + self, + name: str, + size: int = 0, + offset: int = 0, + description: Optional[str] = None, + binary: Optional[bytes] = None, + pattern: Optional[BinaryPattern] = None, + alignment: int = 1, + parent: Optional["BinaryImage"] = None, + ) -> None: + """Binary Image class constructor. + + :param name: Name of Image. + :param size: Image size. + :param offset: Image offset in parent image, defaults to 0 + :param description: Text description of image, defaults to None + :param binary: Optional binary content. + :param pattern: Optional binary pattern. + :param alignment: Optional alignment of result image + :param parent: Handle to parent object, defaults to None + """ + self.name = name + self.description = description + self.offset = offset + self._size = align(size, alignment) + self.binary = binary + self.pattern = pattern + self.alignment = alignment + self.parent = parent + + if parent: + assert isinstance(parent, BinaryImage) + self.sub_images: List["BinaryImage"] = [] + + @property + def size(self) -> int: + """Size property.""" + return len(self) + + @size.setter + def size(self, value: int) -> None: + """Size property setter.""" + self._size = align(value, self.alignment) + + def add_image(self, image: "BinaryImage") -> None: + """Add new sub image information. + + :param image: Image object. + """ + image.parent = self + for i, child in enumerate(self.sub_images): + if image.offset < child.offset: + self.sub_images.insert(i, image) + return + self.sub_images.append(image) + + def join_images(self) -> None: + """Join all sub images into main binary block.""" + binary = self.export() + self.sub_images.clear() + self.binary = binary + + @property + def image_name(self) -> str: + """Image name including all parents. + + :return: Full Image name + """ + if self.parent: + return self.parent.image_name + "=>" + self.name + return self.name + + @property + def absolute_address(self) -> int: + """Image absolute address relative to base parent. + + :return: Absolute address relative to base parent + """ + if self.parent: + return self.parent.absolute_address + self.offset + return self.offset + + def aligned_start(self, alignment: int = 4) -> int: + """Returns aligned start address. + + :param alignment: The alignment value, defaults to 4. + :return: Floor alignment address. + """ + return math.floor(self.absolute_address / alignment) * alignment + + def aligned_length(self, alignment: int = 4) -> int: + """Returns aligned length for erasing purposes. + + :param alignment: The alignment value, defaults to 4. + :return: Ceil alignment length. + """ + end_address = self.absolute_address + len(self) + aligned_end = math.ceil(end_address / alignment) * alignment + aligned_len = aligned_end - self.aligned_start(alignment) + return aligned_len + + def __str__(self) -> str: + """Provides information about image. + + :return: String information about Image. + """ + size = len(self) + ret = "" + ret += f"Name: {self.image_name}\n" + ret += f"Starts: {hex(self.absolute_address)}\n" + ret += f"Ends: {hex(self.absolute_address+ size-1)}\n" + ret += f"Size: {self._get_size_line(size)}\n" + ret += f"Alignment: {size_fmt(self.alignment, use_kibibyte=False)}\n" + if self.pattern: + ret += f"Pattern:{self.pattern.pattern}\n" + if self.description: + ret += self.description + "\n" + return ret + + def validate(self) -> None: + """Validate if the images doesn't overlaps each other.""" + if self.offset < 0: + raise SPSDKValueError( + f"Image offset of {self.image_name} cannot be in negative numbers." + ) + if len(self) < 0: + raise SPSDKValueError(f"Image size of {self.image_name} cannot be in negative numbers.") + for image in self.sub_images: + image.validate() + begin = image.offset + end = begin + len(image) - 1 + # Check if it fits inside the parent image + if end >= len(self): + raise SPSDKOverlapError( + f"The image {image.name} doesn't fit into {self.name} parent image." + ) + # Check if it doesn't overlap any other sibling image + for sibling in self.sub_images: + if sibling != image: + sibling_begin = sibling.offset + sibling_end = sibling_begin + len(sibling) - 1 + if end < sibling_begin or begin > sibling_end: + continue + + raise SPSDKOverlapError( + f"The image overlap error:\n" + f"{str(image)}\n" + "overlaps the:\n" + f"{str(sibling)}\n" + ) + + def _get_size_line(self, size: int) -> str: + """Get string of size line. + + :param size: Size in bytes + :return: Formatted size line. + """ + if size >= 1024: + real_size = ",".join(re.findall(".{1,3}", (str(len(self)))[::-1]))[::-1] + return f"Size: {size_fmt(len(self), False)}; {real_size} B" + + return f"Size: {size_fmt(len(self), False)}" + + def get_min_draw_width(self, include_sub_images: bool = True) -> int: + """Get minimal width of table for draw function. + + :param include_sub_images: Include also sub images into, defaults to True + :return: Minimal width in characters. + """ + widths = [ + self.MINIMAL_DRAW_WIDTH, + len(f"+==-0x0000_0000= {self.name} =+"), + len(f"|{self._get_size_line(self.size)}|"), + ] + if include_sub_images: + for child in self.sub_images: + widths.append(child.get_min_draw_width() + 2) # +2 means add vertical borders + return max(widths) + + def draw( + self, + include_sub_images: bool = True, + width: int = 0, + color: str = "", + no_color: bool = False, + ) -> str: + # fmt: off + """Draw the image into the ASCII graphics. + + :param include_sub_images: Include also sub images into, defaults to True + :param width: Fixed width of table, 0 means autosize. + :param color: Color of this block, None means automatic color. + :param no_color: Disable adding colors into output. + :raises SPSDKValueError: In case of invalid width. + :return: ASCII art representation of image. + """ + # +==0x0000_0000==Title1===============+ + # | Size: 2048B | + # | Description1 | + # | Description1 2nd line | + # |+==0x0000_0000==Title11============+| + # || Size: 512B || + # || Description11 || + # || Description11 2nd line || + # |+==0x0000_01FF=====================+| + # | | + # |+==0x0000_0210==Title12============+| + # || Size: 512B || + # || Description12 || + # || Description12 2nd line || + # |+==0x0000_041F=====================+| + # +==0x0000_07FF=======================+ + # fmt: on + def _get_centered_line(text: str) -> str: + text_len = len(text) + spaces = width - text_len - 2 + assert spaces >= 0, "Binary Image Draw: Center line is longer than width" + padding_l = int(spaces / 2) + padding_r = int(spaces - padding_l) + return color + f"|{' '*padding_l}{text}{' '*padding_r}|\n" + + def wrap_block(inner: str) -> str: + wrapped_block = "" + lines = inner.splitlines(keepends=False) + for line in lines: + wrapped_block += color + "|" + line + color + "|\n" + return wrapped_block + + if no_color: + color = "" + else: + color_picker = ColorPicker() + try: + self.validate() + color = color or color_picker.get_color() + except SPSDKError: + color = colorama.Fore.RED + + block = "" if self.parent else "\n" + min_width = self.get_min_draw_width(include_sub_images) + if not width and self.parent is None: + width = min_width + + if width < min_width: + raise SPSDKValueError( + f"Binary Image Draw: Width is to short ({width} < minimal width: {min_width})" + ) + + # - Title line + header = f"+=={format_value(self.absolute_address, 32)}= {self.name} =" + block += color + f"{header}{'='*(width-len(header)-1)}+\n" + # - Size + block += _get_centered_line(self._get_size_line(len(self))) + # - Description + if self.description: + for line in textwrap.wrap(self.description, width=width - 2, fix_sentence_endings=True): + block += _get_centered_line(line) + # - Pattern + if self.pattern: + block += _get_centered_line(f"Pattern: {self.pattern.pattern}") + # - Inner blocks + if include_sub_images: + next_free_space = 0 + for child in self.sub_images: + # If the images doesn't comes one by one place empty line + if child.offset != next_free_space: + block += _get_centered_line( + f"Gap: {size_fmt(child.offset-next_free_space, False)}" + ) + next_free_space = child.offset + len(child) + inner_block = child.draw( + include_sub_images=include_sub_images, + width=width - 2, + color="" if no_color else color_picker.get_color(color), + no_color=no_color, + ) + block += wrap_block(inner_block) + + # - Closing line + footer = f"+=={format_value(self.absolute_address + len(self) - 1, 32)}==" + block += color + f"{footer}{'='*(width-len(footer)-1)}+\n" + + if self.parent is None: + block += "\n" + "" if no_color else colorama.Fore.RESET + return block + + def update_offsets(self) -> None: + """Update offsets from the sub images into main offset value begin offsets.""" + offsets = [] + for image in self.sub_images: + offsets.append(image.offset) + + min_offset = min(offsets) + for image in self.sub_images: + image.offset -= min_offset + self.offset += min_offset + + def __len__(self) -> int: + """Get length of image. + + If internal member size is not set(is zero) the size is computed from sub images. + :return: Size of image. + """ + if self._size: + return self._size + max_size = len(self.binary) if self.binary else 0 + for image in self.sub_images: + size = image.offset + len(image) + max_size = max(size, max_size) + return align(max_size, self.alignment) + + def export(self) -> bytes: + """Export represented binary image. + + :return: Byte array of binary image. + """ + if self.binary and len(self) == len(self.binary) and len(self.sub_images) == 0: + return self.binary + + if self.pattern: + ret = bytearray(self.pattern.get_block(len(self))) + else: + ret = bytearray(len(self)) + + if self.binary: + binary_view = memoryview(self.binary) + ret[: len(self.binary)] = binary_view + + for image in self.sub_images: + image_data = image.export() + ret_slice = memoryview(ret)[image.offset : image.offset + len(image_data)] + image_data_view = memoryview(image_data) + ret_slice[:] = image_data_view + + return align_block(ret, self.alignment, self.pattern) + + @staticmethod + def get_validation_schemas() -> List[Dict[str, Any]]: + """Get validation schemas list to check a supported configuration. + + :return: Validation schemas. + """ + return [DatabaseManager().db.get_schema_file("binary")] + + @staticmethod + def load_from_config( + config: Dict[str, Any], search_paths: Optional[List[str]] = None + ) -> "BinaryImage": + """Converts the configuration option into an Binary Image object. + + :param config: Description of binary image. + :param search_paths: List of paths where to search for the file, defaults to None + :return: Initialized Binary Image. + """ + name = config.get("name", "Base Image") + size = config.get("size", 0) + pattern = BinaryPattern(config.get("pattern", "zeros")) + alignment = config.get("alignment", 1) + ret = BinaryImage(name=name, size=size, pattern=pattern, alignment=alignment) + regions = config.get("regions") + if regions: + for i, region in enumerate(regions): + binary_file: Dict = region.get("binary_file") + if binary_file: + offset = binary_file.get("offset", ret.aligned_length(ret.alignment)) + name = binary_file.get("name", binary_file["path"]) + ret.add_image( + BinaryImage.load_binary_image( + binary_file["path"], + name=name, + offset=offset, + pattern=pattern, + search_paths=search_paths, + ) + ) + binary_block: Dict = region.get("binary_block") + if binary_block: + size = binary_block["size"] + offset = binary_block.get("offset", ret.aligned_length(ret.alignment)) + name = binary_block.get("name", f"Binary block(#{i})") + pattern = BinaryPattern(binary_block["pattern"]) + ret.add_image(BinaryImage(name, size, offset, pattern=pattern)) + return ret + + def save_binary_image( + self, + path: str, + file_format: str = "BIN", + ) -> None: + # pylint: disable=missing-param-doc + """Save binary data file. + + :param path: Path to the file. + :param file_format: Format of saved file ('BIN', 'HEX', 'S19'), defaults to 'BIN'. + :raises SPSDKValueError: The file format is invalid. + """ + file_format = file_format.upper() + if file_format.upper() not in ("BIN", "HEX", "S19"): + raise SPSDKValueError(f"Invalid input file format: {file_format}") + + if file_format == "BIN": + write_file(self.export(), path, mode="wb") + return + + def add_into_binary(bin_image: BinaryImage) -> None: + if bin_image.pattern: + bin_file.add_binary( + bin_image.pattern.get_block(len(bin_image)), + address=bin_image.absolute_address, + overwrite=True, + ) + + if bin_image.binary: + bin_file.add_binary( + bin_image.binary, address=bin_image.absolute_address, overwrite=True + ) + + for sub_image in bin_image.sub_images: + add_into_binary(sub_image) + + # import bincopy only if needed to save startup time + import bincopy # pylint: disable=import-outside-toplevel + + bin_file = bincopy.BinFile() + add_into_binary(self) + + if file_format == "HEX": + write_file(bin_file.as_ihex(), path) + return + + # And final supported format is....... Yes, S record from MOTOROLA + write_file(bin_file.as_srec(), path) + + @staticmethod + def generate_config_template() -> str: + """Generate configuration template. + + :return: Template to create binary merge.. + """ + return CommentedConfig( + "Binary Image Configuration template.", BinaryImage.get_validation_schemas() + ).get_template() + + @staticmethod + def load_binary_image( + path: str, + name: Optional[str] = None, + size: int = 0, + offset: int = 0, + description: Optional[str] = None, + pattern: Optional[BinaryPattern] = None, + search_paths: Optional[List[str]] = None, + alignment: int = 1, + load_bin: bool = True, + ) -> "BinaryImage": + # pylint: disable=missing-param-doc + r"""Load binary data file. + + Supported formats are ELF, HEX, SREC and plain binary + + :param path: Path to the file. + :param name: Name of Image, defaults to file name. + :param size: Image size, defaults to 0. + :param offset: Image offset in parent image, defaults to 0 + :param description: Text description of image, defaults to None + :param pattern: Optional binary pattern. + :param search_paths: List of paths where to search for the file, defaults to None + :param alignment: Optional alignment of result image + :param load_bin: Load as binary in case of every other format load fails + :raises SPSDKError: The binary file cannot be loaded. + :return: Binary data represented in BinaryImage class. + """ + path = find_file(path, search_paths=search_paths) + try: + with open(path, "rb") as f: + data = f.read(4) + except Exception as e: + raise SPSDKError(f"Error loading file: {str(e)}") from e + + # import bincopy only if needed to save startup time + import bincopy # pylint: disable=import-outside-toplevel + + bin_file = bincopy.BinFile() + try: + if data == b"\x7fELF": + bin_file.add_elf_file(path) + else: + try: + bin_file.add_file(path) + except (UnicodeDecodeError, bincopy.UnsupportedFileFormatError) as e: + if load_bin: + bin_file.add_binary_file(path) + else: + raise SPSDKError("Cannot load file as ELF, HEX or SREC") from e + except Exception as e: + raise SPSDKError(f"Error loading file: {str(e)}") from e + + img_name = name or os.path.basename(path) + img_size = size or 0 + img_descr = description or f"The image loaded from: {path} ." + bin_image = BinaryImage( + name=img_name, + size=img_size, + offset=offset, + description=img_descr, + pattern=pattern, + alignment=alignment, + ) + if len(bin_file.segments) == 0: + raise SPSDKError(f"Load of {path} failed, can't be decoded.") + + for i, segment in enumerate(bin_file.segments): + bin_image.add_image( + BinaryImage( + name=f"Segment {i}", + size=len(segment.data), + offset=segment.address, + pattern=pattern, + binary=segment.data, + parent=bin_image, + alignment=alignment, + ) + ) + # Optimize offsets in image + bin_image.update_offsets() + return bin_image diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/__init__.py new file mode 100644 index 00000000..7a3c4d88 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Device Interfaces.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/commands.py new file mode 100644 index 00000000..232ec055 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/commands.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Generic commands implementation.""" +from abc import ABC, abstractmethod + + +class CmdResponseBase(ABC): + """Response base format class.""" + + @abstractmethod + def __str__(self) -> str: + """Get object info.""" + + @property + @abstractmethod + def value(self) -> int: + """Return a integer representation of the response.""" + + +class CmdPacketBase(ABC): + """COmmand protocol base.""" + + @abstractmethod + def to_bytes(self, padding: bool = True) -> bytes: + """Serialize CmdPacket into bytes. + + :param padding: If True, add padding to specific size + :return: Serialized object into bytes + """ diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/__init__.py new file mode 100644 index 00000000..28f99a1b --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing the low level device.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/base.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/base.py new file mode 100644 index 00000000..8724cfa6 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/base.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level device base class.""" +import logging +from abc import ABC, abstractmethod +from types import TracebackType +from typing import Optional, Type + +from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class DeviceBase(ABC): + """Device base class.""" + + def __enter__(self) -> Self: + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + @property + @abstractmethod + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + + @abstractmethod + def open(self) -> None: + """Open the interface.""" + + @abstractmethod + def close(self) -> None: + """Close the interface.""" + + @abstractmethod + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read data from the device. + + :param length: Length of data to be read + :param timeout: Read timeout to be applied + """ + + @abstractmethod + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Write data to the device. + + :param data: Data to be written + :param timeout: Read timeout to be applied + """ + + @property + @abstractmethod + def timeout(self) -> int: + """Timeout property.""" + + @timeout.setter + @abstractmethod + def timeout(self, value: int) -> None: + """Timeout property setter.""" + + @abstractmethod + def __str__(self) -> str: + """Return string containing information about the interface.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py new file mode 100644 index 00000000..0f3f850f --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level sdio device.""" +import os +import time +from io import FileIO +from typing import List, Optional + +from typing_extensions import Self + +from ....exceptions import SPSDKConnectionError, SPSDKError +from ....utils.exceptions import SPSDKTimeoutError +from ....utils.interfaces.device.base import DeviceBase, logger +from ....utils.misc import Timeout + + +class SdioDevice(DeviceBase): + """SDIO device class.""" + + DEFAULT_TIMEOUT = 2000 + + def __init__( + self, + path: Optional[str] = None, + timeout: int = DEFAULT_TIMEOUT, + ) -> None: + """Initialize the SDIO interface object. + + :raises McuBootConnectionError: when the path is empty + """ + self._opened = False + # Temporarily use hard code until there is a way to retrive VID/PID + self.vid = 0x0471 + self.pid = 0x0209 + self._timeout = timeout + if path is None: + raise SPSDKConnectionError("No SDIO device path") + self.path = path + self.is_blocking = False + self.device: Optional[FileIO] = None + + @property + def timeout(self) -> int: + """Timeout property.""" + return self._timeout + + @timeout.setter + def timeout(self, value: int) -> None: + """Timeout property setter.""" + self._timeout = value + + @property + def is_opened(self) -> bool: + """Indicates whether device is open. + + :return: True if device is open, False othervise. + """ + return self.device is not None and self._opened + + def open(self) -> None: + """Open the interface with non-blocking mode. + + :raises McuBootError: if non-blocking mode is not available + :raises SPSDKError: if trying to open in non-blocking mode on non-linux os + :raises SPSDKConnectionError: if no device is available + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug("Opening the sdio device.") + if not self._opened: + try: + self.device = open(self.path, "rb+", buffering=0) + if self.device is None: + raise SPSDKConnectionError("No device available") + if not self.is_blocking: + if not hasattr(os, "set_blocking"): + raise SPSDKError("Opening in non-blocking mode is available only on Linux") + # pylint: disable=no-member # this is available only on Unix + os.set_blocking(self.device.fileno(), False) + self._opened = True + except Exception as error: + raise SPSDKConnectionError( + f"Unable to open device '{self.path}' VID={self.vid} PID={self.pid}" + ) from error + + def close(self) -> None: + """Close the interface. + + :raises SPSDKConnectionError: if no device is available + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug("Closing the sdio Interface.") + if not self.device: + raise SPSDKConnectionError("No device available") + if self._opened: + try: + self.device.close() + self._opened = False + except Exception as error: + raise SPSDKConnectionError( + f"Unable to close device '{self.path}' VID={self.vid} PID={self.pid}" + ) from error + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises SPSDKTimeoutError: Time-out + :raises SPSDKConnectionError: When device was not open for reading + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + _read = self._read_blocking if self.is_blocking else self._read_non_blocking + data = _read(length=length, timeout=timeout) + if not data: + raise SPSDKTimeoutError() + logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") + return data + + def _read_blocking(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device in blocking mode. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises SPSDKConnectionError: When reading data from device fails + :raises SPSDKConnectionError: Raises if device is not opened for reading + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing") + logger.debug("Reading with blocking mode.") + try: + return self.device.read(length) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + def _read_non_blocking(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device in non-blocking mode. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises TimeoutError: When timeout occurs + :raises SPSDKConnectionError: When reading data from device fails + :raises SPSDKConnectionError: Raises if device is not opened for reading + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + logger.debug("Reading with non-blocking mode.") + has_data = 0 + no_data_continuous = 0 + + data = bytearray() + _timeout = Timeout(timeout or self.timeout, "ms") + while len(data) < length: + try: + buf = self.device.read(length) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + if buf is None: + time.sleep(0.05) # delay for access device + if has_data != 0: + no_data_continuous = no_data_continuous + 1 + else: + data.extend(buf) + logger.debug("expend buf") + has_data = has_data + 1 + no_data_continuous = 0 + + if no_data_continuous > 5: + break + if _timeout.overflow(): + logger.debug("SDIO interface : read timeout") + break + return bytes(data) + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device with non-blocking mode. + + :param data: Data to send + :param timeout: Write timeout + :raises SPSDKConnectionError: Raises an error if device is not available + :raises SPSDKConnectionError: When sending the data fails + :raises TimeoutError: When timeout occurs + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing.") + logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") + _write = self._write_blocking if self.is_blocking else self._write_non_blocking + _write(data=data, timeout=timeout) + + def _write_blocking(self, data: bytes, timeout: Optional[int] = None) -> None: + """Write data to device in blocking mode. + + :param data: Data to be written + :param timeout: Write timeout + + :raises SPSDKConnectionError: When writing data to device fails + :raises SPSDKConnectionError: Raises if device is not opened for writing + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing") + logger.debug("Writing in blocking mode") + try: + self.device.write(data) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + def _write_non_blocking(self, data: bytes, timeout: Optional[int] = None) -> None: + """Write data to device in non-blocking mode. + + :param data: Data to be written + :param timeout: Write timeout + + :raises SPSDKConnectionError: When writing data to device fails + :raises SPSDKConnectionError: Raises if device is not opened for writing + """ + if not self.device or not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing") + logger.debug("Writing in non-blocking mode") + tx_len = len(data) + _timeout = Timeout(timeout or self.timeout, "ms") + while tx_len > 0: + try: + wr_count = self.device.write(data) + time.sleep(0.05) + data = data[wr_count:] + tx_len -= wr_count + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if _timeout.overflow(): + raise SPSDKTimeoutError() + + def __str__(self) -> str: + """Return information about the SDIO interface.""" + return f"(0x{self.vid:04X}, 0x{self.pid:04X})" + + @classmethod + def scan( + cls, + device_path: str, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected SDIO devices. + + :param device_path: device path string + :param timeout: default read/write timeout + :return: matched SDIO device + """ + if device_path is None: + logger.debug("No sdio path has been defined.") + devices = [] + try: + logger.debug(f"Checking path: {device_path}") + device = cls(path=device_path, timeout=timeout or cls.DEFAULT_TIMEOUT) + device.open() + device.close() + devices = [device] if device else [] + except Exception as e: # pylint: disable=broad-except + logger.debug(f"{type(e).__name__}: {e}") + devices = [] + return devices diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py new file mode 100644 index 00000000..ffa92b88 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level serial device.""" +import logging +from typing import List, Optional + +from serial import Serial, SerialException, SerialTimeoutException +from serial.tools.list_ports import comports +from typing_extensions import Self + +from spsdk.exceptions import SPSDKConnectionError +from spsdk.utils.exceptions import SPSDKTimeoutError +from spsdk.utils.interfaces.device.base import DeviceBase + +logger = logging.getLogger(__name__) + + +class SerialDevice(DeviceBase): + """Serial device class.""" + + default_baudrate = 115200 + default_timeout = 5000 + + def __init__( + self, + port: Optional[str] = None, + timeout: int = default_timeout, + baudrate: int = default_baudrate, + ): + """Initialize the UART interface. + + :param port: name of the serial port, defaults to None + :param baudrate: speed of the UART interface, defaults to 115200 + :param timeout: read/write timeout in milliseconds, defaults to 1000 + :raises SPSDKConnectionError: when there is no port available + """ + super().__init__() + self._timeout = timeout + try: + timeout_s = timeout / 1000 + self._device = Serial( + port=port, timeout=timeout_s, write_timeout=timeout_s, baudrate=baudrate + ) + self.expect_status = True + except SerialException as se: + logger.debug(f"Exception occurred during device opening: {se}") + self.expect_status = False + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + @property + def timeout(self) -> int: + """Timeout property.""" + return self._timeout + + @timeout.setter + def timeout(self, value: int) -> None: + """Timeout property setter.""" + self._timeout = value + self._device.timeout = value / 1000 + self._device.write_timeout = value / 1000 + + @property + def is_opened(self) -> bool: + """Indicates whether device is open. + + :return: True if device is open, False otherwise. + """ + if self.expect_status == False: + return False + else: + return self._device.is_open + + def open(self) -> None: + """Open the UART interface. + + :raises SPSDKConnectionError: when opening device fails + """ + if not self.is_opened: + try: + self._device.open() + except Exception as e: + self.close() + raise SPSDKConnectionError(str(e)) from e + + def close(self) -> None: + """Close the UART interface. + + :raises SPSDKConnectionError: when closing device fails + """ + if self.is_opened: + try: + self._device.reset_input_buffer() + self._device.reset_output_buffer() + self._device.close() + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises SPSDKTimeoutError: Time-out + :raises SPSDKConnectionError: When reading data from device fails + """ + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + try: + data = self._device.read(length) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if not data: + raise SPSDKTimeoutError() + logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") + return data + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device. + + :param data: Data to send + :param timeout: Write timeout + :raises SPSDKTimeoutError: when sending of data times-out + :raises SPSDKConnectionError: when send data to device fails + """ + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") + try: + self._device.reset_input_buffer() + self._device.reset_output_buffer() + self._device.write(data) + self._device.flush() + except SerialTimeoutException as e: + raise SPSDKTimeoutError( + f"Write timeout error. The timeout is set to {self._device.write_timeout} s. Consider increasing it." + ) from e + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + def __str__(self) -> str: + """Return information about the UART interface. + + :return: information about the UART interface + :raises SPSDKConnectionError: when information can not be collected from device + """ + try: + return self._device.port + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + + @classmethod + def scan( + cls, + port: Optional[str] = None, + baudrate: Optional[int] = None, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected serial ports. + + Returns list of serial ports with devices that respond to PING command. + If 'port' is specified, only that serial port is checked + If no devices are found, return an empty list. + + :param port: name of preferred serial port, defaults to None + :param baudrate: speed of the UART interface, defaults to 56700 + :param timeout: timeout in milliseconds, defaults to 5000 + :return: list of interfaces responding to the PING command + """ + baudrate = baudrate or cls.default_baudrate + timeout = timeout or 5000 + if port: + device = cls._check_port(port, baudrate, timeout) + devices = [device] if device else [] + else: + all_ports = [ + cls._check_port(comport.device, baudrate, timeout) + for comport in comports(include_links=True) + ] + devices = list(filter(None, all_ports)) + return devices + + @classmethod + def _check_port(cls, port: str, baudrate: int, timeout: int) -> Optional[Self]: + """Check if device on comport 'port' responds to PING command. + + :param port: name of port to check + :param baudrate: speed of the UART interface, defaults to 56700 + :param timeout: timeout in milliseconds + :return: None if device doesn't respond to PING, instance of Interface if it does + """ + try: + logger.debug(f"Checking port: {port}, baudrate: {baudrate}, timeout: {timeout}") + device = cls(port=port, baudrate=baudrate, timeout=timeout) + device.open() + device.close() + return device + except Exception as e: # pylint: disable=broad-except + logger.debug(f"{type(e).__name__}: {e}") + return None diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py new file mode 100644 index 00000000..058bb971 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level Hid device.""" +import logging +from typing import Dict, List, Optional + +import libusbsio +from typing_extensions import Self + +from ....exceptions import SPSDKConnectionError, SPSDKError +from ....utils.exceptions import SPSDKTimeoutError +from ....utils.interfaces.device.base import DeviceBase +from ....utils.misc import get_hash +from ....utils.usbfilter import NXPUSBDeviceFilter, USBDeviceFilter + +logger = logging.getLogger(__name__) + + +class UsbDevice(DeviceBase): + """USB device class.""" + + def __init__( + self, + vid: Optional[int] = None, + pid: Optional[int] = None, + path: Optional[bytes] = None, + serial_number: Optional[str] = None, + vendor_name: Optional[str] = None, + product_name: Optional[str] = None, + interface_number: Optional[int] = None, + timeout: Optional[int] = None, + ) -> None: + """Initialize the USB interface object.""" + self._opened = False + self.vid = vid or 0 + self.pid = pid or 0 + self.path = path or b"" + self.serial_number = serial_number or "" + self.vendor_name = vendor_name or "" + self.product_name = product_name or "" + self.interface_number = interface_number or 0 + self._timeout = timeout or 2000 + libusbsio_logger = logging.getLogger("libusbsio") + self._device: libusbsio.LIBUSBSIO.HID_DEVICE = libusbsio.usbsio( + loglevel=libusbsio_logger.getEffectiveLevel() + ).HIDAPI_DeviceCreate() + + @property + def timeout(self) -> int: + """Timeout property.""" + return self._timeout + + @timeout.setter + def timeout(self, value: int) -> None: + """Timeout property setter.""" + self._timeout = value + + @property + def is_opened(self) -> bool: + """Indicates whether device is open. + + :return: True if device is open, False othervise. + """ + return self._opened + + def open(self) -> None: + """Open the interface. + + :raises SPSDKError: if device is already opened + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug(f"Opening the Interface: {str(self)}") + if self.is_opened: + # This would get HID_DEVICE into broken state + raise SPSDKError("Can't open already opened device") + try: + self._device.Open(self.path) + self._opened = True + except Exception as error: + raise SPSDKConnectionError(f"Unable to open device '{str(self)}'") from error + + def close(self) -> None: + """Close the interface. + + :raises SPSDKConnectionError: if no device is available + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug(f"Closing the Interface: {str(self)}") + if self.is_opened: + try: + self._device.Close() + self._opened = False + except Exception as error: + raise SPSDKConnectionError(f"Unable to close device '{str(self)}'") from error + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read data on the IN endpoint associated to the HID interface. + + :return: Return CmdResponse object. + :raises SPSDKConnectionError: Raises an error if device is not opened for reading + :raises SPSDKConnectionError: Raises if device is not available + :raises SPSDKConnectionError: Raises if reading fails + :raises SPSDKTimeoutError: Time-out + """ + timeout = timeout or self.timeout + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + try: + (data, result) = self._device.Read(length, timeout_ms=timeout) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if not data: + logger.error(f"Cannot read from HID device, error={result}") + raise SPSDKTimeoutError() + return data + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device. + + :param data: Data to send + :param timeout: Timeout to be used + :raises SPSDKConnectionError: Sending data to device failure + """ + timeout = timeout or self.timeout + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing") + try: + bytes_written = self._device.Write(data, timeout_ms=timeout) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if bytes_written < 0 or bytes_written < len(data): + raise SPSDKConnectionError( + f"Invalid size of written bytes has been detected: {bytes_written} != {len(data)}" + ) + + def __str__(self) -> str: + """Return information about the USB interface.""" + return ( + f"{self.product_name:s} (0x{self.vid:04X}, 0x{self.pid:04X})" + f"path={self.path!r} sn='{self.serial_number}'" + ) + + @property + def path_str(self) -> str: + """BLHost-friendly string representation of USB path.""" + return NXPUSBDeviceFilter.convert_usb_path(self.path) + + @property + def path_hash(self) -> str: + """BLHost-friendly hash of the USB path.""" + return get_hash(self.path) + + def __hash__(self) -> int: + return hash(self.path) + + @classmethod + def scan( + cls, + device_id: Optional[str] = None, + usb_devices_filter: Optional[Dict] = None, + timeout: Optional[int] = None, + ) -> List[Self]: + """Scan connected USB devices. + + :param device_id: Device identifier , , device/instance path, device name are supported + :param usb_devices_filter: Dictionary holding NXP device vid/pid {"device_name": [vid(int), pid(int)]}. + If set, only devices included in the dictionary will be scanned + :param timeout: Read/write timeout + :return: list of matching RawHid devices + """ + usb_filter = NXPUSBDeviceFilter(usb_id=device_id, nxp_device_names=usb_devices_filter) + devices = cls.enumerate(usb_filter, timeout=timeout) + return devices + + @classmethod + def enumerate( + cls, usb_device_filter: USBDeviceFilter, timeout: Optional[int] = None + ) -> List[Self]: + """Get list of all connected devices which matches device_id. + + :param usb_device_filter: USBDeviceFilter object + :param timeout: Default timeout to be set + :return: List of interfaces found + """ + devices = [] + libusbsio_logger = logging.getLogger("libusbsio") + sio = libusbsio.usbsio(loglevel=libusbsio_logger.getEffectiveLevel()) + all_hid_devices = sio.HIDAPI_Enumerate() + + # iterate on all devices found + for dev in all_hid_devices: + if usb_device_filter.compare(vars(dev)) is True: + new_device = cls( + vid=dev["vendor_id"], + pid=dev["product_id"], + path=dev["path"], + vendor_name=dev["manufacturer_string"], + product_name=dev["product_string"], + interface_number=dev["interface_number"], + timeout=timeout, + ) + devices.append(new_device) + return devices diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py new file mode 100644 index 00000000..c4819acd --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright (c) 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level usbsio device.""" +import logging +import re +from dataclasses import dataclass +from typing import List, Optional, Union + +import libusbsio +from libusbsio.libusbsio import LIBUSBSIO +from typing_extensions import Self + +from ....exceptions import SPSDKConnectionError, SPSDKError, SPSDKValueError +from ....utils.exceptions import SPSDKTimeoutError +from ....utils.interfaces.device.base import DeviceBase +from ....utils.misc import value_to_int +from ....utils.usbfilter import USBDeviceFilter + +logger = logging.getLogger(__name__) + + +@dataclass +class ScanArgs: + """Scan arguments dataclass.""" + + config: str + + @classmethod + def parse(cls, params: str) -> Self: + """Parse given scanning parameters into ScanArgs class. + + :param params: Parameters as a string + """ + return cls(config=params) + + +class UsbSioDevice(DeviceBase): + """USBSIO device class.""" + + def __init__(self, dev: int = 0, config: Optional[str] = None, timeout: int = 5000) -> None: + """Initialize the Interface object. + + :param dev: device index to be used, default is set to 0 + :param config: configuration string identifying spi or i2c SIO interface + :param timeout: read timeout in milliseconds, defaults to 5000 + :raises SPSDKError: When LIBUSBSIO device is not opened. + """ + # device is the LIBUSBSIO.PORT instance (LIBUSBSIO.SPI or LIBUSBSIO.I2C class) + self.port: Optional[Union[LIBUSBSIO.SPI, LIBUSBSIO.I2C]] = None + + # work with the global LIBUSBSIO instance + self.dev_ix = dev + self.sio = self._get_usbsio() + self._timeout = timeout + + # store USBSIO configuration and version + self.config = config + + @property + def timeout(self) -> int: + """Timeout property.""" + return self._timeout + + @timeout.setter + def timeout(self, value: int) -> None: + """Timeout property setter.""" + self._timeout = value + + @property + def is_opened(self) -> bool: + """Indicates whether device is open. + + :return: True if device is open, False othervise. + """ + return bool(self.port) + + def close(self) -> None: + """Close the interface.""" + if self.port: + self.port.Close() + self.port = None + self.sio.Close() + # re-init the libusb to prepare it for next open + self.sio.GetNumPorts() + + def __str__(self) -> str: + """Return string containing information about the interface.""" + class_name = self.__class__.__name__ + config = f":'{self.config}'" if self.config else "" + return f"libusbsio interface ({class_name}){config}" + + @staticmethod + def get_interface_cfg(config: str, interface: str) -> str: + """Return part of interface config. + + :param config: Full config of LIBUSBSIO + :param interface: Name of interface to find. + :return: Part with interface config. + """ + i = config.rfind(interface) + if i < 0: + return "" + return config[i:] + + @staticmethod + def _get_usbsio() -> LIBUSBSIO: + """Wraps getting USBSIO library to raise SPSDK errors in case of problem. + + :return: LIBUSBSIO object + :raises SPSDKError: When libusbsio library error or if no bridge device found + """ + try: + # get the global singleton instance of LIBUSBSIO library + libusbsio_logger = logging.getLogger("libusbsio") + return libusbsio.usbsio(loglevel=libusbsio_logger.getEffectiveLevel()) + except libusbsio.LIBUSBSIO_Exception as e: + raise SPSDKError(f"Error in libusbsio interface: {e}") from e + except Exception as e: + raise SPSDKError(str(e)) from e + + @classmethod + def scan( + cls, config: Optional[str] = None, timeout: int = 5000 + ) -> List[Union["UsbSioSPIDevice", "UsbSioI2CDevice"]]: + """Scan connected USB-SIO bridge devices. + + :param config: Configuration string identifying spi or i2c SIO interface + and could filter out USB devices + :param timeout: Read timeout in milliseconds, defaults to 5000 + :return: List of matching UsbSio devices + :raises SPSDKError: When libusbsio library error or if no bridge device found + :raises SPSDKValueError: Invalid configuration detected. + """ + cfg = config.split(",") if config else [] + re_spi = re.compile(r"^spi(?P\d*)") + re_i2c = re.compile(r"^i2c(?P\d*)") + spi = None + i2c = None + for cfg_part in cfg: + match_i2c = re_i2c.match(cfg_part.lower()) + if match_i2c: + i2c = value_to_int(match_i2c.group("index"), 0) + match_spi = re_spi.match(cfg_part.lower()) + if match_spi: + spi = value_to_int(match_spi.group("index"), 0) + if i2c is not None and spi is not None: + raise SPSDKValueError( + f"Cannot be specified spi and i2c together in configuration: {cfg}" + ) + intf_specified = i2c is not None or spi is not None + + port_indexes = cls.get_usbsio_devices(config) + sio = cls._get_usbsio() + devices: List[Union["UsbSioSPIDevice", "UsbSioI2CDevice"]] = [] + for port in port_indexes: + if not sio.Open(port): + raise SPSDKError(f"Cannot open libusbsio bridge {port}.") + i2c_ports = sio.GetNumI2CPorts() + if i2c_ports: + if i2c is not None: + devices.append( + UsbSioI2CDevice(dev=port, port=i2c, config=config, timeout=timeout) + ) + elif not intf_specified: + devices.extend( + [ + UsbSioI2CDevice(dev=port, port=p, timeout=timeout) + for p in range(i2c_ports) + ] + ) + spi_ports = sio.GetNumSPIPorts() + if spi_ports: + if spi is not None: + devices.append( + UsbSioSPIDevice(dev=port, port=spi, config=config, timeout=timeout) + ) + elif not intf_specified: + devices.extend( + [ + UsbSioSPIDevice(dev=port, port=p, timeout=timeout) + for p in range(spi_ports) + ] + ) + if sio.Close() < 0: + raise SPSDKError(f"Cannot close libusbsio bridge {port}.") + # re-init the libusb to prepare it for next open + sio.GetNumPorts() + return devices + + @classmethod + def get_usbsio_devices(cls, config: Optional[str] = None) -> List[int]: + """Returns list of ports indexes of USBSIO devices. + + It could be filtered by standard SPSDK USB filters. + + :param config: Could contain USB filter configuration, defaults to None + :return: List of port indexes of founded USBSIO device + """ + + def _filter_usb(sio: LIBUSBSIO, ports: List[int], flt: str) -> List[int]: + """Filter the LIBUSBSIO device. + + :param sio: LIBUSBSIO instance. + :param ports: Input list of LIBUSBSIO available ports. + :param flt: Filter string (PATH, PID/VID, SERIAL_NUMBER) + :raises SPSDKError: When libusbsio library error or if no bridge device found + :return: List with selected device, empty list otherwise. + """ + usb_filter = USBDeviceFilter(flt.casefold()) + port_indexes = [] + for port in ports: + info = sio.GetDeviceInfo(port) + if not info: + raise SPSDKError(f"Cannot retrive information from LIBUSBSIO device {port}.") + dev_info = { + "vendor_id": info.vendor_id, + "product_id": info.product_id, + "serial_number": info.serial_number, + "path": info.path, + } + if usb_filter.compare(dev_info): + port_indexes.append(port) + break + return port_indexes + + cfg = config.split(",") if config else [] + port_indexes = [] + + sio = UsbSioDevice._get_usbsio() + # it may already be open (?), in that case, just close it - We are scan function! + if sio.IsOpen(): + sio.Close() + + port_indexes.extend(list(range(sio.GetNumPorts()))) + + # filter out the USB devices + if cfg and cfg[0] == "usb": + port_indexes = _filter_usb(sio, port_indexes, cfg[1]) + + return port_indexes + + +class UsbSioSPIDevice(UsbSioDevice): + """USBSIO SPI interface.""" + + def __init__( + self, + config: Optional[str] = None, + dev: int = 0, + port: int = 0, + ssel_port: int = 0, + ssel_pin: int = 15, + speed_khz: int = 1000, + cpol: int = 1, + cpha: int = 1, + timeout: int = 5000, + ) -> None: + """Initialize the UsbSioSPI Interface object. + + :param config: configuration string passed from command line + :param dev: device index to be used, default is set to 0 + :param port: default SPI port to be used, typically 0 as only one port is supported by LPCLink2/MCULink + :param ssel_port: bridge GPIO port used to drive SPI SSEL signal + :param ssel_pin: bridge GPIO pin used to drive SPI SSEL signal + :param speed_khz: SPI clock speed in kHz + :param cpol: SPI clock polarity mode + :param cpha: SPI clock phase mode + :param timeout: read timeout in milliseconds, defaults to 5000 + :raises SPSDKError: When port configuration cannot be parsed + """ + super().__init__(dev=dev, config=config, timeout=timeout) + + # default configuration taken from parameters (and their default values) + self.spi_port = port + self.spi_sselport = ssel_port + self.spi_sselpin = ssel_pin + self.spi_speed_khz = speed_khz + self.spi_cpol = cpol + self.spi_cpha = cpha + + # values can be also overridden by a configuration string + if config: + # config format: spi[,,,,,] + cfg = self.get_interface_cfg(config, "spi").split(",") + try: + self.spi_sselport = int(cfg[1], 0) + self.spi_sselpin = int(cfg[2], 0) + self.spi_speed_khz = int(cfg[3], 0) + self.spi_cpol = int(cfg[4], 0) + self.spi_cpha = int(cfg[5], 0) + except IndexError: + pass + except Exception as e: + raise SPSDKError( + "Cannot parse lpcusbsio SPI parameters.\n" + "Expected: spi[,,,,,]\n" + f"Given: {config}" + ) from e + + def open(self) -> None: + """Open the interface.""" + if not self.sio.IsOpen(): + self.sio.Open(self.dev_ix) + + self.port: LIBUSBSIO.SPI = self.sio.SPI_Open( + portNum=self.spi_port, + busSpeed=self.spi_speed_khz * 1000, + cpol=self.spi_cpol, + cpha=self.spi_cpha, + ) + if not self.port: + raise SPSDKError("Cannot open lpcusbsio SPI interface.\n") + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises SPSDKConnectionError: When reading data from device fails + :raises TimeoutError: When no data received + """ + try: + (data, result) = self.port.Transfer( + devSelectPort=self.spi_sselport, + devSelectPin=self.spi_sselpin, + txData=None, + size=length, + ) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if result < 0 or not data: + raise SPSDKTimeoutError() + logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") + return data + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device. + + :param data: Data to send + :param timeout: Write timeout + :raises SPSDKConnectionError: When sending the data fails + :raises SPSDKTimeoutError: When data could not be written + """ + logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") + try: + (dummy, result) = self.port.Transfer( + devSelectPort=self.spi_sselport, devSelectPin=self.spi_sselpin, txData=data + ) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if result < 0: + raise SPSDKTimeoutError() + + +class UsbSioI2CDevice(UsbSioDevice): + """USBSIO I2C interface.""" + + def __init__( + self, + config: Optional[str] = None, + dev: int = 0, + port: int = 0, + address: int = 0x10, + speed_khz: int = 100, + timeout: int = 5000, + ) -> None: + """Initialize the UsbSioI2C Interface object. + + :param config: configuration string passed from command line + :param dev: device index to be used, default is set to 0 + :param port: default I2C port to be used, typically 0 as only one port is supported by LPCLink2/MCULink + :param address: I2C target device address + :param speed_khz: I2C clock speed in kHz + :param timeout: read timeout in milliseconds, defaults to 5000 + :raises SPSDKError: When port configuration cannot be parsed + """ + super().__init__(dev=dev, config=config, timeout=timeout) + + # default configuration taken from parameters (and their default values) + self.i2c_port = port + self.i2c_address = address + self.i2c_speed_khz = speed_khz + + # values can be also overridden by a configuration string + if config: + # config format: i2c[,
,] + cfg = self.get_interface_cfg(config, "i2c").split(",") + try: + self.i2c_address = int(cfg[1], 0) + self.i2c_speed_khz = int(cfg[2], 0) + except IndexError: + pass + except Exception as e: + raise SPSDKError( + "Cannot parse lpcusbsio I2C parameters.\n" + "Expected: i2c[,
,]\n" + f"Given: {config}" + ) from e + + def open(self) -> None: + """Open the interface.""" + if not self.sio.IsOpen(): + self.sio.Open(self.dev_ix) + self.port: LIBUSBSIO.I2C = self.sio.I2C_Open( + clockRate=self.i2c_speed_khz * 1000, portNum=self.i2c_port + ) + if not self.port: + raise SPSDKError("Cannot open lpcusbsio I2C interface.\n") + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read 'length' amount for bytes from device. + + :param length: Number of bytes to read + :param timeout: Read timeout + :return: Data read from the device + :raises SPSDKConnectionError: When reading data from device fails + :raises SPSDKTimeoutError: When no data received + """ + try: + (data, result) = self.port.DeviceRead(devAddr=self.i2c_address, rxSize=length) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if result < 0 or not data: + raise SPSDKTimeoutError() + logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") + return data + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device. + + :param data: Data to send + :param timeout: Write timeout + :raises SPSDKConnectionError: When sending the data fails + :raises TimeoutError: When data NAKed or could not be written + """ + logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") + try: + result = self.port.DeviceWrite(devAddr=self.i2c_address, txData=data) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if result < 0: + raise SPSDKTimeoutError() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py new file mode 100644 index 00000000..e06c1a13 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Protocol base.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py new file mode 100644 index 00000000..53e3741d --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Protocol base.""" +from abc import ABC, abstractmethod +from types import ModuleType, TracebackType +from typing import Dict, List, Optional, Type, Union + +from typing_extensions import Self + +from ....exceptions import SPSDKError +from ....utils.interfaces.commands import CmdPacketBase, CmdResponseBase +from ....utils.interfaces.device.base import DeviceBase +from ....utils.plugins import PluginsManager, PluginType + + +class ProtocolBase(ABC): + """Protocol base class.""" + + device: DeviceBase + identifier: str + + def __init__(self, device: DeviceBase) -> None: + """Initialize the MbootSerialProtocol object. + + :param device: The device instance + """ + self.device = device + + def __str__(self) -> str: + return f"identifier='{self.identifier}', device={self.device}" + + def __enter__(self) -> Self: + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + @abstractmethod + def open(self) -> None: + """Open the interface.""" + + @abstractmethod + def close(self) -> None: + """Close the interface.""" + + @property + @abstractmethod + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + + @classmethod + @abstractmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List[Self]: + """Scan method.""" + + @abstractmethod + def write_command(self, packet: CmdPacketBase) -> None: + """Write command to the device. + + :param packet: Command packet to be sent + """ + + @abstractmethod + def write_data(self, data: bytes) -> None: + """Write data to the device. + + :param data: Data to be send + """ + + @abstractmethod + def read(self, length: Optional[int] = None) -> Union[CmdResponseBase, bytes]: + """Read data from device. + + :return: read data + """ + + @classmethod + def _get_interfaces(cls) -> List[Type[Self]]: + """Get list of all available interfaces.""" + cls._load_plugins() + return [ + sub_class + for sub_class in cls._get_subclasses(cls) + if getattr(sub_class, "identifier", None) + ] + + @classmethod + def get_interface(cls, identifier: str) -> Type[Self]: + """Get list of all available interfaces.""" + interface = next( + (iface for iface in cls._get_interfaces() if iface.identifier == identifier), None + ) + if not interface: + raise SPSDKError(f"Interface with identifier {identifier} does not exist.") + return interface + + @staticmethod + def _load_plugins() -> Dict[str, ModuleType]: + """Load all installed interface plugins.""" + plugins_manager = PluginsManager() + plugins_manager.load_from_entrypoints(PluginType.DEVICE_INTERFACE.label) + return plugins_manager.plugins + + @classmethod + def _get_subclasses( + cls, + base_class: Type, + ) -> List[Type[Self]]: + """Recursively find all subclasses.""" + subclasses = [] + for subclass in base_class.__subclasses__(): + subclasses.append(subclass) + subclasses.extend(cls._get_subclasses(subclass)) + return subclasses diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py new file mode 100644 index 00000000..ede09d3b --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Helper module used for supporting the scanning.""" + +from dataclasses import dataclass +from typing import Dict, Optional, Tuple + +from ...exceptions import SPSDKKeyError + + +def parse_plugin_config(plugin_conf: str) -> Tuple[str, str]: + """Extract 'identifier' from plugin params and build the params back to original format. + + :param plugin_conf: Plugin configuration string as given on command line + :return: Tuple with identifier and params + """ + params_dict: Dict[str, str] = dict([tuple(p.split("=")) for p in plugin_conf.split(",")]) # type: ignore + if "identifier" not in params_dict: + raise SPSDKKeyError("Plugin parameter must contain 'identifier' key") + identifier = params_dict.pop("identifier") + params = ",".join([f"{key}={value}" for key, value in params_dict.items()]) + return identifier, params + + +@dataclass +class InterfaceParams: + """Interface input parameters.""" + + identifier: str + is_defined: bool + params: Optional[str] = None + extra_params: Optional[str] = None diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py new file mode 100644 index 00000000..5fca3c50 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py @@ -0,0 +1,915 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Miscellaneous functions used throughout the SPSDK.""" +import contextlib +import hashlib +import json +import logging +import math +import os +import re +import textwrap +import time +from enum import Enum +from math import ceil +from struct import pack, unpack +from typing import ( + Any, + Callable, + Dict, + Generator, + Iterable, + Iterator, + List, + Optional, + Type, + TypeVar, + Union, +) + +from spsdk.crypto.rng import random_bytes +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.exceptions import SPSDKTimeoutError + +# for generics +T = TypeVar("T") # pylint: disable=invalid-name + +logger = logging.getLogger(__name__) + + +class Endianness(str, Enum): + """Endianness enum.""" + + BIG = "big" + LITTLE = "little" + + @classmethod + def values(cls) -> List[str]: + """Get enumeration values.""" + return [mem.value for mem in Endianness.__members__.values()] + + +class BinaryPattern: + """Binary pattern class. + + Supported patterns: + - rand: Random Pattern + - zeros: Filled with zeros + - ones: Filled with all ones + - inc: Filled with repeated numbers incremented by one 0-0xff + - any kind of number, that will be repeated to fill up whole image. + The format could be decimal, hexadecimal, bytes. + """ + + SPECIAL_PATTERNS = ["rand", "zeros", "ones", "inc"] + + def __init__(self, pattern: str) -> None: + """Constructor of pattern class. + + :param pattern: Supported patterns: + - rand: Random Pattern + - zeros: Filled with zeros + - ones: Filled with all ones + - inc: Filled with repeated numbers incremented by one 0-0xff + - any kind of number, that will be repeated to fill up whole image. + The format could be decimal, hexadecimal, bytes. + :raises SPSDKValueError: Unsupported pattern detected. + """ + try: + value_to_int(pattern) + except SPSDKError: + if not pattern in BinaryPattern.SPECIAL_PATTERNS: + raise SPSDKValueError( # pylint: disable=raise-missing-from + f"Unsupported input pattern {pattern}" + ) + + self._pattern = pattern + + def get_block(self, size: int) -> bytes: + """Get block filled with pattern. + + :param size: Size of block to return. + :return: Filled up block with specified pattern. + """ + if self._pattern == "zeros": + return bytes(size) + + if self._pattern == "ones": + return bytes(b"\xff" * size) + + if self._pattern == "rand": + return random_bytes(size) + + if self._pattern == "inc": + return bytes((x & 0xFF for x in range(size))) + + pattern = value_to_bytes(self._pattern, align_to_2n=False) + block = bytes(pattern * (int((size / len(pattern))) + 1)) + return block[:size] + + @property + def pattern(self) -> str: + """Get the pattern. + + :return: Pattern in string representation. + """ + try: + return hex(value_to_int(self._pattern)) + except SPSDKError: + return self._pattern + + +def align(number: int, alignment: int = 4) -> int: + """Align number (size or address) size to specified alignment, typically 4, 8 or 16 bytes boundary. + + :param number: input to be aligned + :param alignment: the boundary to align; typical value is power of 2 + :return: aligned number; result is always >= size (e.g. aligned up) + :raises SPSDKError: When there is wrong alignment + """ + if alignment <= 0 or number < 0: + raise SPSDKError("Wrong alignment") + + return (number + (alignment - 1)) // alignment * alignment + + +def align_block( + data: Union[bytes, bytearray], + alignment: int = 4, + padding: Optional[Union[int, str, BinaryPattern]] = None, +) -> bytes: + """Align binary data block length to specified boundary by adding padding bytes to the end. + + :param data: to be aligned + :param alignment: boundary alignment (typically 2, 4, 16, 64 or 256 boundary) + :param padding: byte to be added or BinaryPattern + :return: aligned block + :raises SPSDKError: When there is wrong alignment + """ + assert isinstance(data, (bytes, bytearray)) + + if alignment < 0: + raise SPSDKError("Wrong alignment") + current_size = len(data) + num_padding = align(current_size, alignment) - current_size + if not num_padding: + return bytes(data) + if not padding: + padding = BinaryPattern("zeros") + elif not isinstance(padding, BinaryPattern): + padding = BinaryPattern(str(padding)) + return bytes(data + padding.get_block(num_padding)) + + +def align_block_fill_random(data: bytes, alignment: int = 4) -> bytes: + """Same as `align_block`, just parameter `padding` is fixed to `-1` to fill with random data.""" + return align_block(data, alignment, BinaryPattern("rand")) + + +def extend_block(data: bytes, length: int, padding: int = 0) -> bytes: + """Add padding to the binary data block to extend the length to specified value. + + :param data: block to be extended + :param length: requested block length; the value must be >= current block length + :param padding: 8-bit value value to be used as a padding + :return: block extended with padding + :raises SPSDKError: When the length is incorrect + """ + current_len = len(data) + if length < current_len: + raise SPSDKError("Incorrect length") + num_padding = length - current_len + if not num_padding: + return data + return data + bytes([padding]) * num_padding + + +def find_first(iterable: Iterable[T], predicate: Callable[[T], bool]) -> Optional[T]: + """Find first element from the list, that matches the condition. + + :param iterable: list of elements + :param predicate: function for selection of the element + :return: found element; None if not found + """ + return next((a for a in iterable if predicate(a)), None) + + +def load_binary(path: str, search_paths: Optional[List[str]] = None) -> bytes: + """Loads binary file into bytes. + + :param path: Path to the file. + :param search_paths: List of paths where to search for the file, defaults to None + :return: content of the binary file as bytes + """ + data = load_file(path, mode="rb", search_paths=search_paths) + assert isinstance(data, bytes) + return data + + +def load_text(path: str, search_paths: Optional[List[str]] = None) -> str: + """Loads text file into string. + + :param path: Path to the file. + :param search_paths: List of paths where to search for the file, defaults to None + :return: content of the text file as string + """ + text = load_file(path, mode="r", search_paths=search_paths) + assert isinstance(text, str) + return text + + +def load_file( + path: str, mode: str = "r", search_paths: Optional[List[str]] = None +) -> Union[str, bytes]: + """Loads a file into bytes. + + :param path: Path to the file. + :param mode: mode for reading the file 'r'/'rb' + :param search_paths: List of paths where to search for the file, defaults to None + :return: content of the binary file as bytes or str (based on mode) + """ + path = find_file(path, search_paths=search_paths) + logger.debug(f"Loading {'binary' if 'b' in mode else 'text'} file from {path}") + with open(path, mode) as f: + return f.read() + + +def write_file( + data: Union[str, bytes], path: str, mode: str = "w", encoding: Optional[str] = None +) -> int: + """Writes data into a file. + + :param data: data to write + :param path: Path to the file. + :param mode: writing mode, 'w' for text, 'wb' for binary data, defaults to 'w' + :param encoding: Encoding of written file ('ascii', 'utf-8'). + :return: number of written elements + """ + path = path.replace("\\", "/") + folder = os.path.dirname(path) + if folder and not os.path.exists(folder): + os.makedirs(folder, exist_ok=True) + + logger.debug(f"Storing {'binary' if 'b' in mode else 'text'} file at {path}") + with open(path, mode, encoding=encoding) as f: + return f.write(data) + + +def get_abs_path(file_path: str, base_dir: Optional[str] = None) -> str: + """Return a full path to the file. + + param base_dir: Base directory to create absolute path, if not specified the system CWD is used. + return: Absolute file path. + """ + if os.path.isabs(file_path): + return file_path.replace("\\", "/") + + return os.path.abspath(os.path.join(base_dir or os.getcwd(), file_path)).replace("\\", "/") + + +def _find_path( + path: str, + check_func: Callable[[str], bool], + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the file. + + `search_paths` takes precedence over `CWD` if used (default) + + :param path: File name, part of file path or full path + :param use_cwd: Try current working directory to find the file, defaults to True + :param search_paths: List of paths where to search for the file, defaults to None + :param raise_exc: Raise exception if file is not found, defaults to True + :return: Full path to the file + :raises SPSDKError: File not found + """ + path = path.replace("\\", "/") + + if os.path.isabs(path): + if not check_func(path): + raise SPSDKError(f"Path '{path}' not found") + return path + if search_paths: + for dir_candidate in search_paths: + if not dir_candidate: + continue + dir_candidate = dir_candidate.replace("\\", "/") + path_candidate = get_abs_path(path, base_dir=dir_candidate) + if check_func(path_candidate): + return path_candidate + if use_cwd and check_func(path): + return get_abs_path(path) + # list all directories in error message + searched_in: List[str] = [] + if use_cwd: + searched_in.append(os.path.abspath(os.curdir)) + if search_paths: + searched_in.extend(filter(None, search_paths)) + searched_in = [s.replace("\\", "/") for s in searched_in] + err_str = f"Path '{path}' not found, Searched in: {', '.join(searched_in)}" + if not raise_exc: + logger.debug(err_str) + return "" + raise SPSDKError(err_str) + + +def find_dir( + dir_path: str, + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the directory. + + `search_paths` takes precedence over `CWD` if used (default) + + :param dir_path: Directory name, part of directory path or full path + :param use_cwd: Try current working directory to find the directory, defaults to True + :param search_paths: List of paths where to search for the directory, defaults to None + :param raise_exc: Raise exception if directory is not found, defaults to True + :return: Full path to the directory + :raises SPSDKError: File not found + """ + return _find_path( + path=dir_path, + check_func=os.path.isdir, + use_cwd=use_cwd, + search_paths=search_paths, + raise_exc=raise_exc, + ) + + +def find_file( + file_path: str, + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the file. + + `search_paths` takes precedence over `CWD` if used (default) + + :param file_path: File name, part of file path or full path + :param use_cwd: Try current working directory to find the file, defaults to True + :param search_paths: List of paths where to search for the file, defaults to None + :param raise_exc: Raise exception if file is not found, defaults to True + :return: Full path to the file + :raises SPSDKError: File not found + """ + return _find_path( + path=file_path, + check_func=os.path.isfile, + use_cwd=use_cwd, + search_paths=search_paths, + raise_exc=raise_exc, + ) + + +@contextlib.contextmanager +def use_working_directory(path: str) -> Iterator[None]: + # pylint: disable=missing-yield-doc + """Execute the block in given directory. + + Cd into specific directory. + Execute the block. + Change the directory back into the original one. + + :param path: the path, where the current directory will be changed to + """ + current_dir = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(current_dir) + assert os.getcwd() == current_dir + + +def format_value(value: int, size: int, delimiter: str = "_", use_prefix: bool = True) -> str: + """Convert the 'value' into either BIN or HEX string, depending on 'size'. + + if 'size' is divisible by 8, function returns HEX, BIN otherwise + digits in result string are grouped by 4 using 'delimiter' (underscore) + """ + padding = size if size % 8 else (size // 8) * 2 + infix = "b" if size % 8 else "x" + sign = "-" if value < 0 else "" + parts = re.findall(".{1,4}", f"{abs(value):0{padding}{infix}}"[::-1]) + rev = delimiter.join(parts)[::-1] + prefix = f"0{infix}" if use_prefix else "" + return f"{sign}{prefix}{rev}" + + +def get_bytes_cnt_of_int( + value: int, align_to_2n: bool = True, byte_cnt: Optional[int] = None +) -> int: + """Returns count of bytes needed to store handled integer. + + :param value: Input integer value. + :param align_to_2n: The result will be aligned to standard sizes 1,2,4,8,12,16,20. + :param byte_cnt: The result count of bytes. + :raises SPSDKValueError: The integer input value doesn't fit into byte_cnt. + :return: Number of bytes needed to store integer. + """ + cnt = 0 + if value == 0: + return byte_cnt or 1 + + while value != 0: + value >>= 8 + cnt += 1 + + if align_to_2n and cnt > 2: + cnt = int(ceil(cnt / 4)) * 4 + + if byte_cnt and cnt > byte_cnt: + raise SPSDKValueError( + f"Value takes more bytes than required byte count {byte_cnt} after align." + ) + + cnt = byte_cnt or cnt + + return cnt + + +def value_to_int(value: Union[bytes, bytearray, int, str], default: Optional[int] = None) -> int: + """Function loads value from lot of formats to integer. + + :param value: Input value. + :param default: Default Value in case of invalid input. + :return: Value in Integer. + :raises SPSDKError: Unsupported input type. + """ + if isinstance(value, int): + return value + + if isinstance(value, (bytes, bytearray)): + return int.from_bytes(value, Endianness.BIG.value) + + if isinstance(value, str) and value != "": + match = re.match( + r"(?P0[box])?(?P[0-9a-f_]+)(?P[ul]{0,3})$", + value.strip().lower(), + ) + if match: + base = {"0b": 2, "0o": 8, "0": 10, "0x": 16, None: 10}[match.group("prefix")] + try: + return int(match.group("number"), base=base) + except ValueError: + pass + + if default is not None: + return default + raise SPSDKError(f"Invalid input number type({type(value)}) with value ({value})") + + +def value_to_bytes( + value: Union[bytes, bytearray, int, str], + align_to_2n: bool = True, + byte_cnt: Optional[int] = None, + endianness: Endianness = Endianness.BIG, +) -> bytes: + """Function loads value from lot of formats. + + :param value: Input value. + :param align_to_2n: When is set, the function aligns length of return array to 1,2,4,8,12 etc. + :param byte_cnt: The result count of bytes. + :param endianness: The result bytes endianness ['big', 'little']. + :return: Value in bytes. + """ + if isinstance(value, bytes): + return value + + if isinstance(value, bytearray): + return bytes(value) + + value = value_to_int(value) + return value.to_bytes( + get_bytes_cnt_of_int(value, align_to_2n, byte_cnt=byte_cnt), endianness.value + ) + + +def value_to_bool(value: Union[bool, int, str]) -> bool: + """Function decode bool value from various formats. + + :param value: Input value. + :return: Boolean value. + :raises SPSDKError: Unsupported input type. + """ + if isinstance(value, bool): + return value + + if isinstance(value, int): + return bool(value) + + if isinstance(value, str): + return value in ("True", "T", "1") + + raise SPSDKError(f"Invalid input Boolean type({type(value)}) with value ({value})") + + +def load_hex_string( + source: Optional[Union[str, int, bytes]], + expected_size: int, + search_paths: Optional[List[str]] = None, +) -> bytes: + """Get the HEX string from the command line parameter (Keys, digests, etc). + + :param source: File path to key file or hexadecimal value. If not specified random value is used. + :param expected_size: Expected size of key in bytes. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid key + :return: Key in bytes. + """ + if not source: + logger.warning( + f"The key source is not specified, the random value is used in size of {expected_size} B." + ) + return random_bytes(expected_size) + + key = None + assert expected_size > 0, "Invalid expected size of key" + if isinstance(source, (bytes, int)): + return value_to_bytes(source, byte_cnt=expected_size) + + try: + file_path = find_file(source, search_paths=search_paths) + try: + str_key = load_file(file_path) + assert isinstance(str_key, str) + if not str_key.startswith(("0x", "0X")): + str_key = "0x" + str_key + key = value_to_bytes(str_key, byte_cnt=expected_size) + if len(key) != expected_size: + raise SPSDKError("Invalid Key size.") + except (SPSDKError, UnicodeDecodeError): + key = load_binary(file_path) + except Exception: + try: + if not source.startswith(("0x", "0X")): + source = "0x" + source + key = value_to_bytes(source, byte_cnt=expected_size) + except SPSDKError: + pass + + if key is None or len(key) != expected_size: + raise SPSDKError(f"Invalid key input: {source}") + + return key + + +def reverse_bytes_in_longs(arr: bytes) -> bytes: + """The function reverse byte order in longs from input bytes. + + :param arr: Input array. + :return: New array with reversed bytes. + :raises SPSDKError: Raises when invalid value is in input. + """ + arr_len = len(arr) + if arr_len % 4 != 0: + raise SPSDKError("The input array is not in modulo 4!") + + result = bytearray() + + for x in range(0, arr_len, 4): + word = bytearray(arr[x : x + 4]) + word.reverse() + result.extend(word) + return bytes(result) + + +def reverse_bits_in_bytes(arr: bytes) -> bytes: + """The function reverse bits order in input bytes. + + :param arr: Input array. + :return: New array with reversed bits in bytes. + :raises SPSDKError: Raises when invalid value is in input. + """ + result = bytearray() + + for x in arr: + result.append(int(f"{x:08b}"[::-1], 2)) + + return bytes(result) + + +def change_endianness(bin_data: bytes) -> bytes: + """Convert binary format used in files to binary used in register object. + + :param bin_data: input binary array. + :return: Converted array (practically little to big endianness). + :raises SPSDKError: Invalid value on input. + """ + data = bytearray(bin_data) + length = len(data) + if length == 1: + return data + + if length == 2: + data.reverse() + return data + + # The length of 24 bits is not supported yet + if length == 3: + raise SPSDKError("Unsupported length (3) for change endianness.") + + return reverse_bytes_in_longs(data) + + +class Timeout: + """Simple timeout handle class.""" + + UNITS = { + "s": 1000000, + "ms": 1000, + "us": 1, + } + + def __init__(self, timeout: int, units: str = "s") -> None: + """Simple timeout class constructor. + + :param timeout: Timeout value. + :param units: Timeout units (MUST be from the UNITS list) + :raises SPSDKValueError: Invalid input value. + """ + if units not in self.UNITS: + raise SPSDKValueError("Units are not in supported units.") + self.enabled = timeout != 0 + self.timeout_us = timeout * self.UNITS[units] + self.start_time_us = self._get_current_time_us() + self.end_time = self.start_time_us + self.timeout_us + self.units = units + + @staticmethod + def _get_current_time_us() -> int: + """Returns current system time in microseconds. + + :return: Current time in microseconds + """ + return ceil(time.time() * 1_000_000) + + def _convert_to_units(self, time_us: int) -> int: + """Converts time in us into used units. + + :param time_us: Time in micro seconds. + :return: Time in user units. + """ + return time_us // self.UNITS[self.units] + + def get_consumed_time(self) -> int: + """Returns consumed time since start of timeout operation. + + :return: Consumed time in units as the class was constructed + """ + return self._convert_to_units(self._get_current_time_us() - self.start_time_us) + + def get_consumed_time_ms(self) -> int: + """Returns consumed time since start of timeouted operation in milliseconds. + + :return: Consumed time in milliseconds + """ + return (self._get_current_time_us() - self.start_time_us) // 1000 + + def get_rest_time(self, raise_exc: bool = False) -> int: + """Returns rest time to timeout overflow. + + :param raise_exc: If set, the function raise SPSDKTimeoutError in case of overflow. + :return: Rest time in units as the class was constructed + :raises SPSDKTimeoutError: In case of overflow + """ + if self.enabled and self._get_current_time_us() > self.end_time and raise_exc: + raise SPSDKTimeoutError("Timeout of operation.") + + return ( + self._convert_to_units(self.end_time - self._get_current_time_us()) + if self.enabled + else 0 + ) + + def get_rest_time_ms(self, raise_exc: bool = False) -> int: + """Returns rest time to timeout overflow. + + :param raise_exc: If set, the function raise SPSDKTimeoutError in case of overflow. + :return: Rest time in milliseconds + :raises SPSDKTimeoutError: In case of overflow + """ + if self.enabled and self._get_current_time_us() > self.end_time and raise_exc: + raise SPSDKTimeoutError("Timeout of operation.") + + # pylint: disable=superfluous-parens # because PEP20: Readability counts + return ((self.end_time - self._get_current_time_us()) // 1000) if self.enabled else 0 + + def overflow(self, raise_exc: bool = False) -> bool: + """Check the the timer has been overflowed. + + :param raise_exc: If set, the function raise SPSDKTimeoutError in case of overflow. + :return: True if timeout overflowed, False otherwise. + :raises SPSDKTimeoutError: In case of overflow + """ + overflow = self.enabled and self._get_current_time_us() > self.end_time + if overflow and raise_exc: + raise SPSDKTimeoutError("Timeout of operation.") + return overflow + + +def size_fmt(num: Union[float, int], use_kibibyte: bool = True) -> str: + """Size format.""" + base, suffix = [(1000.0, "B"), (1024.0, "iB")][use_kibibyte] + i = "B" + for i in ["B"] + [i + suffix for i in list("kMGTP")]: + if num < base: + break + num /= base + + return f"{int(num)} {i}" if i == "B" else f"{num:3.1f} {i}" + + +def numberify_version(version: str, separator: str = ".", valid_numbers: int = 3) -> int: + """Turn version string into a number. + + Each group is weighted by a multiple of 1000 + + 1.2.3 -> 1 * 1_000_000 + 2 * 1_000 + 3 * 1 = 1_002_003 + 21.100.9 -> 21 * 1_000_000 + 100 * 1_000 + 9 * 1 = 21_100_009 + + :param version: Version string numbers separated by `separator` + :param separator: Separator used in the version string, defaults to "." + :param valid_numbers: Amount of numbers to sanitize to consider, defaults to 3 + :return: Number representing the version + """ + sanitized_version = sanitize_version( + version=version, separator=separator, valid_numbers=valid_numbers + ) + return int( + sum( + int(number) * math.pow(10, 3 * order) + for order, number in enumerate(reversed(sanitized_version.split(separator))) + ) + ) + + +def sanitize_version(version: str, separator: str = ".", valid_numbers: int = 3) -> str: + """Sanitize version string. + + Append '.0' in case version string has fewer parts than `valid_numbers` + Remove right-most version parts after `valid_numbers` amount of parts + + 1.2 -> 1.2.0 + 1.2.3.4 -> 1.2.3 + + :param version: Original version string + :param separator: Separator used in the version string, defaults to "." + :param valid_numbers: Amount of numbers to sanitize, defaults to 3 + :return: Sanitized version string + """ + version_parts = version.split(separator) + version_parts += ["0"] * (valid_numbers - len(version_parts)) + return separator.join(version_parts[:valid_numbers]) + + +def get_key_by_val(value: str, dictionary: Dict[str, List[str]]) -> str: + """Return key by its value. + + :param value: Value to find. + :param dictionary: Dictionary to find in. + :raises SPSDKValueError: Value is not present in dictionary. + :return: Key name + """ + for key, item in dictionary.items(): + if value.lower() in [x.lower() for x in item]: + return key + + raise SPSDKValueError(f"Value {value} is not in {dictionary}.") + + +def swap16(x: int) -> int: + """Swap bytes in half word (16bit). + + :param x: Original number + :return: Number with swapped bytes + :raises SPSDKError: When incorrect number to be swapped is provided + """ + if x < 0 or x > 0xFFFF: + raise SPSDKError("Incorrect number to be swapped") + return ((x << 8) & 0xFF00) | ((x >> 8) & 0x00FF) + + +def swap32(x: int) -> int: + """Swap 32 bit integer. + + :param x: integer to be swapped + :return: swapped value + :raises SPSDKError: When incorrect number to be swapped is provided + """ + if x < 0 or x > 0xFFFFFFFF: + raise SPSDKError("Incorrect number to be swapped") + return unpack("I", x))[0] + + +def check_range(x: int, start: int = 0, end: int = (1 << 32) - 1) -> bool: + """Check if the number is in range. + + :param x: Number to check. + :param start: Lower border of range, default is 0. + :param end: Upper border of range, default is unsigned 32-bit range. + :return: True if fits, False otherwise. + """ + if start > x > end: + return False + + return True + + +def load_configuration(path: str, search_paths: Optional[List[str]] = None) -> Dict: + """Load configuration from yml/json file. + + :param path: Path to configuration file + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: When unsupported file is provided + :return: Content of configuration as dictionary + """ + try: + config = load_text(path, search_paths=search_paths) + except Exception as exc: + raise SPSDKError(f"Can't load configuration file: {str(exc)}") from exc + + try: + return json.loads(config) + except json.JSONDecodeError: + # import YAML only if needed to save startup time + from yaml import YAMLError, safe_load # pylint: disable=import-outside-toplevel + + try: + return safe_load(config) + except (YAMLError, UnicodeDecodeError): + pass + + raise SPSDKError(f"Unable to load '{path}'.") + + +def split_data(data: Union[bytearray, bytes], size: int) -> Generator[bytes, None, None]: + """Split data into chunks of size. + + :param bytearray data: array of bytes to be split + :param int size: size of splitted array + :return Generator[bytes]: splitted array + """ + for i in range(0, len(data), size): + yield data[i : i + size] + + +def get_hash(text: Union[str, bytes]) -> str: + """Returns hash of given text.""" + if isinstance(text, str): + text = text.encode("utf-8") + return hashlib.sha1(text).digest().hex()[:8] + + +def deep_update(d: Dict, u: Dict) -> Dict: + """Deep update nested dictionaries. + + :param d: Dictionary that will be updated + :param u: Dictionary with update information + :returns: Updated dictionary. + """ + for k, v in u.items(): + if isinstance(v, dict): + d[k] = deep_update(d.get(k, {}), v) + else: + d[k] = v + return d + + +def wrap_text(text: str, max_line: int = 100) -> str: + """Wrap text in SPSDK standard. + + Count with new lines in input string and do wrapping after that. + + :param text: Text to wrap + :param max_line: Max line in output, defaults to 100 + :return: Wrapped text (added new lines characters on right places) + """ + lines = text.splitlines() + return "\n".join([textwrap.fill(text=line, width=max_line) for line in lines]) + + +TS = TypeVar("TS", bound="SingletonMeta") # pylint: disable=invalid-name + + +class SingletonMeta(type): + """Singleton metaclass.""" + + _instance = None + + def __call__(cls: Type[TS], *args: Any, **kwargs: Any) -> TS: # type: ignore + """Call dunder override.""" + if cls._instance is None: + instance = super().__call__(*args, **kwargs) + cls._instance = instance + return cls._instance diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py new file mode 100644 index 00000000..16fcb29f --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""SPSDK plugins manager.""" + +import logging +import os +import sys +from importlib.machinery import ModuleSpec +from importlib.util import find_spec, module_from_spec, spec_from_file_location +from types import ModuleType +from typing import Dict, List, Optional + +import importlib_metadata + +from spsdk.exceptions import SPSDKError, SPSDKTypeError +from spsdk.utils.misc import SingletonMeta +from spsdk.utils.spsdk_enum import SpsdkEnum + +logger = logging.getLogger(__name__) + + +class PluginType(SpsdkEnum): + """Contains commands tags.""" + + SIGNATURE_PROVIDER = (0, "spsdk.sp", "Signature provider") + DEVICE_INTERFACE = (1, "spsdk.device.interface", "Device interface") + DEBUG_PROBE = (2, "spsdk.debug_probe", "Debug Probe") + WPC_SERVICE = (3, "spsdk.wpc.service", "WPC Service") + + +class PluginsManager(metaclass=SingletonMeta): + """Plugin manager.""" + + def __init__(self) -> None: + """Plugin manager constructor.""" + self.plugins: Dict[str, ModuleType] = {} + + def load_from_entrypoints(self, group_name: Optional[str] = None) -> int: + """Load modules from given setuptools group. + + :param group_name: Entry point group to load plugins + + :return: The number of loaded plugins. + """ + if group_name is not None and not isinstance(group_name, str): + raise SPSDKTypeError("Group name must be of string type.") + group_names = ( + [group_name] + if group_name is not None + else [PluginType.get_label(tag) for tag in PluginType.tags()] + ) + + entry_points: List[importlib_metadata.EntryPoint] = [] + for group_name in group_names: + eps = importlib_metadata.entry_points(group=group_name) + entry_points.extend(eps) + + count = 0 + for ep in entry_points: + try: + plugin = ep.load() + except (ModuleNotFoundError, ImportError) as exc: + logger.warning(f"Module {ep.module} could not be loaded: {exc}") + continue + logger.info(f"Plugin {ep.name} has been loaded.") + self.register(plugin) + count += 1 + return count + + def load_from_source_file(self, source_file: str, module_name: Optional[str] = None) -> None: + """Import Python source file directly. + + :param source_file: Path to python source file: absolute or relative to cwd + :param module_name: Name for the new module, default is basename of the source file + :raises SPSDKError: If importing of source file failed + """ + name = module_name or os.path.splitext(os.path.basename(source_file))[0] + spec = spec_from_file_location(name=name, location=source_file) + if not spec: + raise SPSDKError( + f"Source '{source_file}' does not exist. Check if it is valid file path name" + ) + + module = self._import_module_spec(spec) + self.register(module) + + def load_from_module_name(self, module_name: str) -> None: + """Import Python module directly. + + :param module_name: Module name to be imported + :raises SPSDKError: If importing of source file failed + """ + spec = find_spec(name=module_name) + if not spec: + raise SPSDKError( + f"Source '{module_name}' does not exist.Check if it is valid file module name" + ) + module = self._import_module_spec(spec) + self.register(module) + + def _import_module_spec(self, spec: ModuleSpec) -> ModuleType: + """Import module from module specification. + + :param spec: Module specification + :return: Imported module type + """ + module = module_from_spec(spec) + try: + sys.modules[spec.name] = module + spec.loader.exec_module(module) # type: ignore + logger.debug(f"A module spec {spec.name} has been loaded.") + except Exception as e: + raise SPSDKError(f"Failed to load module spec {spec.name}: {e}") from e + return module + + def register(self, plugin: ModuleType) -> None: + """Register a plugin with the given name. + + :param plugin: Plugin as a module + """ + plugin_name = self.get_plugin_name(plugin) + if plugin_name in self.plugins: + logger.debug(f"Plugin {plugin_name} has been already registered.") + return + self.plugins[plugin_name] = plugin + logger.debug(f"A plugin {plugin_name} has been registered.") + + def get_plugin(self, name: str) -> Optional[ModuleType]: + """Return a plugin for the given name. + + :param name: Plugin name + :return: Plugin or None if plugin with name is not registered + """ + return self.plugins.get(name) + + def get_plugin_name(self, plugin: ModuleType) -> str: + """Get canonical name of plugin. + + :param plugin: Plugin as a module + :return: String with plugin name + """ + name = getattr(plugin, "__name__", None) + if name is None: + raise SPSDKError("Plugin name could not be determined.") + return name + + +def load_plugin_from_source(source: str, name: Optional[str] = None) -> None: + """Load plugin from source. + + :param source: The source to be loaded + Accepted values: + - Path to source file + - Existing module name + - Existing entrypoint + :param name: Name for the new module/plugin + """ + manager = PluginsManager() + if name and name in manager.plugins: + logger.debug(f"Plugin {name} has been already registered.") + return + try: + return manager.load_from_source_file(source) + except SPSDKError: + pass + try: + manager.load_from_module_name(source) + return + except SPSDKError: + pass + try: + manager.load_from_entrypoints(source) + return + except SPSDKError: + pass + raise SPSDKError(f"Unable to load from source '{source}'.") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py new file mode 100644 index 00000000..2391e744 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py @@ -0,0 +1,1323 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module to handle registers descriptions with support for XML files.""" + +import logging +import re +import xml.etree.ElementTree as ET +from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union +from xml.dom import minidom + +from spsdk.exceptions import SPSDKError, SPSDKValueError +from spsdk.utils.exceptions import ( + SPSDKRegsError, + SPSDKRegsErrorBitfieldNotFound, + SPSDKRegsErrorEnumNotFound, + SPSDKRegsErrorRegisterGroupMishmash, + SPSDKRegsErrorRegisterNotFound, +) +from spsdk.utils.images import BinaryImage, BinaryPattern +from spsdk.utils.misc import ( + Endianness, + format_value, + get_bytes_cnt_of_int, + value_to_bool, + value_to_bytes, + value_to_int, + write_file, +) + +HTMLDataElement = Mapping[str, Union[str, dict, list]] +HTMLData = List[HTMLDataElement] + +logger = logging.getLogger(__name__) + + +class RegsEnum: + """Storage for register enumerations.""" + + def __init__(self, name: str, value: Any, description: str, max_width: int = 0) -> None: + """Constructor of RegsEnum class. Used to store enumeration information of bitfield. + + :param name: Name of enumeration. + :param value: Value of enumeration. + :param description: Text description of enumeration. + :param max_width: Maximal width of enum value used to format output + :raises SPSDKRegsError: Invalid input value. + """ + self.name = name or "N/A" + try: + self.value = value_to_int(value) + except (TypeError, ValueError, SPSDKError) as exc: + raise SPSDKRegsError(f"Invalid Enum Value: {value}") from exc + self.description = description or "N/A" + self.max_width = max_width + + @classmethod + def from_xml_element(cls, xml_element: ET.Element, maxwidth: int = 0) -> "RegsEnum": + """Initialization Enum by XML ET element. + + :param xml_element: Input XML subelement with enumeration data. + :param maxwidth: The maximal width of bitfield for this enum (used for formatting). + :return: The instance of this class. + :raises SPSDKRegsError: Error during enum XML parsing. + """ + name = xml_element.attrib.get("name", "N/A") + if "value" not in xml_element.attrib: + raise SPSDKRegsError(f"Missing Enum Value Key for {name}.") + + raw_val = xml_element.attrib["value"] + try: + value = value_to_int(raw_val) + except (TypeError, ValueError, SPSDKError) as exc: + raise SPSDKRegsError(f"Invalid Enum Value: {raw_val}") from exc + + description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + + return cls(name, value, description, maxwidth) + + def get_value_int(self) -> int: + """Method returns Integer value of enum. + + :return: Integer value of Enum. + """ + return self.value + + def get_value_str(self) -> str: + """Method returns formatted value. + + :return: Formatted string with enum value. + """ + return format_value(self.value, self.max_width) + + def add_et_subelement(self, parent: ET.Element) -> None: + """Creates the register XML structure in ElementTree. + + :param parent: The parent object of ElementTree. + """ + element = ET.SubElement(parent, "bit_field_value") + element.set("name", self.name) + element.set("value", self.get_value_str()) + element.set("description", self.description) + + def __str__(self) -> str: + """Overrides 'ToString()' to print register. + + :return: Friendly string with enum information. + """ + output = "" + output += f"Name: {self.name}\n" + output += f"Value: {self.get_value_str()}\n" + output += f"Description: {self.description}\n" + + return output + + +class ConfigProcessor: + """Base class for processing configuration data.""" + + NAME = "NOP" + + def __init__(self, description: str = "") -> None: + """Initialize the processor.""" + self.description = description + + def pre_process(self, value: int) -> int: + """Pre-process value coming from config file.""" + return value + + def post_process(self, value: int) -> int: + """Post-process value going to config file.""" + return value + + def width_update(self, value: int) -> int: + """Update bit-width of value going to config file.""" + return value + + @classmethod + def get_method_name(cls, config_string: str) -> str: + """Return config processor method name.""" + return config_string.split(":")[0] + + @classmethod + def get_params(cls, config_string: str) -> Dict[str, int]: + """Return config processor method parameters.""" + + def split_params(param: str) -> Tuple[str, str]: + """Split key=value pair into a tuple.""" + parts = param.split("=") + if len(parts) != 2: + raise SPSDKRegsError( + f"Invalid param setting: '{param}'. Expected format '='" + ) + return (parts[0], parts[1]) + + parts = config_string.split(";", maxsplit=1)[0].split(":") + if len(parts) == 1: + return {} + params = parts[1].split(",") + params_dict: Dict[str, str] = dict(split_params(p) for p in params) + return {key.lower(): value_to_int(value) for key, value in params_dict.items()} + + @classmethod + def get_description(cls, config_string: str) -> str: + """Return extra description for config processor.""" + parts = config_string.partition(";") + return parts[2].replace("DESC=", "") + + @classmethod + def from_str(cls, config_string: str) -> "ConfigProcessor": + """Create config processor instance from configuration string.""" + return cls(config_string) + + @classmethod + def from_xml(cls, element: ET.Element) -> Optional["ConfigProcessor"]: + """Create config processor from XML data entry.""" + processor_node = element.find("alias[@type='CONFIG_PREPROCESS']") + if processor_node is None: + return None + if "value" not in processor_node.attrib: + raise SPSDKRegsError("CONFIG_PREPROCESS alias node doesn't have a value") + config_string = processor_node.attrib["value"] + method_name = cls.get_method_name(config_string=config_string) + for klass in cls.__subclasses__(): + if klass.NAME == method_name: + return klass.from_str(config_string=config_string) + return None + + +class ShiftRightConfigProcessor(ConfigProcessor): + """Config processor performing the right-shift operation.""" + + NAME = "SHIFT_RIGHT" + + def __init__(self, count: int, description: str = "") -> None: + """Initialize the right-shift config processor. + + :param count: Count of bit for shift operation + :param description: Extra description for config processor, defaults to "" + """ + super().__init__( + description=description or f"Actual binary value is shifted by {count} bits to right." + ) + self.count = count + + def pre_process(self, value: int) -> int: + """Pre-process value coming from config file.""" + return value >> self.count + + def post_process(self, value: int) -> int: + """Post-process value going to config file.""" + return value << self.count + + def width_update(self, value: int) -> int: + """Update bit-width of value going to config file.""" + return value + self.count + + @classmethod + def from_str(cls, config_string: str) -> "ShiftRightConfigProcessor": + """Create config processor instance from configuration string.""" + name = cls.get_method_name(config_string=config_string) + if name != cls.NAME: + raise SPSDKRegsError(f"Invalid method name '{name}' expected {cls.NAME}") + params = cls.get_params(config_string=config_string) + if "count" not in params: + raise SPSDKRegsError(f"{cls.NAME} requires the COUNT parameter") + description = cls.get_description(config_string=config_string) + return cls(count=value_to_int(params["count"]), description=description) + + +class RegsBitField: + """Storage for register bitfields.""" + + def __init__( + self, + parent: "RegsRegister", + name: str, + offset: int, + width: int, + description: Optional[str] = None, + reset_val: Any = "0", + access: str = "RW", + hidden: bool = False, + config_processor: Optional[ConfigProcessor] = None, + ) -> None: + """Constructor of RegsBitField class. Used to store bitfield information. + + :param parent: Parent register of bitfield. + :param name: Name of bitfield. + :param offset: Bit offset of bitfield. + :param width: Bit width of bitfield. + :param description: Text description of bitfield. + :param reset_val: Reset value of bitfield. + :param access: Access type of bitfield. + :param hidden: The bitfield will be hidden from standard searches. + """ + self.parent = parent + self.name = name or "N/A" + self.offset = offset + self.width = width + self.description = description or "N/A" + self.reset_value = value_to_int(reset_val, 0) + self.access = access + self.hidden = hidden + self._enums: List[RegsEnum] = [] + self.config_processor = config_processor or ConfigProcessor() + self.config_width = self.config_processor.width_update(width) + self.set_value(self.reset_value, raw=True) + + @classmethod + def from_xml_element(cls, xml_element: ET.Element, parent: "RegsRegister") -> "RegsBitField": + """Initialization register by XML ET element. + + :param xml_element: Input XML subelement with register data. + :param parent: Reference to parent RegsRegister object. + :return: The instance of this class. + """ + name = xml_element.attrib.get("name", "N/A") + offset = value_to_int(xml_element.attrib.get("offset", 0)) + width = value_to_int(xml_element.attrib.get("width", 0)) + description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + access = xml_element.attrib.get("access", "R/W") + reset_value = value_to_int(xml_element.attrib.get("reset_value", 0)) + hidden = xml_element.tag != "bit_field" + config_processor = ConfigProcessor.from_xml(xml_element) + + bitfield = cls( + parent, name, offset, width, description, reset_value, access, hidden, config_processor + ) + + for xml_enum in xml_element.findall("bit_field_value"): + bitfield.add_enum(RegsEnum.from_xml_element(xml_enum, width)) + + return bitfield + + def has_enums(self) -> bool: + """Returns if the bitfields has enums. + + :return: True is has enums, False otherwise. + """ + return len(self._enums) > 0 + + def get_enums(self) -> List[RegsEnum]: + """Returns bitfield enums. + + :return: List of bitfield enumeration values. + """ + return self._enums + + def add_enum(self, enum: RegsEnum) -> None: + """Add bitfield enum. + + :param enum: New enumeration value for bitfield. + """ + self._enums.append(enum) + + def get_value(self) -> int: + """Returns integer value of the bitfield. + + :return: Current value of bitfield. + """ + reg_val = self.parent.get_value(raw=False) + value = reg_val >> self.offset + mask = (1 << self.width) - 1 + value = value & mask + value = self.config_processor.post_process(value) + return value + + def get_reset_value(self) -> int: + """Returns integer reset value of the bitfield. + + :return: Reset value of bitfield. + """ + return self.reset_value + + def set_value(self, new_val: Any, raw: bool = False) -> None: + """Updates the value of the bitfield. + + :param new_val: New value of bitfield. + :param raw: If set, no automatic modification of value is applied. + :raises SPSDKValueError: The input value is out of range. + """ + new_val_int = value_to_int(new_val) + new_val_int = self.config_processor.pre_process(new_val_int) + if new_val_int > 1 << self.width: + raise SPSDKValueError("The input value is out of bitfield range") + reg_val = self.parent.get_value(raw=raw) + + mask = ((1 << self.width) - 1) << self.offset + reg_val = reg_val & ~mask + value = (new_val_int << self.offset) & mask + reg_val = reg_val | value + self.parent.set_value(reg_val, raw) + + def set_enum_value(self, new_val: str, raw: bool = False) -> None: + """Updates the value of the bitfield by its enum value. + + :param new_val: New enum value of bitfield. + :param raw: If set, no automatic modification of value is applied. + :raises SPSDKRegsErrorEnumNotFound: Input value cannot be decoded. + """ + try: + val_int = self.get_enum_constant(new_val) + except SPSDKRegsErrorEnumNotFound: + # Try to decode standard input + try: + val_int = value_to_int(new_val) + except TypeError: + raise SPSDKRegsErrorEnumNotFound # pylint: disable=raise-missing-from + self.set_value(val_int, raw) + + def get_enum_value(self) -> Union[str, int]: + """Returns enum value of the bitfield. + + :return: Current value of bitfield. + """ + value = self.get_value() + for enum in self._enums: + if enum.get_value_int() == value: + return enum.name + # return value + return self.get_hex_value() + + def get_hex_value(self) -> str: + """Get the value of register in string hex format. + + :return: Hexadecimal value of register. + """ + fmt = f"0{self.config_width // 4}X" + val = f"0x{format(self.get_value(), fmt)}" + return val + + def get_enum_constant(self, enum_name: str) -> int: + """Returns constant representation of enum by its name. + + :return: Constant of enum. + :raises SPSDKRegsErrorEnumNotFound: The enum has not been found. + """ + for enum in self._enums: + if enum.name == enum_name: + return enum.get_value_int() + + raise SPSDKRegsErrorEnumNotFound(f"The enum for {enum_name} has not been found.") + + def get_enum_names(self) -> List[str]: + """Returns list of the enum strings. + + :return: List of enum names. + """ + return [x.name for x in self._enums] + + def add_et_subelement(self, parent: ET.Element) -> None: + """Creates the register XML structure in ElementTree. + + :param parent: The parent object of ElementTree. + """ + element = ET.SubElement(parent, "reserved_bit_field" if self.hidden else "bit_field") + element.set("offset", hex(self.offset)) + element.set("width", str(self.width)) + element.set("name", self.name) + element.set("access", self.access) + element.set("reset_value", format_value(self.reset_value, self.width)) + element.set("description", self.description) + for enum in self._enums: + enum.add_et_subelement(element) + + def __str__(self) -> str: + """Override 'ToString()' to print register. + + :return: Friendly looking string that describes the bitfield. + """ + output = "" + output += f"Name: {self.name}\n" + output += f"Offset: {self.offset} bits\n" + output += f"Width: {self.width} bits\n" + output += f"Access: {self.access} bits\n" + output += f"Reset val:{self.reset_value}\n" + output += f"Description: \n {self.description}\n" + if self.hidden: + output += "This is hidden bitfield!\n" + + i = 0 + for enum in self._enums: + output += f"Enum #{i}: \n" + str(enum) + i += 1 + + return output + + +class RegsRegister: + """Initialization register by input information.""" + + def __init__( + self, + name: str, + offset: int, + width: int, + description: Optional[str] = None, + reverse: bool = False, + access: Optional[str] = None, + config_as_hexstring: bool = False, + otp_index: Optional[int] = None, + reverse_subregs_order: bool = False, + base_endianness: Endianness = Endianness.BIG, + alt_widths: Optional[List[int]] = None, + ) -> None: + """Constructor of RegsRegister class. Used to store register information. + + :param name: Name of register. + :param offset: Byte offset of register. + :param width: Bit width of register. + :param description: Text description of register. + :param reverse: Multi byte register value could be printed in reverse order. + :param access: Access type of register. + :param config_as_hexstring: Config is stored as a hex string. + :param otp_index: Index of OTP fuse. + :param reverse_subregs_order: Reverse order of sub registers. + :param base_endianness: Base endianness for bytes import/export of value. + :param alt_widths: List of alternative widths. + """ + if width % 8 != 0: + raise SPSDKValueError("SPSDK Register supports only widths in multiply 8 bits.") + self.name = name + self.offset = offset + self.width = width + self.description = description or "N/A" + self.access = access or "RW" + self.reverse = reverse + self._bitfields: List[RegsBitField] = [] + self._set_value_hooks: List = [] + self._value = 0 + self._reset_value = 0 + self.config_as_hexstring = config_as_hexstring + self.otp_index = otp_index + self.reverse_subregs_order = reverse_subregs_order + self.base_endianness = base_endianness + self.alt_widths = alt_widths + self._alias_names: List[str] = [] + + # Grouped register members + self.sub_regs: List["RegsRegister"] = [] + self._sub_regs_width_init = False + self._sub_regs_width = 0 + + def __eq__(self, obj: Any) -> bool: + """Compare if the objects has same settings.""" + if not isinstance(obj, self.__class__): + return False + if obj.name != self.name: + return False + if obj.width != self.width: + return False + if obj.reverse != self.reverse: + return False + if obj._value != self._value: + return False + if obj._reset_value != self._reset_value: + return False + return True + + @classmethod + def from_xml_element(cls, xml_element: ET.Element) -> "RegsRegister": + """Initialization register by XML ET element. + + :param xml_element: Input XML subelement with register data. + :return: The instance of this class. + """ + name = xml_element.attrib.get("name", "N/A") + offset = value_to_int(xml_element.attrib.get("offset", 0)) + width = value_to_int(xml_element.attrib.get("width", 0)) + description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + reverse = (xml_element.attrib.get("reversed", "False")) == "True" + access = xml_element.attrib.get("access", "N/A") + otp_index_raw = xml_element.attrib.get("otp_index") + otp_index = None + if otp_index_raw: + otp_index = value_to_int(otp_index_raw) + reg = cls( + name, + offset, + width, + description, + reverse, + access, + otp_index=otp_index, + ) + value = xml_element.attrib.get("value") + if value: + reg.set_value(value) + + if xml_element.text: + xml_bitfields = xml_element.findall("bit_field") + xml_bitfields.extend(xml_element.findall("reserved_bit_field")) + xml_bitfields_len = len(xml_bitfields) + for xml_bitfield in xml_bitfields: + bitfield = RegsBitField.from_xml_element(xml_bitfield, reg) + if ( + xml_bitfields_len == 1 + and bitfield.width == reg.width + and not bitfield.has_enums() + ): + if len(reg.description) < len(bitfield.description): + reg.description = bitfield.description + reg.access = bitfield.access + reg._reset_value = bitfield.reset_value + else: + if reg.access == "N/A": + reg.access = "Bitfields depended" + reg.add_bitfield(bitfield) + return reg + + def add_alias(self, alias: str) -> None: + """Add alias name to register. + + :param alias: Register name alias. + """ + if not alias in self._alias_names: + self._alias_names.append(alias) + + def has_group_registers(self) -> bool: + """Returns true if register is compounded from sub-registers. + + :return: True if register has sub-registers, False otherwise. + """ + return len(self.sub_regs) > 0 + + def add_group_reg(self, reg: "RegsRegister") -> None: + """Add group element for this register. + + :param reg: Register member of this register group. + :raises SPSDKRegsErrorRegisterGroupMishmash: When any inconsistency is detected. + """ + first_member = not self.has_group_registers() + if first_member: + if self.offset == 0: + self.offset = reg.offset + if self.width == 0: + self.width = reg.width + else: + self._sub_regs_width_init = True + self._sub_regs_width = reg.width + if self.access == "RW": + self.access = reg.access + else: + # There is strong rule that supported group MUST be in one row in memory! + if not self._sub_regs_width_init: + if self.offset + self.width // 8 != reg.offset: + raise SPSDKRegsErrorRegisterGroupMishmash( + f"The register {reg.name} doesn't follow the previous one." + ) + self.width += reg.width + else: + if self.offset + self.width // 8 <= reg.offset: + raise SPSDKRegsErrorRegisterGroupMishmash( + f"The register {reg.name} doesn't follow the previous one." + ) + self._sub_regs_width += reg.width + if self._sub_regs_width > self.width: + raise SPSDKRegsErrorRegisterGroupMishmash( + f"The register {reg.name} bigger width than is defined." + ) + if self.sub_regs[0].width != reg.width: + raise SPSDKRegsErrorRegisterGroupMishmash( + f"The register {reg.name} has different width." + ) + if self.access != reg.access: + raise SPSDKRegsErrorRegisterGroupMishmash( + f"The register {reg.name} has different access type." + ) + reg.base_endianness = self.base_endianness + self.sub_regs.append(reg) + + def add_et_subelement(self, parent: ET.Element) -> None: + """Creates the register XML structure in ElementTree. + + :param parent: The parent object of ElementTree. + """ + element = ET.SubElement(parent, "register") + element.set("offset", hex(self.offset)) + element.set("width", str(self.width)) + element.set("name", self.name) + element.set("reversed", str(self.reverse)) + element.set("description", self.description) + if self.otp_index: + element.set("otp_index", str(self.otp_index)) + for bitfield in self._bitfields: + bitfield.add_et_subelement(element) + + def set_value(self, val: Any, raw: bool = False) -> None: + """Set the new value of register. + + :param val: The new value to set. + :param raw: Do not use any modification hooks. + :raises SPSDKError: When invalid values is loaded into register + """ + try: + if isinstance(val, (bytes, bytearray)): + value = int.from_bytes(val, self.base_endianness.value) + else: + value = value_to_int(val) + if value >= 1 << self.width: + raise SPSDKError( + f"Input value {value} doesn't fit into register of width {self.width}." + ) + + alt_width = self.get_alt_width(value) + + if not raw: + for hook in self._set_value_hooks: + value = hook[0](value, hook[1]) + if self.reverse: + # The value_to_int internally is using BIG endian + val_bytes = value_to_bytes( + value, + align_to_2n=False, + byte_cnt=alt_width // 8, + endianness=Endianness.BIG, + ) + value = value.from_bytes(val_bytes, Endianness.LITTLE.value) + + if self.has_group_registers(): + # Update also values in sub registers + subreg_width = self.sub_regs[0].width + sub_regs = self.sub_regs[: alt_width // subreg_width] + for index, sub_reg in enumerate(sub_regs, start=1): + if self.reverse_subregs_order: + bit_pos = alt_width - index * subreg_width + else: + bit_pos = (index - 1) * subreg_width + + sub_reg.set_value((value >> bit_pos) & ((1 << subreg_width) - 1), raw=raw) + else: + self._value = value + + except SPSDKError as exc: + raise SPSDKError(f"Loaded invalid value {str(val)}") from exc + + def reset_value(self, raw: bool = False) -> None: + """Reset the value of register. + + :param raw: Do not use any modification hooks. + """ + self.set_value(self.get_reset_value(), raw) + + def get_alt_width(self, value: int) -> int: + """Get alternative width of register. + + :param value: Input value to recognize width + :return: Current width + """ + alt_width = self.width + if self.alt_widths: + real_byte_cnt = get_bytes_cnt_of_int(value, align_to_2n=False) + self.alt_widths.sort() + for alt in self.alt_widths: + if real_byte_cnt <= alt // 8: + alt_width = alt + break + return alt_width + + def get_value(self, raw: bool = False) -> int: + """Get the value of register. + + :param raw: Do not use any modification hooks. + """ + if self.has_group_registers(): + # Update local value, by the sub register values + subreg_width = self.sub_regs[0].width + sub_regs_value = 0 + for index, sub_reg in enumerate(self.sub_regs, start=1): + if self.reverse_subregs_order: + bit_pos = self.width - index * subreg_width + else: + bit_pos = (index - 1) * subreg_width + sub_regs_value |= sub_reg.get_value(raw=raw) << (bit_pos) + value = sub_regs_value + else: + value = self._value + + alt_width = self.get_alt_width(value) + + if not raw and self.reverse: + val_bytes = value_to_bytes( + value, + align_to_2n=False, + byte_cnt=alt_width // 8, + endianness=self.base_endianness, + ) + value = value.from_bytes( + val_bytes, + Endianness.BIG.value + if self.base_endianness == Endianness.LITTLE + else Endianness.LITTLE.value, + ) + + return value + + def get_bytes_value(self, raw: bool = False) -> bytes: + """Get the bytes value of register. + + :param raw: Do not use any modification hooks. + :return: Register value in bytes. + """ + value = self.get_value(raw=raw) + return value_to_bytes( + value, + align_to_2n=False, + byte_cnt=self.get_alt_width(value) // 8, + endianness=self.base_endianness, + ) + + def get_hex_value(self, raw: bool = False) -> str: + """Get the value of register in string hex format. + + :param raw: Do not use any modification hooks. + :return: Hexadecimal value of register. + """ + val_int = self.get_value(raw=raw) + count = "0" + str(self.get_alt_width(val_int) // 4) + value = f"{val_int:{count}X}" + if not self.config_as_hexstring: + value = "0x" + value + return value + + def get_reset_value(self) -> int: + """Returns reset value of the register. + + :return: Reset value of register. + """ + value = self._reset_value + for bitfield in self._bitfields: + width = bitfield.width + offset = bitfield.offset + val = bitfield.reset_value + value |= (val & ((1 << width) - 1)) << offset + + return value + + def add_bitfield(self, bitfield: RegsBitField) -> None: + """Add register bitfield. + + :param bitfield: New bitfield value for register. + """ + self._bitfields.append(bitfield) + + def get_bitfields(self, exclude: Optional[List[str]] = None) -> List[RegsBitField]: + """Returns register bitfields. + + Method allows exclude some bitfields by their names. + :param exclude: Exclude list of bitfield names if needed. + :return: Returns List of register bitfields. + """ + ret = [] + for bitf in self._bitfields: + if bitf.hidden: + continue + if exclude and bitf.name.startswith(tuple(exclude)): + continue + ret.append(bitf) + return ret + + def get_bitfield_names(self, exclude: Optional[List[str]] = None) -> List[str]: + """Returns list of the bitfield names. + + :param exclude: Exclude list of bitfield names if needed. + :return: List of bitfield names. + """ + return [x.name for x in self.get_bitfields(exclude)] + + def find_bitfield(self, name: str) -> RegsBitField: + """Returns the instance of the bitfield by its name. + + :param name: The name of the bitfield. + :return: Instance of the bitfield. + :raises SPSDKRegsErrorBitfieldNotFound: The bitfield doesn't exist. + """ + for bitfield in self._bitfields: + if name == bitfield.name: + return bitfield + + raise SPSDKRegsErrorBitfieldNotFound(f" The {name} is not found in register {self.name}.") + + def add_setvalue_hook(self, hook: Callable, context: Optional[Any] = None) -> None: + """Set the value hook for write operation. + + :param hook: Callable hook for set value operation. + :param context: Context data for this hook. + """ + self._set_value_hooks.append((hook, context)) + + def __str__(self) -> str: + """Override 'ToString()' to print register. + + :return: Friendly looking string that describes the register. + """ + output = "" + output += f"Name: {self.name}\n" + output += f"Offset: 0x{self.offset:04X}\n" + output += f"Width: {self.width} bits\n" + output += f"Access: {self.access}\n" + output += f"Description: \n {self.description}\n" + if self.otp_index: + output += f"OTP Word: \n {self.otp_index}\n" + + i = 0 + for bitfield in self._bitfields: + output += f"Bitfield #{i}: \n" + str(bitfield) + i += 1 + + return output + + +class Registers: + """SPSDK Class for registers handling.""" + + TEMPLATE_NOTE = ( + "All registers is possible to define also as one value although the bitfields are used. " + "Instead of bitfields: ... field, the value: ... definition works as well." + ) + + def __init__(self, device_name: str, base_endianness: Endianness = Endianness.BIG) -> None: + """Initialization of Registers class.""" + self._registers: List[RegsRegister] = [] + self.dev_name = device_name + self.base_endianness = base_endianness + + def __eq__(self, obj: Any) -> bool: + """Compare if the objects has same settings.""" + if not ( + isinstance(obj, self.__class__) + and obj.dev_name == self.dev_name + and obj.base_endianness == self.base_endianness + ): + return False + ret = obj._registers == self._registers + return ret + + def find_reg(self, name: str, include_group_regs: bool = False) -> RegsRegister: + """Returns the instance of the register by its name. + + :param name: The name of the register. + :param include_group_regs: The algorithm will check also group registers. + :return: Instance of the register. + :raises SPSDKRegsErrorRegisterNotFound: The register doesn't exist. + """ + for reg in self._registers: + if name == reg.name: + return reg + if name in reg._alias_names: + return reg + if include_group_regs and reg.has_group_registers(): + for sub_reg in reg.sub_regs: + if name == sub_reg.name: + return sub_reg + + raise SPSDKRegsErrorRegisterNotFound( + f"The {name} is not found in loaded registers for {self.dev_name} device." + ) + + def add_register(self, reg: RegsRegister) -> None: + """Adds register into register list. + + :param reg: Register to add to the class. + :raises SPSDKError: Invalid type has been provided. + :raises SPSDKRegsError: Cannot add register with same name + """ + if not isinstance(reg, RegsRegister): + raise SPSDKError("The 'reg' has invalid type.") + + if reg.name in self.get_reg_names(): + raise SPSDKRegsError(f"Cannot add register with same name: {reg.name}.") + + for idx, register in enumerate(self._registers): + # TODO solve problem with group register that are always at 0 offset + if register.offset == reg.offset != 0: + logger.debug( + f"Found register at the same offset {hex(reg.offset)}" + f", adding {reg.name} as an alias to {register.name}" + ) + self._registers[idx].add_alias(reg.name) + self._registers[idx]._bitfields.extend(reg._bitfields) + return + # update base endianness for all registers in group + reg.base_endianness = self.base_endianness + self._registers.append(reg) + + def remove_registers(self) -> None: + """Remove all registers.""" + self._registers.clear() + + def get_registers( + self, exclude: Optional[List[str]] = None, include_group_regs: bool = False + ) -> List[RegsRegister]: + """Returns list of the registers. + + Method allows exclude some register by their names. + :param exclude: Exclude list of register names if needed. + :param include_group_regs: The algorithm will check also group registers. + :return: List of register names. + """ + if exclude: + regs = [r for r in self._registers if not r.name.startswith(tuple(exclude))] + else: + regs = self._registers.copy() + if include_group_regs: + sub_regs = [] + for reg in regs: + if reg.has_group_registers(): + sub_regs.extend(reg.sub_regs) + regs.extend(sub_regs) + + return regs + + def get_reg_names( + self, exclude: Optional[List[str]] = None, include_group_regs: bool = False + ) -> List[str]: + """Returns list of the register names. + + :param exclude: Exclude list of register names if needed. + :param include_group_regs: The algorithm will check also group registers. + :return: List of register names. + """ + return [x.name for x in self.get_registers(exclude, include_group_regs)] + + def reset_values(self, exclude: Optional[List[str]] = None) -> None: + """The method reset values in registers. + + :param exclude: The list of register names to be excluded. + """ + for reg in self.get_registers(exclude): + reg.reset_value(True) + + def __str__(self) -> str: + """Override 'ToString()' to print register. + + :return: Friendly looking string that describes the registers. + """ + output = "" + output += "Device name: " + self.dev_name + "\n" + for reg in self._registers: + output += str(reg) + "\n" + + return output + + def write_xml(self, file_name: str) -> None: + """Write loaded register structures into XML file. + + :param file_name: The name of XML file that should be created. + """ + xml_root = ET.Element("regs") + for reg in self._registers: + reg.add_et_subelement(xml_root) + + no_pretty_data = minidom.parseString( + ET.tostring(xml_root, encoding="unicode", short_empty_elements=False) + ) + write_file(no_pretty_data.toprettyxml(), file_name, encoding="utf-8") + + def image_info( + self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros") + ) -> BinaryImage: + """Export Registers into binary information. + + :param size: Result size of Image, 0 means automatic minimal size. + :param pattern: Pattern of gaps, defaults to "zeros" + """ + image = BinaryImage(self.dev_name, size=size, pattern=pattern) + for reg in self._registers: + description = reg.description + if reg._alias_names: + description += f"\n Alias names: {', '.join(reg._alias_names)}" + image.add_image( + BinaryImage( + reg.name, + reg.width // 8, + offset=reg.offset, + description=description, + binary=reg.get_bytes_value(raw=True), + ) + ) + + return image + + def export(self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros")) -> bytes: + """Export Registers into binary. + + :param size: Result size of Image, 0 means automatic minimal size. + :param pattern: Pattern of gaps, defaults to "zeros" + """ + return self.image_info(size, pattern).export() + + def parse(self, binary: bytes) -> None: + """Parse the binary data values into loaded registers. + + :param binary: Binary data to parse. + """ + bin_len = len(binary) + if bin_len < len(self.image_info()): + logger.info( + f"Input binary is smaller than registers supports: {bin_len} != {len(self.image_info())}" + ) + for reg in self.get_registers(): + if bin_len < reg.offset + reg.width // 8: + logger.debug(f"Parsing of binary block ends at {reg.name}") + break + reg.set_value(binary[reg.offset : reg.offset + reg.width // 8], raw=True) + + def _get_bitfield_yaml_description(self, bitfield: RegsBitField) -> str: + """Create the valuable comment for bitfield. + + :param bitfield: Bitfield used to generate description. + :return: Bitfield description. + """ + description = f"Offset: {bitfield.offset}b, Width: {bitfield.config_width}b" + if bitfield.description not in ("", "."): + description += ", " + bitfield.description.replace(" ", "\n") + if bitfield.config_processor.description: + description += ".\n NOTE: " + bitfield.config_processor.description + if bitfield.has_enums(): + for enum in bitfield.get_enums(): + descr = enum.description if enum.description != "." else enum.name + enum_description = descr.replace(" ", "\n") + description += f"\n- {enum.name}, ({enum.get_value_int()}): {enum_description}" + return description + + def get_validation_schema(self) -> Dict: + """Get the JSON SCHEMA for registers. + + :return: JSON SCHEMA. + """ + properties: Dict[str, Any] = {} + for reg in self.get_registers(): + bitfields = reg.get_bitfields() + reg_schema = [ + { + "type": ["string", "number"], + "skip_in_template": len(bitfields) > 0, + # "format": "number", # TODO add option to hexstring + "template_value": f"{reg.get_hex_value()}", + }, + { # Obsolete type + "type": "object", + "required": ["value"], + "skip_in_template": True, + "additionalProperties": False, + "properties": { + "value": { + "type": ["string", "number"], + # "format": "number", # TODO add option to hexstring + "template_value": f"{reg.get_hex_value()}", + } + }, + }, + ] + + if bitfields: + bitfields_schema = {} + for bitfield in bitfields: + if not bitfield.has_enums(): + bitfields_schema[bitfield.name] = { + "type": ["string", "number"], + "title": f"{bitfield.name}", + "description": self._get_bitfield_yaml_description(bitfield), + "template_value": bitfield.get_value(), + } + else: + bitfields_schema[bitfield.name] = { + "type": ["string", "number"], + "title": f"{bitfield.name}", + "description": self._get_bitfield_yaml_description(bitfield), + "enum_template": bitfield.get_enum_names(), + "minimum": 0, + "maximum": (1 << bitfield.width) - 1, + "template_value": bitfield.get_enum_value(), + } + # Extend register schema by obsolete style + reg_schema.append( + { + "type": "object", + "required": ["bitfields"], + "skip_in_template": True, + "additionalProperties": False, + "properties": { + "bitfields": {"type": "object", "properties": bitfields_schema} + }, + } + ) + # Extend by new style of bitfields + reg_schema.append( + { + "type": "object", + "skip_in_template": False, + "required": [], + "additionalProperties": False, + "properties": bitfields_schema, + }, + ) + + properties[reg.name] = { + "title": f"{reg.name}", + "description": f"{reg.description}", + "oneOf": reg_schema, + } + + return {"type": "object", "title": self.dev_name, "properties": properties} + + # pylint: disable=no-self-use #It's better to have this function visually close to callies + def _filter_by_names(self, items: List[ET.Element], names: List[str]) -> List[ET.Element]: + """Filter out all items in the "items" tree,whose name starts with one of the strings in "names" list. + + :param items: Items to be filtered out. + :param names: Names to filter out. + :return: Filtered item elements list. + """ + return [item for item in items if not item.attrib["name"].startswith(tuple(names))] + + # pylint: disable=dangerous-default-value + def load_registers_from_xml( + self, + xml: str, + filter_reg: Optional[List[str]] = None, + grouped_regs: Optional[List[dict]] = None, + ) -> None: + """Function loads the registers from the given XML. + + :param xml: Input XML data in string format. + :param filter_reg: List of register names that should be filtered out. + :param grouped_regs: List of register prefixes names to be grouped into one. + :raises SPSDKRegsError: XML parse problem occurs. + """ + + def is_reg_in_group(reg: str) -> Union[dict, None]: + """Help function to recognize if the register should be part of group.""" + if grouped_regs: + for group in grouped_regs: + # pylint: disable=anomalous-backslash-in-string # \d is a part of the regex pattern + if re.fullmatch(f"{group['name']}" + r"\d+", reg) is not None: + return group + return None + + try: + xml_elements = ET.parse(xml) + except ET.ParseError as exc: + raise SPSDKRegsError(f"Cannot Parse XML data: {str(exc)}") from exc + xml_registers = xml_elements.findall("register") + xml_registers = self._filter_by_names(xml_registers, filter_reg or []) + # Load all registers into the class + for xml_reg in xml_registers: + group = is_reg_in_group(xml_reg.attrib["name"]) + if group: + try: + group_reg = self.find_reg(group["name"]) + except SPSDKRegsErrorRegisterNotFound: + group_reg = RegsRegister( + name=group["name"], + offset=value_to_int(group.get("offset", 0)), + width=value_to_int(group.get("width", 0)), + description=group.get( + "description", f"Group of {group['name']} registers." + ), + reverse=value_to_bool(group.get("reversed", False)), + access=group.get("access", None), + config_as_hexstring=group.get("config_as_hexstring", False), + reverse_subregs_order=group.get("reverse_subregs_order", False), + alt_widths=group.get("alternative_widths"), + ) + + self.add_register(group_reg) + group_reg.add_group_reg(RegsRegister.from_xml_element(xml_reg)) + else: + self.add_register(RegsRegister.from_xml_element(xml_reg)) + + def load_yml_config(self, yml_data: Dict[str, Any]) -> None: + """The function loads the configuration from YML file. + + :param yml_data: The YAML commented data with register values. + """ + for reg_name in yml_data.keys(): + reg_value = yml_data[reg_name] + register = self.find_reg(reg_name, include_group_regs=True) + if isinstance(reg_value, dict): + if "value" in reg_value.keys(): + raw_val = reg_value["value"] + val = ( + int(raw_val, 16) + if register.config_as_hexstring and isinstance(raw_val, str) + else value_to_int(raw_val) + ) + register.set_value(val, False) + else: + bitfields = ( + reg_value["bitfields"] if "bitfields" in reg_value.keys() else reg_value + ) + for bitfield_name in bitfields: + bitfield_val = bitfields[bitfield_name] + try: + bitfield = register.find_bitfield(bitfield_name) + except SPSDKRegsErrorBitfieldNotFound: + logger.error( + f"The {bitfield_name} is not found in register {register.name}." + ) + continue + try: + bitfield.set_enum_value(bitfield_val, True) + except SPSDKValueError as e: + raise SPSDKError( + f"Bitfield value: {hex(bitfield_val)} of {bitfield.name} is out of range." + + f"\nBitfield width is {bitfield.width} bits" + ) from e + except SPSDKError: + # New versions of register data do not contain register and bitfield value in enum + old_bitfield = bitfield_val + bitfield_val = bitfield_val.replace(bitfield.name + "_", "").replace( + register.name + "_", "" + ) + # Some bitfield were renamed from ENABLE to ALLOW + bitfield_val = "ALLOW" if bitfield_val == "ENABLE" else bitfield_val + logger.warning( + f"Bitfield {old_bitfield} not found, trying backward" + " compatibility mode with {bitfield_val}" + ) + bitfield.set_enum_value(bitfield_val, True) + + # Run the processing of loaded register value + register.set_value(register.get_value(True), False) + elif isinstance(reg_value, (int, str)): + val = ( + int(reg_value, 16) + if register.config_as_hexstring and isinstance(reg_value, str) + else value_to_int(reg_value) + ) + register.set_value(val, False) + + else: + logger.error(f"There are no data for {reg_name} register.") + + logger.debug(f"The register {reg_name} has been loaded from configuration.") + + def get_config(self, diff: bool = False) -> Dict[str, Any]: + """Get the whole configuration in dictionary. + + :param diff: Get only configuration with difference value to reset state. + :return: Dictionary of registers values. + """ + ret: Dict[str, Any] = {} + for reg in self.get_registers(): + if diff and reg.get_value(raw=True) == reg.get_reset_value(): + continue + bitfields = reg.get_bitfields() + if bitfields: + btf = {} + for bitfield in bitfields: + if diff and bitfield.get_value() == bitfield.get_reset_value(): + continue + btf[bitfield.name] = bitfield.get_enum_value() + ret[reg.name] = btf + else: + ret[reg.name] = reg.get_hex_value() + + return ret diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py new file mode 100644 index 00000000..8e3900e1 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py @@ -0,0 +1,757 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# Copyright 2021-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for schema-based configuration validation.""" + +import copy +import io +import logging +import os +from collections import OrderedDict +from typing import Any, Callable, Dict, List, Optional, Union + +import fastjsonschema +from deepmerge import Merger, always_merger +from deepmerge.strategy.dict import DictStrategies +from deepmerge.strategy.list import ListStrategies +from deepmerge.strategy.set import SetStrategies +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap as CMap +from ruamel.yaml.comments import CommentedSeq as CSeq + +from spsdk import SPSDK_YML_INDENT +from spsdk.exceptions import SPSDKError +from spsdk.utils.misc import ( + find_dir, + find_file, + load_configuration, + value_to_int, + wrap_text, + write_file, +) +from spsdk.utils.spsdk_enum import SpsdkEnum + +ENABLE_DEBUG = False + +logger = logging.getLogger(__name__) + + +def cmap_update(cmap: CMap, updater: CMap) -> None: + """Update CMap including comments. + + :param cmap: Original CMap to be updated. + :param updater: CMap updater. + """ + cmap.update(updater) + cmap.ca.items.update(updater.ca.items) + + +class PropertyRequired(SpsdkEnum): + """Enum describing if the property is required or optional.""" + + REQUIRED = (0, "REQUIRED", "Required") + CONDITIONALLY_REQUIRED = (1, "CONDITIONALLY_REQUIRED", "Conditionally required") + OPTIONAL = (2, "OPTIONAL", "Optional") + + +class SPSDKListStrategies(ListStrategies): + """Extended List Strategies.""" + + # pylint: disable=unused-argument # because of the base class + @staticmethod + def strategy_set(_config, _path, base, nxt): # type: ignore + """Use the set of both as a output.""" + try: + ret = list(set(base + nxt)) + ret.sort() + except TypeError: + try: + ret = base + nxt + except TypeError: + logger.warning( + "Found unhashable object in List 'set' strategy during merge." + " It was used 'override' method instead of 'set'." + ) + ret = nxt + return ret + + +class SPSDKMerger(Merger): + """Modified Merger to add new list strategy 'set'.""" + + PROVIDED_TYPE_STRATEGIES = { + list: SPSDKListStrategies, + dict: DictStrategies, + set: SetStrategies, + } + + +def _is_number(param: Any) -> bool: + """Checks whether the input represents a number. + + :param param: Input to analyze + :raises SPSDKError: Input doesn't represent a number + :return: True if input represents a number + """ + try: + value_to_int(param) + return True + except SPSDKError: + return False + + +def _is_hex_number(param: Any) -> bool: + """Checks whether the input represents a hexnumber. + + :param param: Input to analyze + :raises SPSDKError: Input doesn't represent a hexnumber + :return: True if input represents a hexnumber + """ + try: + bytes.fromhex(param) + return True + except (TypeError, ValueError): + return False + + +def _print_validation_fail_reason( + exc: fastjsonschema.JsonSchemaValueException, + extra_formatters: Optional[Dict[str, Callable[[str], bool]]] = None, +) -> str: + """Print formatted and easy to read reason why the validation failed. + + :param exc: Original exception. + :param extra_formatters: Additional custom formatters + :return: String explaining the reason of fail. + """ + + def process_nested_rule( + exception: fastjsonschema.JsonSchemaValueException, + extra_formatters: Optional[Dict[str, Callable[[str], bool]]], + ) -> str: + message = "" + for rule_def_ix, rule_def in enumerate(exception.rule_definition): + try: + validator = fastjsonschema.compile(rule_def, formats=extra_formatters) + validator(exception.value) + message += f"\nRule#{rule_def_ix} passed.\n" + except fastjsonschema.JsonSchemaValueException as _exc: + message += ( + f"\nReason of fail for {exception.rule} rule#{rule_def_ix}: " + f"\n {_print_validation_fail_reason(_exc , extra_formatters)}\n" + ) + if all(rule_def.get("required") for rule_def in exception.rule_definition): + message += f"\nYou need to define {exception.rule} of the following sets:" + for rule_def in exc.rule_definition: + message += f" {rule_def['required']}" + return message + + message = str(exc) + if exc.rule == "required": + missing = filter(lambda x: x not in exc.value.keys(), exc.rule_definition) + message += f"; Missing field(s): {', '.join(missing)}" + elif exc.rule == "format": + if exc.rule_definition == "file": + message += f"; Non-existing file: {exc.value}" + message += "; The file must exists even if the key is NOT used in configuration." + elif exc.rule == "anyOf": + message += process_nested_rule(exc, extra_formatters=extra_formatters) + elif exc.rule == "oneOf": + message += process_nested_rule(exc, extra_formatters=extra_formatters) + return message + + +def check_config( + config: Union[str, Dict[str, Any]], + schemas: List[Dict[str, Any]], + extra_formatters: Optional[Dict[str, Callable[[str], bool]]] = None, + search_paths: Optional[List[str]] = None, +) -> None: + """Check the configuration by provided list of validation schemas. + + :param config: Configuration to check + :param schemas: List of validation schemas + :param extra_formatters: Additional custom formatters + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid validation schema or configuration + """ + custom_formatters: Dict[str, Callable[[str], bool]] = { + "dir": lambda x: bool(find_dir(x, search_paths=search_paths, raise_exc=False)), + "file": lambda x: bool(find_file(x, search_paths=search_paths, raise_exc=False)), + "file_name": lambda x: os.path.basename(x.replace("\\", "/")) not in ("", None), + "optional_file": lambda x: not x + or bool(find_file(x, search_paths=search_paths, raise_exc=False)), + "number": _is_number, + "hex_value": _is_hex_number, + } + if isinstance(config, str): + config_to_check = load_configuration(config) + config_dir = os.path.dirname(config) + if search_paths: + search_paths.append(config_dir) + else: + search_paths = [config_dir] + else: + config_to_check = copy.deepcopy(config) + + schema: Dict[str, Any] = {} + for sch in schemas: + always_merger.merge(schema, copy.deepcopy(sch)) + validator = None + formats = always_merger.merge(custom_formatters, extra_formatters or {}) + try: + if ENABLE_DEBUG: + validator_code = fastjsonschema.compile_to_code(schema, formats=formats) + write_file(validator_code, "validator_file.py") + else: + validator = fastjsonschema.compile(schema, formats=formats) + except (TypeError, fastjsonschema.JsonSchemaDefinitionException) as exc: + raise SPSDKError(f"Invalid validation schema to check config: {str(exc)}") from exc + try: + if ENABLE_DEBUG: + # pylint: disable=import-error,import-outside-toplevel + import validator_file + + validator_file.validate(config_to_check, formats) + else: + assert validator is not None + validator(config_to_check) + except fastjsonschema.JsonSchemaValueException as exc: + message = _print_validation_fail_reason(exc, formats) + raise SPSDKError(f"Configuration validation failed: {message}") from exc + + +class CommentedConfig: + """Class for generating commented config templates or custom configurations.""" + + MAX_LINE_LENGTH = 120 - 2 # Minus '# ' + + def __init__( + self, + main_title: str, + schemas: List[Dict[str, Any]], + note: Optional[str] = None, + ): + """Constructor for Config templates. + + :param main_title: Main title of final template. + :param schemas: Main description of final template. + :param note: Additional Note after title test. + """ + self.main_title = main_title + self.schemas = schemas + self.indent = 0 + self.note = note + self.creating_configuration = False + + @property + def max_line(self) -> int: + """Maximal line with current indent.""" + return self.MAX_LINE_LENGTH - max(SPSDK_YML_INDENT * (self.indent - 1), 0) + + def _get_title_block(self, title: str, description: Optional[str] = None) -> str: + """Get unified title blob. + + :param title: Simple title of block + :param description: Description of block + :return: ASCII art block + """ + delimiter = "=" * self.max_line + title_str = f" == {title} == " + title_str = title_str.center(self.max_line) + + ret = delimiter + "\n" + title_str + "\n" + if description: + wrapped_description = wrap_text(description, self.max_line) + lines = wrapped_description.splitlines() + ret += "\n".join([line.center(self.max_line) for line in lines]) + ret += "\n" + ret += delimiter + return ret + + @staticmethod + def get_property_optional_required(key: str, block: Dict[str, Any]) -> PropertyRequired: + """Function to determine if the config property is required or not. + + :param key: Name of config record + :param block: Source data block + :return: Final description. + """ + schema_kws = ["allOf", "anyOf", "oneOf", "if", "then", "else"] + + def _find_required(d_in: Dict[str, Any]) -> Optional[List[str]]: + if "required" in d_in: + return d_in["required"] + + for d_v in d_in.values(): + if isinstance(d_v, dict): + ret = _find_required(d_v) + if ret: + return ret + return None + + def _find_required_in_schema_kws(schema_node: Union[List, Dict[str, Any]]) -> List[str]: + """Find all required properties in structure composed of nested properties.""" + all_props: List[str] = [] + if isinstance(schema_node, dict): + for k, v in schema_node.items(): + if k == "required": + all_props.extend(v) + elif k in schema_kws: + req_props = _find_required_in_schema_kws(v) + all_props.extend(req_props) + if isinstance(schema_node, list): + for item in schema_node: + req_props = _find_required_in_schema_kws(item) + all_props.extend(req_props) + return list(set(all_props)) + + if "required" in block and key in block["required"]: + return PropertyRequired.REQUIRED + + for val in block.values(): + if isinstance(val, dict): + ret = _find_required(val) + if ret and key in ret: + return PropertyRequired.CONDITIONALLY_REQUIRED + + actual_kws = {k: v for k, v in block.items() if k in schema_kws} + ret = _find_required_in_schema_kws(actual_kws) + if key in ret: + return PropertyRequired.CONDITIONALLY_REQUIRED + + return PropertyRequired.OPTIONAL + + def _create_object_block( + self, + block: Dict[str, Dict[str, Any]], + custom_value: Optional[Union[Dict[str, Any], List[Any]]] = None, + ) -> CMap: + """Private function used to create object block with data. + + :param block: Source block with data + :param custom_value: + Optional dictionary or List of properties to be exported. + It is recommended to pass OrderedDict to preserve the key order. + - key is property ID to be exported + - value is its value; or None if default value shall be used + :return: CMap or CSeq base configuration object + :raises SPSDKError: In case of invalid data pattern. + """ + assert block.get("type") == "object" + self.indent += 1 + + assert "properties" in block.keys() + + cfg_m = CMap() + for key in self._get_schema_block_keys(block): + assert ( + key in block["properties"].keys() + ), f"Missing key ({key}, in block properties. Block title: {block.get('title', 'Unknown')})" + + # Skip the record in case that custom value key is defined, + # but it has None value as a mark to not use this record + value = custom_value.get(key, None) if custom_value else None # type: ignore + if custom_value and value is None: + continue + + val_p: Dict = block["properties"][key] + value_to_add = self._get_schema_value(val_p, value) + if value_to_add is None: + raise SPSDKError(f"Cannot create the value for {key}") + + cfg_m[key] = value_to_add + required = self.get_property_optional_required(key, block).description + assert required + self._add_comment( + cfg_m, + val_p, + key, + value_to_add, + required, + ) + + self.indent -= 1 + return cfg_m + + def _create_array_block( + self, block: Dict[str, Dict[str, Any]], custom_value: Optional[List[Any]] + ) -> CSeq: + """Private function used to create array block with data. + + :param block: Source block with data + :return: CS base configuration object + :raises SPSDKError: In case of invalid data pattern. + """ + assert block.get("type") == "array" + assert "items" in block.keys() + self.indent += 1 + val_i: Dict = block["items"] + + cfg_s = CSeq() + if custom_value is not None: + for cust_val in custom_value: + value = self._get_schema_value(val_i, cust_val) + if isinstance(value, (CSeq, List)): + cfg_s.extend(value) + else: + cfg_s.append(value) + else: + value = self._get_schema_value(val_i, None) + # the template_value can be the actual list(not only one element) + if isinstance(value, (CSeq, List)): + cfg_s.extend(value) + else: + cfg_s.append(value) + self.indent -= 1 + return cfg_s + + @staticmethod + def _check_matching_oneof_option(one_of: Dict[str, Any], cust_val: Any) -> bool: + """Find matching given custom value to "oneOf" schema. + + :param one_of:oneOf schema + :param cust_val: custom value + :raises SPSDKError: if not found + """ + + def check_type(option: Dict, t: str) -> bool: + option_type = option.get("type") + if isinstance(option_type, list): + return t in option_type + return t == option_type + + if cust_val: + if isinstance(cust_val, dict) and check_type(one_of, "object"): + properties = one_of.get("properties") + assert properties, "non-empty properties must be defined" + if all([key in properties for key in cust_val.keys()]): + return True + + if isinstance(cust_val, str) and check_type(one_of, "string"): + return True + + if isinstance(cust_val, int) and check_type(one_of, "number"): + return True + + return False + + def _handle_one_of_block( + self, + block: Dict[str, Any], + custom_value: Optional[Union[Dict[str, Any], List[Any]]] = None, + ) -> CMap: + """Private function used to create oneOf block with data, and return as an array that contains all values. + + :param block: Source block with data + :param custom_value: custom value to fill the array + :return: CS base configuration object + """ + + def get_help_name(schema: Dict) -> str: + if schema.get("type") == "object": + options = list(schema["properties"].keys()) + if len(options) == 1: + return options[0] + return str(options) + return str(schema.get("title", schema.get("type", "Unknown"))) + + ret = CMap() + one_of = block + assert isinstance(one_of, list) + if custom_value is not None: + for i, one_option in enumerate(one_of): + if not self._check_matching_oneof_option(one_option, custom_value): + continue + return self._get_schema_value(one_option, custom_value) + raise SPSDKError("Any allowed option matching the configuration data") + + # Check the restriction into templates in oneOf block + one_of_mod = [] + for x in one_of: + skip = x.get("skip_in_template", False) + if not skip: + one_of_mod.append(x) + + # In case that only one oneOf option left just return simple value + if len(one_of_mod) == 1: + return self._get_schema_value(one_of_mod[0], custom_value) + + option_types = ", ".join([get_help_name(x) for x in one_of_mod]) + title = f"List of possible {len(one_of_mod)} options." + for i, option in enumerate(one_of_mod): + if option.get("type") != "object": + continue + value = self._get_schema_value(option, None) + assert isinstance(value, CMap) + cmap_update(ret, value) + + key = list(value.keys())[0] + comment = "" + if i == 0: + comment = self._get_title_block(title, f"Options [{option_types}]") + "\n" + comment += "\n " + ( + f" [Example of possible configuration #{i}] ".center(self.max_line, "=") + ) + self._update_before_comment(cfg=ret, key=key, comment=comment) + return ret + + def _get_schema_value( + self, block: Dict[str, Any], custom_value: Any + ) -> Union[CMap, CSeq, str, int, float, List]: + """Private function used to fill up configuration block with data. + + :param block: Source block with data + :param custom_value: value to be saved instead of default value + :return: CM/CS base configuration object with comment + :raises SPSDKError: In case of invalid data pattern. + """ + + def get_custom_or_template() -> Any: + assert ( + custom_value or "template_value" in block.keys() + ), f"Template value not provided in {block}" + return ( + custom_value + if (custom_value is not None) + else block.get("template_value", "Unknown") + ) + + ret: Optional[Union[CMap, CSeq, str, int, float]] = None + if "oneOf" in block and not "properties" in block: + ret = self._handle_one_of_block(block["oneOf"], custom_value) + if not ret: + ret = get_custom_or_template() + else: + schema_type = block.get("type") + if not schema_type: + raise SPSDKError(f"Type not available in block: {block}") + assert schema_type, f"Type not available in block: {block}" + + if schema_type == "object": + assert (custom_value is None) or isinstance(custom_value, dict) + ret = self._create_object_block(block, custom_value) + elif schema_type == "array": + assert (custom_value is None) or isinstance(custom_value, list) + ret = self._create_array_block(block, custom_value) + else: + ret = get_custom_or_template() + + assert isinstance(ret, (CMap, CSeq, str, int, float, list)) + + return ret + + def _add_comment( + self, + cfg: Union[CMap, CSeq], + schema: Dict[str, Any], + key: Union[str, int], + value: Optional[Union[CMap, CSeq, str, int, float, List]], + required: str, + ) -> None: + """Private function used to create comment for block. + + :param cfg: Target configuration where the comment should be stored + :param schema: Object configuration JSON SCHEMA + :param key: Config key + :param value: Value of config key + :param required: Required text description + """ + value_len = len(str(key) + ": ") + if value and isinstance(value, (str, int)): + value_len += len(str(value)) + template_title = schema.get("template_title") + title = schema.get("title", "") + descr = schema.get("description", "") + enum_list = schema.get("enum", schema.get("enum_template", [])) + enum = "" + + if len(enum_list): + enum = "Possible options: <" + ", ".join([str(x) for x in enum_list]) + ">" + if title: + # one_line_comment = ( + # f"[{required}] {title}{'; ' if descr else ''}{descr}{';'+enum if enum else ''}" + # ) + # TODO This feature will be disabled since the issue + # https://sourceforge.net/p/ruamel-yaml/tickets/475/ will be solved + # if True: # len(one_line_comment) > self.max_line - value_len: + # Too long comment split it into comment block + comment = f"===== {title} [{required}] =====".center(self.max_line, "-") + if descr: + comment += wrap_text("\nDescription: " + descr, max_line=self.max_line) + if enum: + comment += wrap_text("\n" + enum, max_line=self.max_line) + cfg.yaml_set_comment_before_after_key( + key, comment, indent=SPSDK_YML_INDENT * (self.indent - 1) + ) + # else: + # cfg.yaml_add_eol_comment( + # one_line_comment, + # key=key, + # column=SPSDK_YML_INDENT * (self.indent - 1), + # ) + + if template_title: + self._update_before_comment(cfg, key, "\n" + self._get_title_block(template_title)) + + @staticmethod + def _get_schema_block_keys(schema: Dict[str, Dict[str, Any]]) -> List[str]: + """Creates list of property keys in given schema. + + :param schema: Input schema piece. + :return: List of all property keys. + """ + if "properties" not in schema: + return [] + return [ + key + for key in schema["properties"] + if schema["properties"][key].get("skip_in_template", False) == False + ] + + def _update_before_comment( + self, cfg: Union[CMap, CSeq], key: Union[str, int], comment: str + ) -> None: + """Update comment to add new comment before current one. + + :param sfg: Commented map / Commented Sequence + :param key: Key name + :param comment: Comment that should be place before current one. + """ + from ruamel.yaml.error import CommentMark + from ruamel.yaml.tokens import CommentToken + + def comment_token(s: str, mark: CommentMark) -> CommentToken: + # handle empty lines as having no comment + return CommentToken(("# " if s else "") + s + "\n", mark) + + comments = cfg.ca.items.setdefault(key, [None, None, None, None]) + if not isinstance(comments[1], list): + comments[1] = [] + new_lines = comment.splitlines() + new_lines.reverse() + start_mark = CommentMark(SPSDK_YML_INDENT * (self.indent - 1)) + for c in new_lines: + comments[1].insert(0, comment_token(c, start_mark)) + + def export(self, config: Optional[Dict[str, Any]] = None) -> CMap: + """Export configuration template into CommentedMap. + + :param config: Configuration to be applied to template. + :raises SPSDKError: Error + :return: Configuration template in CM. + """ + self.indent = 0 + self.creating_configuration = bool(config) + loc_schemas = copy.deepcopy(self.schemas) + # 1. Get blocks with their titles and lists of their keys + block_list: Dict[str, Any] = {} + for schema in loc_schemas: + if schema.get("skip_in_template", False): + continue + title = schema.get("title", "General Options") + if title in block_list: + property_list = block_list[title]["properties"] + assert isinstance(property_list, list) + property_list.extend( + [ + x + for x in self._get_schema_block_keys(schema) + if x not in block_list[title]["properties"] + ] + ) + else: + block_list[title] = {} + block_list[title]["properties"] = self._get_schema_block_keys(schema) + block_list[title]["description"] = schema.get("description", "") + + # 2. Merge all schemas together to get whole single schema + schemas_merger = SPSDKMerger( + [(list, ["set"]), (dict, ["merge"]), (set, ["union"])], + ["override"], + ["override"], + ) + + merged: Dict[str, Any] = {} + for schema in loc_schemas: + schemas_merger.merge(merged, copy.deepcopy(schema)) + + # 3. Create order of individual settings + + order_dict: Dict[str, Any] = OrderedDict() + properties_for_template = self._get_schema_block_keys(merged) + for block in block_list.values(): + block_properties: list = block["properties"] + # block_properties.sort() + for block_property in block_properties: + if block_property in properties_for_template: + order_dict[block_property] = merged["properties"][block_property] + merged["properties"] = order_dict + + try: + self.indent = 0 + # 4. Go through all individual logic blocks + cfg = self._create_object_block(merged, config) + assert isinstance(cfg, CMap) + # 5. Add main title of configuration + title = f" {self.main_title} ".center(self.MAX_LINE_LENGTH, "=") + "\n\n" + if self.note: + title += f"\n{' Note '.center(self.MAX_LINE_LENGTH, '-')}\n" + title += wrap_text(self.note, self.max_line) + "\n" + cfg.yaml_set_start_comment(title) + for title, info in block_list.items(): + description = info["description"] + assert isinstance(description, str) or description is None + + first_key = None + for info_key in info["properties"]: + if info_key in cfg.keys(): + first_key = info_key + break + + if first_key: + self._update_before_comment( + cfg, first_key, self._get_title_block(title, description) + ) + + self.creating_configuration = False + return cfg + + except Exception as exc: + self.creating_configuration = False + raise SPSDKError(f"Template generation failed: {str(exc)}") from exc + + def get_template(self) -> str: + """Export Configuration template directly into YAML string format. + + :return: YAML string. + """ + return self.convert_cm_to_yaml(self.export()) + + def get_config(self, config: Dict[str, Any]) -> str: + """Export Configuration directly into YAML string format. + + :return: YAML string. + """ + return self.convert_cm_to_yaml(self.export(config)) + + @staticmethod + def convert_cm_to_yaml(config: CMap) -> str: + """Convert Commented Map for into final YAML string. + + :param config: Configuration in CM format. + :raises SPSDKError: If configuration is empty + :return: YAML string with configuration to use to store in file. + """ + if not config: + raise SPSDKError("Configuration cannot be empty") + yaml = YAML(pure=True) + yaml.indent(sequence=SPSDK_YML_INDENT * 2, offset=SPSDK_YML_INDENT) + stream = io.StringIO() + yaml.dump(config, stream) + yaml_data = stream.getvalue() + + return yaml_data diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py new file mode 100644 index 00000000..7bec30ee --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Custom enum extension.""" +from dataclasses import dataclass +from enum import Enum +from typing import Callable, List, Optional, Union + +from typing_extensions import Self + +from ..exceptions import SPSDKKeyError, SPSDKTypeError + + +@dataclass(frozen=True) +class SpsdkEnumMember: + """SPSDK Enum member.""" + + tag: int + label: str + description: Optional[str] = None + + +class SpsdkEnum(SpsdkEnumMember, Enum): + """SPSDK Enum type.""" + + def __eq__(self, __value: object) -> bool: + return self.tag == __value or self.label == __value + + def __hash__(self) -> int: + return hash((self.tag, self.label, self.description)) + + @classmethod + def labels(cls) -> List[str]: + """Get list of labels of all enum members. + + :return: List of all labels + """ + return [value.label for value in cls.__members__.values()] + + @classmethod + def tags(cls) -> List[int]: + """Get list of tags of all enum members. + + :return: List of all tags + """ + return [value.tag for value in cls.__members__.values()] + + @classmethod + def contains(cls, obj: Union[int, str]) -> bool: + """Check if given member with given tag/label exists in enum. + + :param obj: Label or tag of enum + :return: True if exists False otherwise + """ + if not isinstance(obj, (int, str)): + raise SPSDKTypeError("Object must be either string or integer") + try: + cls.from_attr(obj) + return True + except SPSDKKeyError: + return False + + @classmethod + def get_tag(cls, label: str) -> int: + """Get tag of enum member with given label. + + :param label: Label to be used for searching + :return: Tag of found enum member + """ + value = cls.from_label(label) + return value.tag + + @classmethod + def get_label(cls, tag: int) -> str: + """Get label of enum member with given tag. + + :param tag: Tag to be used for searching + :return: Label of found enum member + """ + value = cls.from_tag(tag) + return value.label + + @classmethod + def get_description(cls, tag: int, default: Optional[str] = None) -> Optional[str]: + """Get description of enum member with given tag. + + :param tag: Tag to be used for searching + :param default: Default value if member contains no description + :return: Description of found enum member + """ + value = cls.from_tag(tag) + return value.description or default + + @classmethod + def from_attr(cls, attribute: Union[int, str]) -> Self: + """Get enum member with given tag/label attribute. + + :param attribute: Attribute value of enum member + :return: Found enum member + """ + # Let's make MyPy happy, see https://github.com/python/mypy/issues/10740 + if isinstance(attribute, int): + return cls.from_tag(attribute) + else: + return cls.from_label(attribute) + + @classmethod + def from_tag(cls, tag: int) -> Self: + """Get enum member with given tag. + + :param tag: Tag to be used for searching + :raises SPSDKKeyError: If enum with given label is not found + :return: Found enum member + """ + for item in cls.__members__.values(): + if item.tag == tag: + return item + raise SPSDKKeyError(f"There is no {cls.__name__} item in with tag {tag} defined") + + @classmethod + def from_label(cls, label: str) -> Self: + """Get enum member with given label. + + :param label: Label to be used for searching + :raises SPSDKKeyError: If enum with given label is not found + :return: Found enum member + """ + for item in cls.__members__.values(): + if item.label.upper() == label.upper(): + return item + raise SPSDKKeyError(f"There is no {cls.__name__} item with label {label} defined") + + +class SpsdkSoftEnum(SpsdkEnum): + """SPSDK Soft Enum type. + + It has API with default values for labels and + descriptions with defaults for non existing members. + """ + + @classmethod + def get_label(cls, tag: int) -> str: + """Get label of enum member with given tag. + + If member not found and default is specified, the default is returned. + + :param tag: Tag to be used for searching + :return: Label of found enum member + """ + try: + return super().get_label(tag) + except SPSDKKeyError: + return f"Unknown ({tag})" + + @classmethod + def get_description(cls, tag: int, default: Optional[str] = None) -> Optional[str]: + """Get description of enum member with given tag. + + :param tag: Tag to be used for searching + :param default: Default value if member contains no description + :return: Description of found enum member + """ + try: + return super().get_description(tag, default) + except SPSDKKeyError: + return f"Unknown ({tag})" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py new file mode 100644 index 00000000..e69de29b From bbebb398e9a20e69ebdeb23ccfd0f858441de17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 20 Mar 2024 17:42:31 +0100 Subject: [PATCH 02/11] Fix compatibility with vendored spsdk --- pynitrokey/cli/__init__.py | 2 +- pynitrokey/nk3/updates.py | 2 +- pynitrokey/trussed/bootloader/lpc55.py | 14 +- .../bootloader/lpc55_upload/__init__.py | 41 +++ .../lpc55_upload/apps/utils/utils.py | 2 + .../lpc55_upload/crypto/certificate.py | 12 +- .../bootloader/lpc55_upload/crypto/cms.py | 12 +- .../lpc55_upload/crypto/exceptions.py | 2 +- .../bootloader/lpc55_upload/crypto/hash.py | 6 +- .../bootloader/lpc55_upload/crypto/hmac.py | 2 +- .../bootloader/lpc55_upload/crypto/keys.py | 6 +- .../bootloader/lpc55_upload/crypto/oscca.py | 5 +- .../lpc55_upload/crypto/signature_provider.py | 14 +- .../lpc55_upload/crypto/symmetric.py | 4 +- .../bootloader/lpc55_upload/crypto/types.py | 2 +- .../bootloader/lpc55_upload/crypto/utils.py | 10 +- .../bootloader/lpc55_upload/ele/ele_comm.py | 14 +- .../lpc55_upload/ele/ele_constants.py | 2 +- .../lpc55_upload/ele/ele_message.py | 10 +- .../lpc55_upload/image/ahab/ahab_container.py | 2 +- .../bootloader/lpc55_upload/image/header.py | 6 +- .../bootloader/lpc55_upload/image/misc.py | 4 +- .../bootloader/lpc55_upload/mboot/commands.py | 4 +- .../lpc55_upload/mboot/error_codes.py | 2 +- .../lpc55_upload/mboot/exceptions.py | 2 +- .../lpc55_upload/mboot/interfaces/buspal.py | 8 +- .../lpc55_upload/mboot/interfaces/sdio.py | 8 +- .../lpc55_upload/mboot/interfaces/uart.py | 4 +- .../lpc55_upload/mboot/interfaces/usb.py | 4 +- .../lpc55_upload/mboot/interfaces/usbsio.py | 4 +- .../bootloader/lpc55_upload/mboot/mcuboot.py | 23 +- .../bootloader/lpc55_upload/mboot/memories.py | 4 +- .../lpc55_upload/mboot/properties.py | 8 +- .../lpc55_upload/mboot/protocol/base.py | 2 +- .../mboot/protocol/bulk_protocol.py | 14 +- .../mboot/protocol/serial_protocol.py | 14 +- .../bootloader/lpc55_upload/mboot/scanner.py | 6 +- .../bootloader/lpc55_upload/sbfile/misc.py | 4 +- .../lpc55_upload/sbfile/sb2/commands.py | 12 +- .../lpc55_upload/sbfile/sb2/headers.py | 10 +- .../lpc55_upload/sbfile/sb2/images.py | 28 +- .../lpc55_upload/sbfile/sb2/sb_21_helper.py | 10 +- .../lpc55_upload/sbfile/sb2/sections.py | 12 +- .../lpc55_upload/sbfile/sb2/sly_bd_lexer.py | 3 +- .../lpc55_upload/sbfile/sb2/sly_bd_parser.py | 4 +- .../bootloader/lpc55_upload/uboot/uboot.py | 4 +- .../lpc55_upload/utils/crypto/cert_blocks.py | 26 +- .../lpc55_upload/utils/crypto/iee.py | 22 +- .../lpc55_upload/utils/crypto/otfad.py | 22 +- .../lpc55_upload/utils/crypto/rkht.py | 12 +- .../lpc55_upload/utils/crypto/rot.py | 20 +- .../bootloader/lpc55_upload/utils/database.py | 11 +- .../bootloader/lpc55_upload/utils/images.py | 8 +- .../utils/interfaces/device/serial_device.py | 6 +- .../bootloader/lpc55_upload/utils/misc.py | 6 +- .../bootloader/lpc55_upload/utils/plugins.py | 6 +- .../lpc55_upload/utils/registers.py | 8 +- .../lpc55_upload/utils/schema_validator.py | 12 +- .../lpc55_upload/utils/usbfilter.py | 292 ++++++++++++++++++ pynitrokey/trussed/utils.py | 4 +- pyproject.toml | 9 +- 61 files changed, 587 insertions(+), 245 deletions(-) create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/__init__.py diff --git a/pynitrokey/cli/__init__.py b/pynitrokey/cli/__init__.py index d51a8ba0..1e56714b 100644 --- a/pynitrokey/cli/__init__.py +++ b/pynitrokey/cli/__init__.py @@ -72,7 +72,7 @@ def nitropy(): "ecdsa", "fido2", "pyusb", - "spsdk", + # "spsdk", ] for x in pymodules: logger.info(f"{x} version: {package_version(x)}") diff --git a/pynitrokey/nk3/updates.py b/pynitrokey/nk3/updates.py index 4899079e..e5edf1ce 100644 --- a/pynitrokey/nk3/updates.py +++ b/pynitrokey/nk3/updates.py @@ -16,7 +16,7 @@ from io import BytesIO from typing import Any, Callable, Iterator, List, Optional -from spsdk.mboot.exceptions import McuBootConnectionError +from ..trussed.bootloader.lpc55_upload.mboot.exceptions import McuBootConnectionError import pynitrokey from pynitrokey.helpers import Retries diff --git a/pynitrokey/trussed/bootloader/lpc55.py b/pynitrokey/trussed/bootloader/lpc55.py index f6aa493c..df47418f 100644 --- a/pynitrokey/trussed/bootloader/lpc55.py +++ b/pynitrokey/trussed/bootloader/lpc55.py @@ -13,13 +13,13 @@ import sys from typing import List, Optional, Tuple, TypeVar -from spsdk.mboot.error_codes import StatusCode -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 .lpc55_upload.mboot.error_codes import StatusCode +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 from pynitrokey.trussed.utils import Uuid, Version diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py new file mode 100644 index 00000000..f68b4dd3 --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + + +version = "2.1.0" + +import os + +__author__ = "NXP" +__contact__ = "michal.starecek@nxp.com" +__license__ = "BSD-3-Clause" +__version__ = version +__release__ = "beta" + +# The SPSDK behavior settings +# SPSDK_DATA_FOLDER might be redefined by SPSDK_DATA_FOLDER_{version} +# or SPSDK_DATA_FOLDER env variable +SPSDK_DATA_FOLDER_ENV_VERSION = "SPSDK_DATA_FOLDER_" + version.replace(".", "_") +SPSDK_DATA_FOLDER = ( + os.environ.get(SPSDK_DATA_FOLDER_ENV_VERSION) + or os.environ.get("SPSDK_DATA_FOLDER") + or os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") +) +SPSDK_DATA_FOLDER_COMMON = os.path.join(SPSDK_DATA_FOLDER, "common") +SPSDK_DATA_FOLDER_SCHEMAS = os.path.join(SPSDK_DATA_FOLDER, "jsonschemas") + +# SPSDK_CACHE_DISABLED might be redefined by SPSDK_CACHE_DISABLED_{version} env variable, default is False +SPSDK_ENV_CACHE_DISABLED = "SPSDK_CACHE_DISABLED_" + version.replace(".", "_") +SPSDK_CACHE_DISABLED = bool( + os.environ.get(SPSDK_ENV_CACHE_DISABLED) or os.environ.get("SPSDK_CACHE_DISABLED") or False +) + +SPSDK_YML_INDENT = 2 + + +ROOT_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) +SPSDK_EXAMPLES_FOLDER = os.path.relpath(os.path.join(ROOT_DIR, "examples")) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py index 4cc43b8f..921b7e59 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py @@ -5,6 +5,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +from typing import Dict + def filepath_from_config( config: Dict, key: str, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py index 5492ab2a..cfeb84ec 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py @@ -15,9 +15,9 @@ from cryptography.x509.extensions import ExtensionNotFound from typing_extensions import Self -from spsdk.crypto.hash import EnumHashAlgorithm -from spsdk.crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa -from spsdk.crypto.types import ( +from ..crypto.hash import EnumHashAlgorithm +from ..crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from ..crypto.types import ( SPSDKEncoding, SPSDKExtensionOID, SPSDKExtensions, @@ -26,9 +26,9 @@ SPSDKObjectIdentifier, SPSDKVersion, ) -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.abstract import BaseClass -from spsdk.utils.misc import align_block, load_binary, write_file +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.abstract import BaseClass +from ..utils.misc import align_block, load_binary, write_file class SPSDKExtensionNotFoundError(SPSDKError, ExtensionNotFound): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py index 2e50ad7f..8f7a9a79 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py @@ -12,12 +12,12 @@ from datetime import datetime from typing import Optional -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash -from spsdk.crypto.keys import ECDSASignature, PrivateKey, PrivateKeyEcc, PrivateKeyRsa -from spsdk.crypto.signature_provider import SignatureProvider -from spsdk.crypto.types import SPSDKEncoding -from spsdk.exceptions import SPSDKError, SPSDKTypeError, SPSDKValueError +from ..crypto.certificate import Certificate +from ..crypto.hash import EnumHashAlgorithm, get_hash +from ..crypto.keys import ECDSASignature, PrivateKey, PrivateKeyEcc, PrivateKeyRsa +from ..crypto.signature_provider import SignatureProvider +from ..crypto.types import SPSDKEncoding +from ..exceptions import SPSDKError, SPSDKTypeError, SPSDKValueError def cms_sign( diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py index 75f8f238..75a84d2d 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/exceptions.py @@ -7,7 +7,7 @@ """Exceptions used in the Crypto module.""" -from spsdk.exceptions import SPSDKError +from ..exceptions import SPSDKError class SPSDKPCryptoError(SPSDKError): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py index 7ead6079..a401a4ac 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py @@ -13,9 +13,9 @@ from cryptography.hazmat.primitives import hashes -from spsdk.exceptions import SPSDKError -from spsdk.utils.misc import Endianness -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..exceptions import SPSDKError +from ..utils.misc import Endianness +from ..utils.spsdk_enum import SpsdkEnum class EnumHashAlgorithm(SpsdkEnum): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py index e498916a..eaaf131c 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py @@ -12,7 +12,7 @@ # Used security modules from cryptography.hazmat.primitives import hmac as hmac_cls -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash_algorithm +from .hash import EnumHashAlgorithm, get_hash_algorithm def hmac(key: bytes, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py index 63993fac..f91a8a99 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py @@ -34,9 +34,9 @@ ) from typing_extensions import Self -from spsdk.exceptions import SPSDKError, SPSDKNotImplementedError, SPSDKValueError -from spsdk.utils.abstract import BaseClass -from spsdk.utils.misc import Endianness, load_binary, write_file +from ..exceptions import SPSDKError, SPSDKNotImplementedError, SPSDKValueError +from ..utils.abstract import BaseClass +from ..utils.misc import Endianness, load_binary, write_file from .hash import EnumHashAlgorithm, get_hash, get_hash_algorithm from .oscca import IS_OSCCA_SUPPORTED diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py index 9de1d8a2..d2df6b05 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py @@ -7,8 +7,9 @@ """Support for OSCCA SM2/SM3.""" -from spsdk import SPSDK_DATA_FOLDER_COMMON -from spsdk.utils.misc import Endianness + +from ..utils.misc import Endianness +from .. import SPSDK_DATA_FOLDER_COMMON try: # this import is to find out whether OSCCA support is installed or not diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py index b10cc161..df3e3df7 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py @@ -22,9 +22,9 @@ import requests from cryptography.hazmat.primitives.hashes import HashAlgorithm -from spsdk.crypto.exceptions import SPSDKKeysNotMatchingError -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash_algorithm -from spsdk.crypto.keys import ( +from ..crypto.exceptions import SPSDKKeysNotMatchingError +from ..crypto.hash import EnumHashAlgorithm, get_hash_algorithm +from ..crypto.keys import ( ECDSASignature, PrivateKey, PrivateKeyEcc, @@ -36,10 +36,10 @@ SPSDKKeyPassphraseMissing, prompt_for_passphrase, ) -from spsdk.crypto.types import SPSDKEncoding -from spsdk.exceptions import SPSDKError, SPSDKKeyError, SPSDKUnsupportedOperation, SPSDKValueError -from spsdk.utils.misc import find_file -from spsdk.utils.plugins import PluginsManager, PluginType +from ..crypto.types import SPSDKEncoding +from ..exceptions import SPSDKError, SPSDKKeyError, SPSDKUnsupportedOperation, SPSDKValueError +from ..utils.misc import find_file +from ..utils.plugins import PluginsManager, PluginType logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py index 8d652d10..64c7d1b8 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py @@ -14,8 +14,8 @@ from cryptography.hazmat.primitives import keywrap from cryptography.hazmat.primitives.ciphers import Cipher, aead, algorithms, modes -from spsdk.exceptions import SPSDKError -from spsdk.utils.misc import Endianness, align_block +from ..exceptions import SPSDKError +from ..utils.misc import Endianness, align_block class Counter: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py index a48c3095..3e88af2f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py @@ -14,7 +14,7 @@ from cryptography.x509.extensions import ExtensionOID, Extensions, KeyUsage from cryptography.x509.name import Name, NameOID, ObjectIdentifier -from spsdk.exceptions import SPSDKError +from ..exceptions import SPSDKError class SPSDKEncoding(utils.Enum): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py index dd9a7118..2e65946c 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py @@ -9,11 +9,11 @@ from typing import Iterable, List, Optional -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.keys import PrivateKey, PublicKey -from spsdk.crypto.signature_provider import SignatureProvider -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.misc import load_binary +from ..crypto.certificate import Certificate +from ..crypto.keys import PrivateKey, PublicKey +from ..crypto.signature_provider import SignatureProvider +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.misc import load_binary def get_matching_key_id(public_keys: List[PublicKey], signature_provider: SignatureProvider) -> int: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py index d2b940f4..763594d3 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py @@ -13,13 +13,13 @@ from types import TracebackType from typing import List, Optional, Tuple, Type, Union -from spsdk.ele.ele_constants import ResponseStatus -from spsdk.ele.ele_message import EleMessage -from spsdk.exceptions import SPSDKError, SPSDKLengthError -from spsdk.mboot.mcuboot import McuBoot -from spsdk.uboot.uboot import Uboot -from spsdk.utils.database import DatabaseManager, get_db, get_families -from spsdk.utils.misc import value_to_bytes +from ..ele.ele_constants import ResponseStatus +from ..ele.ele_message import EleMessage +from ..exceptions import SPSDKError, SPSDKLengthError +from ..mboot.mcuboot import McuBoot +from ..uboot.uboot import Uboot +from ..utils.database import DatabaseManager, get_db, get_families +from ..utils.misc import value_to_bytes logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py index 325c12af..b448ce51 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py @@ -7,7 +7,7 @@ """EdgeLock Enclave Message constants.""" -from spsdk.utils.spsdk_enum import SpsdkEnum, SpsdkSoftEnum +from ..utils.spsdk_enum import SpsdkEnum, SpsdkSoftEnum class MessageIDs(SpsdkSoftEnum): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py index a1cb4042..4e016274 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py @@ -14,7 +14,7 @@ from crcmod.predefined import mkPredefinedCrcFun -from spsdk.ele.ele_constants import ( +from ..ele.ele_constants import ( EleCsalState, EleFwStatus, EleInfo2Commit, @@ -28,10 +28,10 @@ ResponseIndication, ResponseStatus, ) -from spsdk.exceptions import SPSDKParsingError, SPSDKValueError -from spsdk.image.ahab.signed_msg import SignedMessage -from spsdk.utils.misc import Endianness, align, align_block -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..exceptions import SPSDKParsingError, SPSDKValueError +from ..image.ahab.signed_msg import SignedMessage +from ..utils.misc import Endianness, align, align_block +from ..utils.spsdk_enum import SpsdkEnum logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py index dc552abc..abcbac85 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py @@ -19,7 +19,7 @@ from typing_extensions import Self -spsdk_version = "2.1.0" +from ... import version as spsdk_version from ...crypto.hash import EnumHashAlgorithm, get_hash from ...crypto.keys import ( IS_OSCCA_SUPPORTED, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py index 1f6abc97..04e5557a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py @@ -13,9 +13,9 @@ from typing_extensions import Self -from spsdk.exceptions import SPSDKError, SPSDKParsingError -from spsdk.utils.abstract import BaseClass -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..exceptions import SPSDKError, SPSDKParsingError +from ..utils.abstract import BaseClass +from ..utils.spsdk_enum import SpsdkEnum ######################################################################################################################## # Enums diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py index b47aeedf..43ff9f85 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py @@ -11,8 +11,8 @@ from io import SEEK_CUR from typing import Optional, Union -from spsdk.exceptions import SPSDKError -from spsdk.utils.registers import value_to_int +from ..exceptions import SPSDKError +from ..utils.registers import value_to_int from .header import Header diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py index 9f9dc4d5..5c64e02a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py @@ -11,8 +11,8 @@ from struct import pack, unpack, unpack_from from typing import Dict, List, Optional, Type -from spsdk.utils.interfaces.commands import CmdPacketBase, CmdResponseBase -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..utils.interfaces.commands import CmdPacketBase, CmdResponseBase +from ..utils.spsdk_enum import SpsdkEnum from .error_codes import StatusCode from .exceptions import McuBootError diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py index 278fa96a..2b2325f3 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/error_codes.py @@ -8,7 +8,7 @@ """Status and error codes used by the MBoot protocol.""" -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..utils.spsdk_enum import SpsdkEnum ######################################################################################################################## # McuBoot Status Codes (Errors) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py index 91ef89c0..cfe78be7 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py @@ -8,7 +8,7 @@ """Exceptions used in the MBoot module.""" -from spsdk.exceptions import SPSDKError +from ..exceptions import SPSDKError from .error_codes import StatusCode diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py index e0add394..d543cbec 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py @@ -17,10 +17,10 @@ from serial.tools.list_ports import comports from typing_extensions import Self -from spsdk.exceptions import SPSDKError -from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from spsdk.mboot.protocol.serial_protocol import FPType, MbootSerialProtocol, to_int -from spsdk.utils.interfaces.device.serial_device import SerialDevice +from ...exceptions import SPSDKError +from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from ...mboot.protocol.serial_protocol import FPType, MbootSerialProtocol, to_int +from ...utils.interfaces.device.serial_device import SerialDevice logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py index 88d9741a..61ea7737 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py @@ -13,10 +13,10 @@ from typing_extensions import Self -from spsdk.mboot.commands import CmdResponse, parse_cmd_response -from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from spsdk.mboot.protocol.serial_protocol import FPType, MbootSerialProtocol -from spsdk.utils.interfaces.device.sdio_device import SdioDevice +from ...mboot.commands import CmdResponse, parse_cmd_response +from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from ...mboot.protocol.serial_protocol import FPType, MbootSerialProtocol +from ...utils.interfaces.device.sdio_device import SdioDevice logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py index 6785d569..bcd26e1d 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py @@ -13,8 +13,8 @@ from typing_extensions import Self -from spsdk.mboot.protocol.serial_protocol import MbootSerialProtocol -from spsdk.utils.interfaces.device.serial_device import SerialDevice +from ...mboot.protocol.serial_protocol import MbootSerialProtocol +from ...utils.interfaces.device.serial_device import SerialDevice logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py index 687b8e4b..3d104410 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usb.py @@ -14,8 +14,8 @@ from typing_extensions import Self -from spsdk.mboot.protocol.bulk_protocol import MbootBulkProtocol -from spsdk.utils.interfaces.device.usb_device import UsbDevice +from ...mboot.protocol.bulk_protocol import MbootBulkProtocol +from ...utils.interfaces.device.usb_device import UsbDevice @dataclass diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py index e87d6873..78d72ad2 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py @@ -10,8 +10,8 @@ from typing_extensions import Self -from spsdk.mboot.protocol.serial_protocol import MbootSerialProtocol -from spsdk.utils.interfaces.device.usbsio_device import ScanArgs, UsbSioI2CDevice, UsbSioSPIDevice +from ...mboot.protocol.serial_protocol import MbootSerialProtocol +from ...utils.interfaces.device.usbsio_device import ScanArgs, UsbSioI2CDevice, UsbSioSPIDevice class MbootUsbSioI2CInterface(MbootSerialProtocol): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py index 145d580b..99ba4c57 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py @@ -14,8 +14,8 @@ from types import TracebackType from typing import Callable, Dict, List, Optional, Sequence, Type -from spsdk.mboot.protocol.base import MbootProtocolBase -from spsdk.utils.interfaces.device.usb_device import UsbDevice +from ..mboot.protocol.base import MbootProtocolBase +from ..utils.interfaces.device.usb_device import UsbDevice from .commands import ( CmdPacket, @@ -683,20 +683,21 @@ def receive_sb_file( if isinstance(self._interface.device, UsbDevice): try: # pylint: disable=import-outside-toplevel # import only if needed to save time - from spsdk.sbfile.sb2.images import ImageHeaderV2 + from ..sbfile.sb2.images import ImageHeaderV2 sb2_header = ImageHeaderV2.parse(data=data) self._pause_point = sb2_header.first_boot_tag_block * 16 except SPSDKError: pass - try: - # pylint: disable=import-outside-toplevel # import only if needed to save time - from spsdk.sbfile.sb31.images import SecureBinary31Header - - sb3_header = SecureBinary31Header.parse(data=data) - self._pause_point = sb3_header.image_total_length - except SPSDKError: - pass + # Deactivated for pynitrokey + # try: + # # pylint: disable=import-outside-toplevel # import only if needed to save time + # from spsdk.sbfile.sb31.images import SecureBinary31Header + + # sb3_header = SecureBinary31Header.parse(data=data) + # self._pause_point = sb3_header.image_total_length + # except SPSDKError: + # pass result = self._send_data(CommandTag.RECEIVE_SB_FILE, data_chunks, progress_callback) self.enable_data_abort = False return result diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py index d365eb0e..599461b3 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py @@ -10,8 +10,8 @@ from typing import List, Optional, cast -from spsdk.utils.misc import size_fmt -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..utils.misc import size_fmt +from ..utils.spsdk_enum import SpsdkEnum LEGACY_MEM_ID = { "internal": "INTERNAL", diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py index 2d68658d..b8f6f004 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py @@ -13,10 +13,10 @@ from copy import deepcopy from typing import Callable, Dict, List, Optional, Tuple, Type, Union -from spsdk.exceptions import SPSDKKeyError -from spsdk.mboot.exceptions import McuBootError -from spsdk.utils.misc import Endianness -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..exceptions import SPSDKKeyError +from ..mboot.exceptions import McuBootError +from ..utils.misc import Endianness +from ..utils.spsdk_enum import SpsdkEnum from .commands import CommandTag from .error_codes import StatusCode diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py index e62346fb..97330692 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/base.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: BSD-3-Clause """MBoot protocol base.""" -from spsdk.utils.interfaces.protocol.protocol_base import ProtocolBase +from ...utils.interfaces.protocol.protocol_base import ProtocolBase class MbootProtocolBase(ProtocolBase): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py index 5355c5f9..617d0f50 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py @@ -10,13 +10,13 @@ from struct import pack, unpack_from from typing import Optional, Union -from spsdk.exceptions import SPSDKAttributeError -from spsdk.mboot.commands import CmdResponse, parse_cmd_response -from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from spsdk.mboot.protocol.base import MbootProtocolBase -from spsdk.utils.exceptions import SPSDKTimeoutError -from spsdk.utils.interfaces.commands import CmdPacketBase -from spsdk.utils.spsdk_enum import SpsdkEnum +from ...exceptions import SPSDKAttributeError +from ...mboot.commands import CmdResponse, parse_cmd_response +from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from ...mboot.protocol.base import MbootProtocolBase +from ...utils.exceptions import SPSDKTimeoutError +from ...utils.interfaces.commands import CmdPacketBase +from ...utils.spsdk_enum import SpsdkEnum class ReportId(SpsdkEnum): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py index 5510b1cc..1d7f0eb7 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py @@ -16,13 +16,13 @@ from crcmod.predefined import mkPredefinedCrcFun from typing_extensions import Self -from spsdk.exceptions import SPSDKAttributeError -from spsdk.mboot.commands import CmdResponse, parse_cmd_response -from spsdk.mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from spsdk.mboot.protocol.base import MbootProtocolBase -from spsdk.utils.interfaces.commands import CmdPacketBase -from spsdk.utils.misc import Endianness, Timeout -from spsdk.utils.spsdk_enum import SpsdkEnum +from ...exceptions import SPSDKAttributeError +from ...mboot.commands import CmdResponse, parse_cmd_response +from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from ...mboot.protocol.base import MbootProtocolBase +from ...utils.interfaces.commands import CmdPacketBase +from ...utils.misc import Endianness, Timeout +from ...utils.spsdk_enum import SpsdkEnum logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py index 805ca4f5..306eb888 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py @@ -8,9 +8,9 @@ """Helper module used for scanning the existing devices.""" from typing import List, Optional -from spsdk.exceptions import SPSDKError -from spsdk.mboot.protocol.base import MbootProtocolBase -from spsdk.utils.interfaces.scanner_helper import InterfaceParams, parse_plugin_config +from ..exceptions import SPSDKError +from .protocol.base import MbootProtocolBase +from ..utils.interfaces.scanner_helper import InterfaceParams, parse_plugin_config def get_mboot_interface( diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py index 5f008483..8728ae10 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py @@ -10,8 +10,8 @@ from datetime import datetime, timezone from typing import Any, Sequence, Union -from spsdk.exceptions import SPSDKError -from spsdk.utils import misc +from ..exceptions import SPSDKError +from ..utils import misc class SecBootBlckSize: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py index 61f5b0b2..4009a66b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py @@ -14,12 +14,12 @@ from crcmod.predefined import mkPredefinedCrcFun from typing_extensions import Self -from spsdk.exceptions import SPSDKError -from spsdk.mboot.memories import ExtMemId -from spsdk.sbfile.misc import SecBootBlckSize -from spsdk.utils.abstract import BaseClass -from spsdk.utils.misc import Endianness -from spsdk.utils.spsdk_enum import SpsdkEnum +from ...exceptions import SPSDKError +from ...mboot.memories import ExtMemId +from ...sbfile.misc import SecBootBlckSize +from ...utils.abstract import BaseClass +from ...utils.misc import Endianness +from ...utils.spsdk_enum import SpsdkEnum ######################################################################################################################## # Constants diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py index 0e37d594..0da45af4 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py @@ -13,11 +13,11 @@ from typing_extensions import Self -from spsdk.crypto.rng import random_bytes -from spsdk.exceptions import SPSDKError -from spsdk.sbfile.misc import BcdVersion3, pack_timestamp, unpack_timestamp -from spsdk.utils.abstract import BaseClass -from spsdk.utils.misc import swap16 +from ...crypto.rng import random_bytes +from ...exceptions import SPSDKError +from ...sbfile.misc import BcdVersion3, pack_timestamp, unpack_timestamp +from ...utils.abstract import BaseClass +from ...utils.misc import swap16 ######################################################################################################################## diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py index f879e5fe..8d8bb0d4 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py @@ -14,23 +14,23 @@ from typing_extensions import Self -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash -from spsdk.crypto.hmac import hmac -from spsdk.crypto.rng import random_bytes -from spsdk.crypto.signature_provider import ( +from ...crypto.certificate import Certificate +from ...crypto.hash import EnumHashAlgorithm, get_hash +from ...crypto.hmac import hmac +from ...crypto.rng import random_bytes +from ...crypto.signature_provider import ( SignatureProvider, get_signature_provider, try_to_verify_public_key, ) -from spsdk.crypto.symmetric import Counter, aes_key_unwrap, aes_key_wrap -from spsdk.exceptions import SPSDKError -from spsdk.sbfile.misc import SecBootBlckSize -from spsdk.sbfile.sb2.sb_21_helper import SB21Helper -from spsdk.utils.abstract import BaseClass -from spsdk.utils.crypto.cert_blocks import CertBlockV1 -from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file -from spsdk.utils.misc import ( +from ...crypto.symmetric import Counter, aes_key_unwrap, aes_key_wrap +from ...exceptions import SPSDKError +from ...sbfile.misc import SecBootBlckSize +from ...sbfile.sb2.sb_21_helper import SB21Helper +from ...utils.abstract import BaseClass +from ...utils.crypto.cert_blocks import CertBlockV1 +from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file +from ...utils.misc import ( find_first, load_configuration, load_hex_string, @@ -38,7 +38,7 @@ value_to_int, write_file, ) -from spsdk.utils.schema_validator import CommentedConfig, check_config +from ...utils.schema_validator import CommentedConfig, check_config from . import sly_bd_parser as bd_parser from .commands import CmdHeader diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py index 14fa8269..eb5e6789 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py @@ -11,9 +11,9 @@ from numbers import Number from typing import Callable, Dict, List, Optional, Union -from spsdk.exceptions import SPSDKError -from spsdk.mboot.memories import ExtMemId, MemId -from spsdk.sbfile.sb2.commands import ( +from ...exceptions import SPSDKError +from ...mboot.memories import ExtMemId, MemId +from ...sbfile.sb2.commands import ( CmdBaseClass, CmdErase, CmdFill, @@ -26,8 +26,8 @@ CmdVersionCheck, VersionCheckType, ) -from spsdk.utils.crypto.otfad import KeyBlob -from spsdk.utils.misc import ( +from ...utils.crypto.otfad import KeyBlob +from ...utils.misc import ( align_block, get_bytes_cnt_of_int, load_binary, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py index 94d5353a..a9cf938a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py @@ -10,12 +10,12 @@ from struct import unpack_from from typing import Iterator, List, Optional -from spsdk.crypto.hmac import hmac -from spsdk.crypto.symmetric import Counter, aes_ctr_decrypt, aes_ctr_encrypt -from spsdk.exceptions import SPSDKError -from spsdk.sbfile.misc import SecBootBlckSize -from spsdk.utils.abstract import BaseClass -from spsdk.utils.crypto.cert_blocks import CertBlockV1 +from ...crypto.hmac import hmac +from ...crypto.symmetric import Counter, aes_ctr_decrypt, aes_ctr_encrypt +from ...exceptions import SPSDKError +from ...sbfile.misc import SecBootBlckSize +from ...utils.abstract import BaseClass +from ...utils.crypto.cert_blocks import CertBlockV1 from .commands import CmdBaseClass, CmdHeader, EnumCmdTag, EnumSectionFlag, parse_command diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py index ccd4d549..cdc8283a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py @@ -44,8 +44,7 @@ def __str__(self) -> str: """ return f"{self.name}, {self.t}, {self.value}" - -class BDLexer(Lexer): +class BDLexer(Lexer): # type: ignore """Lexer for bd files.""" def __init__(self) -> None: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py index 977d2034..932f912e 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py @@ -15,7 +15,7 @@ from sly.lex import Token from sly.yacc import YaccProduction -from spsdk.exceptions import SPSDKError +from ...exceptions import SPSDKError from . import sly_bd_lexer as bd_lexer @@ -26,7 +26,7 @@ # is disabled. # too-many-lines : the class can't be shortened, as all the methods represent # rules. -class BDParser(Parser): +class BDParser(Parser): # type: ignore """Command (BD) file parser. The parser is based on SLY framework (python implementation of Lex/YACC) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py index 18221b1c..9342a703 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py @@ -12,8 +12,8 @@ from hexdump import restore from serial import Serial -from spsdk.exceptions import SPSDKError -from spsdk.utils.misc import align, change_endianness, split_data +from ..exceptions import SPSDKError +from ..utils.misc import align, change_endianness, split_data logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py index ac6e7b7a..d3314663 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py @@ -17,24 +17,24 @@ from typing_extensions import Self -from spsdk import version as spsdk_version -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash -from spsdk.crypto.keys import PrivateKeyRsa, PublicKeyEcc -from spsdk.crypto.signature_provider import SignatureProvider, get_signature_provider -from spsdk.crypto.types import SPSDKEncoding -from spsdk.crypto.utils import extract_public_key, extract_public_key_from_data, get_matching_key_id -from spsdk.exceptions import ( +from ... import version as spsdk_version +from ...crypto.certificate import Certificate +from ...crypto.hash import EnumHashAlgorithm, get_hash +from ...crypto.keys import PrivateKeyRsa, PublicKeyEcc +from ...crypto.signature_provider import SignatureProvider, get_signature_provider +from ...crypto.types import SPSDKEncoding +from ...crypto.utils import extract_public_key, extract_public_key_from_data, get_matching_key_id +from ...exceptions import ( SPSDKError, SPSDKNotImplementedError, SPSDKTypeError, SPSDKUnsupportedOperation, SPSDKValueError, ) -from spsdk.utils.abstract import BaseClass -from spsdk.utils.crypto.rkht import RKHTv1, RKHTv21 -from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file -from spsdk.utils.misc import ( +from ...utils.abstract import BaseClass +from ...utils.crypto.rkht import RKHTv1, RKHTv21 +from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file +from ...utils.misc import ( Endianness, align, align_block, @@ -46,7 +46,7 @@ value_to_int, write_file, ) -from spsdk.utils.schema_validator import CommentedConfig +from ...utils.schema_validator import CommentedConfig logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py index 197bf5ed..0741328a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py @@ -14,14 +14,14 @@ from crcmod.predefined import mkPredefinedCrcFun -from spsdk import version as spsdk_version -from spsdk.apps.utils.utils import filepath_from_config -from spsdk.crypto.rng import random_bytes -from spsdk.crypto.symmetric import Counter, aes_ctr_encrypt, aes_xts_encrypt -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file -from spsdk.utils.images import BinaryImage -from spsdk.utils.misc import ( +from ... import version as spsdk_version +from ...apps.utils.utils import filepath_from_config +from ...crypto.rng import random_bytes +from ...crypto.symmetric import Counter, aes_ctr_encrypt, aes_xts_encrypt +from ...exceptions import SPSDKError, SPSDKValueError +from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file +from ...utils.images import BinaryImage +from ...utils.misc import ( Endianness, align_block, load_hex_string, @@ -30,9 +30,9 @@ value_to_bytes, value_to_int, ) -from spsdk.utils.registers import Registers -from spsdk.utils.schema_validator import CommentedConfig -from spsdk.utils.spsdk_enum import SpsdkEnum +from ...utils.registers import Registers +from ...utils.schema_validator import CommentedConfig +from ...utils.spsdk_enum import SpsdkEnum logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py index 38e4cc50..06d9158f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py @@ -15,15 +15,15 @@ from crcmod.predefined import mkPredefinedCrcFun -from spsdk import version as spsdk_version -from spsdk.apps.utils.utils import filepath_from_config -from spsdk.crypto.rng import random_bytes -from spsdk.crypto.symmetric import Counter, aes_ctr_encrypt, aes_key_wrap -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.database import DatabaseManager, get_db, get_families, get_schema_file -from spsdk.utils.exceptions import SPSDKRegsErrorBitfieldNotFound -from spsdk.utils.images import BinaryImage -from spsdk.utils.misc import ( +from ... import version as spsdk_version +from ...apps.utils.utils import filepath_from_config +from ...crypto.rng import random_bytes +from ...crypto.symmetric import Counter, aes_ctr_encrypt, aes_key_wrap +from ...exceptions import SPSDKError, SPSDKValueError +from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file +from ...utils.exceptions import SPSDKRegsErrorBitfieldNotFound +from ...utils.images import BinaryImage +from ...utils.misc import ( Endianness, align_block, load_binary, @@ -33,8 +33,8 @@ value_to_bytes, value_to_int, ) -from spsdk.utils.registers import Registers -from spsdk.utils.schema_validator import CommentedConfig +from ...utils.registers import Registers +from ...utils.schema_validator import CommentedConfig logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py index 5afd446a..795a1a98 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py @@ -14,12 +14,12 @@ from typing_extensions import Self -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.hash import EnumHashAlgorithm, get_hash, get_hash_length -from spsdk.crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa -from spsdk.crypto.utils import extract_public_key, extract_public_key_from_data -from spsdk.exceptions import SPSDKError -from spsdk.utils.misc import Endianness +from ...crypto.certificate import Certificate +from ...crypto.hash import EnumHashAlgorithm, get_hash, get_hash_length +from ...crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from ...crypto.utils import extract_public_key, extract_public_key_from_data +from ...exceptions import SPSDKError +from ...utils.misc import Endianness logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py index ac912295..06365f49 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py @@ -11,16 +11,16 @@ from abc import abstractmethod from typing import List, Optional, Sequence, Type, Union -from spsdk.crypto.certificate import Certificate -from spsdk.crypto.keys import PrivateKey, PublicKey -from spsdk.exceptions import SPSDKError -from spsdk.image.ahab.ahab_container import SRKRecord -from spsdk.image.ahab.ahab_container import SRKTable as AhabSrkTable -from spsdk.image.secret import SrkItem -from spsdk.image.secret import SrkTable as HabSrkTable -from spsdk.utils.crypto.rkht import RKHT, RKHTv1, RKHTv21 -from spsdk.utils.database import DatabaseManager, get_db, get_families -from spsdk.utils.misc import load_binary +from ...crypto.certificate import Certificate +from ...crypto.keys import PrivateKey, PublicKey +from ...exceptions import SPSDKError +from ...image.ahab.ahab_container import SRKRecord +from ...image.ahab.ahab_container import SRKTable as AhabSrkTable +from ...image.secret import SrkItem +from ...image.secret import SrkTable as HabSrkTable +from ...utils.crypto.rkht import RKHT, RKHTv1, RKHTv21 +from ...utils.database import DatabaseManager, get_db, get_families +from ...utils.misc import load_binary class Rot: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py index 3d5baa69..c2ce9ac9 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py @@ -17,11 +17,10 @@ import platformdirs from typing_extensions import Self -import spsdk -from spsdk import SPSDK_CACHE_DISABLED, SPSDK_DATA_FOLDER -from spsdk.crypto.hash import EnumHashAlgorithm, Hash, get_hash -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.misc import ( +from .. import SPSDK_DATA_FOLDER, SPSDK_CACHE_DISABLED +from ..crypto.hash import EnumHashAlgorithm, Hash, get_hash +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.misc import ( deep_update, find_first, load_configuration, @@ -667,7 +666,7 @@ def get_cache_filename() -> Tuple[str, str]: + get_hash(data_folder.encode(), algorithm=EnumHashAlgorithm.SHA1)[:6].hex() + ".cache" ) - cache_path = platformdirs.user_cache_dir(appname="spsdk", version=spsdk.version) + cache_path = platformdirs.user_cache_dir(appname="spsdk", version="2.1.0") return (cache_path, os.path.join(cache_path, cache_name)) @staticmethod diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py index 6b758819..d1f6064f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py @@ -15,9 +15,9 @@ import colorama -from spsdk.exceptions import SPSDKError, SPSDKOverlapError, SPSDKValueError -from spsdk.utils.database import DatabaseManager -from spsdk.utils.misc import ( +from ..exceptions import SPSDKError, SPSDKOverlapError, SPSDKValueError +from ..utils.database import DatabaseManager +from ..utils.misc import ( BinaryPattern, align, align_block, @@ -26,7 +26,7 @@ size_fmt, write_file, ) -from spsdk.utils.schema_validator import CommentedConfig +from ..utils.schema_validator import CommentedConfig if TYPE_CHECKING: # bincopy will be loaded lazily as needed, this is just to satisfy type-hint checkers diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py index ffa92b88..7a7ac517 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py @@ -13,9 +13,9 @@ from serial.tools.list_ports import comports from typing_extensions import Self -from spsdk.exceptions import SPSDKConnectionError -from spsdk.utils.exceptions import SPSDKTimeoutError -from spsdk.utils.interfaces.device.base import DeviceBase +from ....exceptions import SPSDKConnectionError +from ....utils.exceptions import SPSDKTimeoutError +from ....utils.interfaces.device.base import DeviceBase logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py index 5fca3c50..c280fd8c 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py @@ -31,9 +31,9 @@ Union, ) -from spsdk.crypto.rng import random_bytes -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.exceptions import SPSDKTimeoutError +from ..crypto.rng import random_bytes +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.exceptions import SPSDKTimeoutError # for generics T = TypeVar("T") # pylint: disable=invalid-name diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py index 16fcb29f..4525ec99 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py @@ -16,9 +16,9 @@ import importlib_metadata -from spsdk.exceptions import SPSDKError, SPSDKTypeError -from spsdk.utils.misc import SingletonMeta -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..exceptions import SPSDKError, SPSDKTypeError +from ..utils.misc import SingletonMeta +from ..utils.spsdk_enum import SpsdkEnum logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py index 2391e744..a6a8d353 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py @@ -12,16 +12,16 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union from xml.dom import minidom -from spsdk.exceptions import SPSDKError, SPSDKValueError -from spsdk.utils.exceptions import ( +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.exceptions import ( SPSDKRegsError, SPSDKRegsErrorBitfieldNotFound, SPSDKRegsErrorEnumNotFound, SPSDKRegsErrorRegisterGroupMishmash, SPSDKRegsErrorRegisterNotFound, ) -from spsdk.utils.images import BinaryImage, BinaryPattern -from spsdk.utils.misc import ( +from ..utils.images import BinaryImage, BinaryPattern +from ..utils.misc import ( Endianness, format_value, get_bytes_cnt_of_int, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py index 8e3900e1..bab8db8a 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py @@ -22,9 +22,10 @@ from ruamel.yaml.comments import CommentedMap as CMap from ruamel.yaml.comments import CommentedSeq as CSeq -from spsdk import SPSDK_YML_INDENT -from spsdk.exceptions import SPSDKError -from spsdk.utils.misc import ( +SPSDK_YML_INDENT = 2 + +from ..exceptions import SPSDKError +from ..utils.misc import ( find_dir, find_file, load_configuration, @@ -32,7 +33,7 @@ wrap_text, write_file, ) -from spsdk.utils.spsdk_enum import SpsdkEnum +from ..utils.spsdk_enum import SpsdkEnum ENABLE_DEBUG = False @@ -212,8 +213,7 @@ def check_config( raise SPSDKError(f"Invalid validation schema to check config: {str(exc)}") from exc try: if ENABLE_DEBUG: - # pylint: disable=import-error,import-outside-toplevel - import validator_file + import validator_file # type: ignore validator_file.validate(config_to_check, formats) else: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py index e69de29b..b5a90909 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module defining a USB filtering class.""" +import platform +import re +from typing import Any, Dict, Optional, Tuple + +from .misc import get_hash + + +class USBDeviceFilter: + """Generic USB Device Filtering class. + + Create a filtering instance. This instance holds the USB ID you are interested + in during USB HID device search and allows you to compare, whether + provided USB HID object is the one you are interested in. + The allowed format of `usb_id` string is following: + + vid or pid - vendor ID or product ID. String holding hex or dec number. + Hex number must be preceded by 0x or 0X. Number of characters after 0x is + 1 - 4. Mixed upper & lower case letters is allowed. e.g. "0xaB12", "0XAB12", + "0x1", "0x0001". + The decimal number is restricted only to have 1 - 5 digits, e.g. "65535" + It's allowed to set the USB filter ID to decimal number "99999", however, as + the USB VID number is four-byte hex number (max value is 65535), this will + lead to zero results. Leading zeros are not allowed e.g. 0001. This will + result as invalid match. + + The user may provide a single number as usb_id. In such a case the number + may represent either VID or PID. By default, the filter expects this number + to be a VID. In rare cases the user may want to filter based on PID. + Initialize the `search_by_pid` parameter to True in such cases. + + vid/pid - string of vendor ID & product ID separated by ':' or ',' + Same rules apply to the number format as in VID case, except, that the + string consists of two numbers separated by ':' or ','. It's not allowed + to mix hex and dec numbers, e.g. "0xab12:12345" is not allowed. + Valid vid/pid strings: + "0x12aB:0xabc", "1,99999" + + Windows specific: + instance ID - String in following format "HID\\VID_&PID_\\", + see instance ID in device manager under Windows OS. + + Linux specific: + USB device path - HID API returns path in following form: + '0003:0002:00' + + The first number represents the Bus, the second Device and the third interface. The Bus:Device + number is unique so interface is not necessary and Bus:Device should be sufficient. + + The Bus:Device can be observed using 'lsusb' command. The interface can be observed using + 'lsusb -t'. lsusb returns the Bus and Device as a 3-digit number. + It has been agreed, that the expected input is: + #, e.g. 3#11 + + Mac specific: + USB device path - HID API returns path in roughly following form: + 'IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS01@14100000/SE + Blank RT Family @14100000/IOUSBHostInterface@0/AppleUserUSBHostHIDDevice' + + This path can be found using the 'ioreg' utility or using 'IO Hardware Registry Explorer' tool. + However, using the system report from 'About This MAC -> System Report -> USB' a partial path + can also be gathered. Using the name of USB device from the 'USB Device Tree' and appending + the 'Location ID' should work. The name can be 'SE Blank RT Family' and the 'Location ID' is + in form / , e.g. '0x14200000 / 18'. + So the 'usb_id' name should be 'SE Blank RT Family @14200000' and the filter should be able to + filter out such device. + """ + + def __init__( + self, + usb_id: Optional[str] = None, + search_by_pid: bool = False, + ): + """Initialize the USB Device Filtering. + + :param usb_id: usb_id string + :param search_by_pid: if true, expects usb_id to be a PID number, VID otherwise. + """ + self.usb_id = usb_id + self.search_by_pid = search_by_pid + + def compare(self, usb_device_object: Dict[str, Any]) -> bool: + """Compares the internal `usb_id` with provided `usb_device_object`. + + The provided USB ID during initialization may be VID or PID, VID/PID pair, + or a path. See private methods for details. + + :param usb_device_object: Libusbsio/HID_API device object (dictionary) + + :return: True on match, False otherwise + """ + # Determine, whether given device matches one of the expected criterion + if self.usb_id is None: + return True + + vendor_id = usb_device_object.get("vendor_id") + product_id = usb_device_object.get("product_id") + serial_number = usb_device_object.get("serial_number") + device_name = usb_device_object.get("device_name") + # the Libusbsio/HID_API holds the path as bytes, so we convert it to string + usb_path_raw = usb_device_object.get("path") + + if usb_path_raw: + if self.usb_id == get_hash(usb_path_raw): + return True + usb_path = self.convert_usb_path(usb_path_raw) + if self._is_path(usb_path=usb_path): + return True + + if self._is_vid_or_pid(vid=vendor_id, pid=product_id): + return True + + if vendor_id and product_id and self._is_vid_pid(vid=vendor_id, pid=product_id): + return True + + if serial_number and self.usb_id.casefold() == serial_number.casefold(): + return True + + if device_name and self.usb_id.casefold() == device_name.casefold(): + return True + + return False + + def _is_path(self, usb_path: str) -> bool: + """Compares the internal usb_id with provided path. + + If the path is a substring of the usb_id, this is considered as a match + and True is returned. + + :param usb_path: path to be compared with usd_id. + :return: true on a match, false otherwise. + """ + # we check the len of usb_id, because usb_id = "" is considered + # to be always in the string returning True, which is not expected + # behavior + # the provided usb string id fully matches the instance ID + usb_id = self.usb_id or "" + if usb_id.casefold() in usb_path.casefold() and len(usb_id) > 0: + return True + + return False + + def _is_vid_or_pid(self, vid: Optional[int], pid: Optional[int]) -> bool: + # match anything starting with 0x or 0X followed by 0-9 or a-f or + # match either 0 or decimal number not starting with zero + # this regex is the same for vid and pid => xid + xid_regex = "0[xX][0-9a-fA-F]{1,4}|0|[1-9][0-9]{0,4}" + usb_id = self.usb_id or "" + if re.fullmatch(xid_regex, usb_id) is not None: + # the string corresponds to the vid/pid specification, check a match + if self.search_by_pid and pid: + if int(usb_id, 0) == pid: + return True + elif vid: + if int(usb_id, 0) == vid: + return True + + return False + + def _is_vid_pid(self, vid: int, pid: int) -> bool: + """If usb_id corresponds to VID/PID pair, compares it with provided vid/pid. + + :param vid: vendor ID to compare. + :param pid: product ID to compare. + :return: true on a match, false otherwise. + """ + # match anything starting with 0x or 0X followed by 0-9 or a-f or + # match either 0 or decimal number not starting with zero + # Above pattern is combined to match a pair corresponding to vid/pid. + vid_pid_regex = "0[xX][0-9a-fA-F]{1,4}(,|:)0[xX][0-9a-fA-F]{1,4}|(0|[1-9][0-9]{0,4})(,|:)(0|[1-9][0-9]{0,4})" + usb_id = self.usb_id or "" + if re.fullmatch(vid_pid_regex, usb_id): + # the string corresponds to the vid/pid specification, check a match + vid_pid = re.split(":|,", usb_id) + if vid == int(vid_pid[0], 0) and pid == int(vid_pid[1], 0): + return True + + return False + + @staticmethod + def convert_usb_path(hid_api_usb_path: bytes) -> str: + """Converts the Libusbsio/HID_API path into string, which can be observed from OS. + + DESIGN REMARK: this function is not part of the USBLogicalDevice, as the + class intention is to be just a simple container. But to help the class + to get the required inputs, this helper method has been provided. Additionally, + this method relies on the fact that the provided path comes from the Libusbsio/HID_API. + This method will most probably fail or provide improper results in case + path from different USB API is provided. + + :param hid_api_usb_path: USB device path from Libusbsio/HID_API + :return: Libusbsio/HID_API path converted for given platform + """ + if platform.system() == "Windows": + device_manager_path = hid_api_usb_path.decode("utf-8").upper() + device_manager_path = device_manager_path.replace("#", "\\") + result = re.search(r"\\\\\?\\(.+?)\\{", device_manager_path) + if result: + device_manager_path = result.group(1) + + return device_manager_path + + if platform.system() == "Linux": + # we expect the path in form of #, Libusbsio/HID_API returns + # :: + linux_path = hid_api_usb_path.decode("utf-8") + linux_path_parts = linux_path.split(":") + + if len(linux_path_parts) > 1: + linux_path = str.format( + "{}#{}", int(linux_path_parts[0], 16), int(linux_path_parts[1], 16) + ) + + return linux_path + + if platform.system() == "Darwin": + return hid_api_usb_path.decode("utf-8") + + return "" + + +class NXPUSBDeviceFilter(USBDeviceFilter): + """NXP Device Filtering class. + + Extension of the generic USB device filter class to support filtering + based on NXP devices. Modifies the way, how single number is handled. + By default, if single value is provided, it's content is expected to be VID. + However, legacy tooling were expecting PID, so from this perspective if + a single number is provided, we expect that VID is out of range NXP_VIDS. + """ + + NXP_VIDS = [0x1FC9, 0x15A2, 0x0471, 0x0D28] + + def __init__( + self, + usb_id: Optional[str] = None, + nxp_device_names: Optional[Dict[str, Tuple[int, int]]] = None, + ): + """Initialize the USB Device Filtering. + + :param usb_id: usb_id string + :param nxp_device_names: Dictionary holding NXP device vid/pid {"device_name": [vid(int), pid(int)]} + """ + super().__init__(usb_id=usb_id, search_by_pid=True) + self.nxp_device_names = nxp_device_names or {} + + def compare(self, usb_device_object: Any) -> bool: + """Compares the internal `usb_id` with provided `usb_device_object`. + + Extends the comparison by USB names - dictionary of device name and + corresponding VID/PID. + + :param usb_device_object: lpcusbsio USB HID device object + + :return: True on match, False otherwise + """ + vendor_id = usb_device_object["vendor_id"] + product_id = usb_device_object["product_id"] + + if self.usb_id: + if super().compare(usb_device_object=usb_device_object): + return True + + return self._is_nxp_device_name(vendor_id, product_id) + + return self._is_nxp_device(vendor_id) + + def _is_vid_or_pid(self, vid: Optional[int], pid: Optional[int]) -> bool: + if vid and vid in NXPUSBDeviceFilter.NXP_VIDS: + return super()._is_vid_or_pid(vid, pid) + + return False + + def _is_nxp_device_name(self, vid: int, pid: int) -> bool: + nxp_device_name_to_compare = {k.lower(): v for k, v in self.nxp_device_names.items()} + assert isinstance(self.usb_id, str) + if self.usb_id.lower() in nxp_device_name_to_compare: + vendor_id, product_id = nxp_device_name_to_compare[self.usb_id.lower()] + if vendor_id == vid and product_id == pid: + return True + return False + + @staticmethod + def _is_nxp_device(vid: int) -> bool: + return vid in NXPUSBDeviceFilter.NXP_VIDS diff --git a/pynitrokey/trussed/utils.py b/pynitrokey/trussed/utils.py index a4b0cc69..49fc1b40 100644 --- a/pynitrokey/trussed/utils.py +++ b/pynitrokey/trussed/utils.py @@ -12,7 +12,7 @@ from functools import total_ordering from typing import Optional, Sequence -from spsdk.sbfile.misc import BcdVersion3 +# from .bootloader.lpc55_upload.sbfile.misc import BcdVersion3 @dataclass(order=True, frozen=True) @@ -229,7 +229,7 @@ def from_v_str(cls, s: str) -> "Version": return Version.from_str(s[1:]) @classmethod - def from_bcd_version(cls, version: BcdVersion3) -> "Version": + def from_bcd_version(cls, version: any) -> "Version": return cls(major=version.major, minor=version.minor, patch=version.service) diff --git a/pyproject.toml b/pyproject.toml index f5a3525e..34682126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "python-dateutil ~= 2.7.0", "pyusb", "requests", - "spsdk >=2.0,<2.2", + # "spsdk >=2.0,<2.2", "tqdm", "tlv8", "typing_extensions ~= 4.3.0", @@ -40,7 +40,10 @@ dependencies = [ "protobuf >=3.17.3, < 4.0.0", "click-aliases", "semver", + "libusbsio", "nethsm >= 1.0.0,<2", + "asn1tools >= 0.166.0", + "gmssl >= 3.2, < 4", ] dynamic = ["version", "description"] @@ -138,6 +141,10 @@ module = [ "tlv8.*", "pytest.*", "click_aliases.*", + "sly.*", + "libusbsio.*", + "fastjsonschema", + "deepmerge.*", ] ignore_missing_imports = true From 7a16a4d81bf324bc06d7d916d25dd361063591d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 20 Mar 2024 17:43:50 +0100 Subject: [PATCH 03/11] Add README for vendored spsdk --- pynitrokey/trussed/bootloader/lpc55_upload/README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/README.md diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/README.md b/pynitrokey/trussed/bootloader/lpc55_upload/README.md new file mode 100644 index 00000000..189b86ca --- /dev/null +++ b/pynitrokey/trussed/bootloader/lpc55_upload/README.md @@ -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. + + From 5e92c6a6b08bea43c44f76be4c10f3565d29db05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 21 Mar 2024 10:30:28 +0100 Subject: [PATCH 04/11] Fix formatting --- pynitrokey/nk3/updates.py | 4 +- pynitrokey/trussed/bootloader/lpc55.py | 7 +- .../bootloader/lpc55_upload/__init__.py | 4 +- .../lpc55_upload/apps/utils/utils.py | 1 + .../lpc55_upload/crypto/certificate.py | 36 +- .../bootloader/lpc55_upload/crypto/cms.py | 16 +- .../bootloader/lpc55_upload/crypto/hash.py | 12 +- .../bootloader/lpc55_upload/crypto/hmac.py | 4 +- .../bootloader/lpc55_upload/crypto/keys.py | 107 +++-- .../bootloader/lpc55_upload/crypto/oscca.py | 10 +- .../lpc55_upload/crypto/signature_provider.py | 35 +- .../lpc55_upload/crypto/symmetric.py | 44 +- .../bootloader/lpc55_upload/crypto/types.py | 6 +- .../bootloader/lpc55_upload/crypto/utils.py | 16 +- .../bootloader/lpc55_upload/ele/ele_comm.py | 48 ++- .../lpc55_upload/ele/ele_constants.py | 138 +++++-- .../lpc55_upload/ele/ele_message.py | 108 +++-- .../image/ahab/ahab_abstract_interfaces.py | 28 +- .../lpc55_upload/image/ahab/ahab_container.py | 384 +++++++++++++----- .../lpc55_upload/image/ahab/signed_msg.py | 139 +++++-- .../lpc55_upload/image/ahab/utils.py | 11 +- .../bootloader/lpc55_upload/image/header.py | 8 +- .../bootloader/lpc55_upload/image/misc.py | 9 +- .../bootloader/lpc55_upload/image/secret.py | 39 +- .../bootloader/lpc55_upload/mboot/commands.py | 5 +- .../lpc55_upload/mboot/exceptions.py | 1 - .../lpc55_upload/mboot/interfaces/buspal.py | 74 +++- .../lpc55_upload/mboot/interfaces/sdio.py | 4 +- .../lpc55_upload/mboot/interfaces/uart.py | 3 +- .../lpc55_upload/mboot/interfaces/usbsio.py | 6 +- .../bootloader/lpc55_upload/mboot/mcuboot.py | 187 ++++++--- .../bootloader/lpc55_upload/mboot/memories.py | 26 +- .../lpc55_upload/mboot/properties.py | 42 +- .../mboot/protocol/serial_protocol.py | 12 +- .../bootloader/lpc55_upload/mboot/scanner.py | 14 +- .../bootloader/lpc55_upload/sbfile/misc.py | 4 +- .../lpc55_upload/sbfile/sb2/commands.py | 80 +++- .../lpc55_upload/sbfile/sb2/headers.py | 4 +- .../lpc55_upload/sbfile/sb2/images.py | 92 +++-- .../lpc55_upload/sbfile/sb2/sb_21_helper.py | 20 +- .../lpc55_upload/sbfile/sb2/sections.py | 32 +- .../lpc55_upload/sbfile/sb2/sly_bd_lexer.py | 3 +- .../lpc55_upload/sbfile/sb2/sly_bd_parser.py | 26 +- .../bootloader/lpc55_upload/uboot/uboot.py | 4 +- .../lpc55_upload/utils/crypto/cert_blocks.py | 232 ++++++++--- .../lpc55_upload/utils/crypto/iee.py | 65 ++- .../lpc55_upload/utils/crypto/otfad.py | 90 +++- .../lpc55_upload/utils/crypto/rkht.py | 8 +- .../lpc55_upload/utils/crypto/rot.py | 41 +- .../bootloader/lpc55_upload/utils/database.py | 62 ++- .../bootloader/lpc55_upload/utils/images.py | 20 +- .../utils/interfaces/device/sdio_device.py | 4 +- .../utils/interfaces/device/serial_device.py | 4 +- .../utils/interfaces/device/usb_device.py | 12 +- .../utils/interfaces/device/usbsio_device.py | 24 +- .../interfaces/protocol/protocol_base.py | 7 +- .../bootloader/lpc55_upload/utils/misc.py | 30 +- .../bootloader/lpc55_upload/utils/plugins.py | 4 +- .../lpc55_upload/utils/registers.py | 100 +++-- .../lpc55_upload/utils/schema_validator.py | 30 +- .../lpc55_upload/utils/spsdk_enum.py | 8 +- .../lpc55_upload/utils/usbfilter.py | 4 +- 62 files changed, 1956 insertions(+), 642 deletions(-) diff --git a/pynitrokey/nk3/updates.py b/pynitrokey/nk3/updates.py index e5edf1ce..1af1c045 100644 --- a/pynitrokey/nk3/updates.py +++ b/pynitrokey/nk3/updates.py @@ -16,8 +16,6 @@ from io import BytesIO from typing import Any, Callable, Iterator, List, Optional -from ..trussed.bootloader.lpc55_upload.mboot.exceptions import McuBootConnectionError - import pynitrokey from pynitrokey.helpers import Retries from pynitrokey.nk3 import NK3_DATA @@ -35,6 +33,8 @@ from pynitrokey.trussed.utils import Version from pynitrokey.updates import Asset, Release +from ..trussed.bootloader.lpc55_upload.mboot.exceptions import McuBootConnectionError + logger = logging.getLogger(__name__) diff --git a/pynitrokey/trussed/bootloader/lpc55.py b/pynitrokey/trussed/bootloader/lpc55.py index df47418f..c08b170d 100644 --- a/pynitrokey/trussed/bootloader/lpc55.py +++ b/pynitrokey/trussed/bootloader/lpc55.py @@ -13,6 +13,9 @@ import sys from typing import List, Optional, Tuple, TypeVar +from pynitrokey.trussed.utils import Uuid, Version + +from . import FirmwareMetadata, NitrokeyTrussedBootloader, ProgressCallback, Variant from .lpc55_upload.mboot.error_codes import StatusCode from .lpc55_upload.mboot.interfaces.usb import MbootUSBInterface from .lpc55_upload.mboot.mcuboot import McuBoot @@ -21,10 +24,6 @@ from .lpc55_upload.utils.interfaces.device.usb_device import UsbDevice from .lpc55_upload.utils.usbfilter import USBDeviceFilter -from pynitrokey.trussed.utils import Uuid, Version - -from . import FirmwareMetadata, NitrokeyTrussedBootloader, ProgressCallback, Variant - RKTH = bytes.fromhex("050aad3e77791a81e59c5b2ba5a158937e9460ee325d8ccba09734b8fdebb171") KEK = bytes([0xAA] * 32) UUID_LEN = 4 diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py index f68b4dd3..9a900c6f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py @@ -31,7 +31,9 @@ # SPSDK_CACHE_DISABLED might be redefined by SPSDK_CACHE_DISABLED_{version} env variable, default is False SPSDK_ENV_CACHE_DISABLED = "SPSDK_CACHE_DISABLED_" + version.replace(".", "_") SPSDK_CACHE_DISABLED = bool( - os.environ.get(SPSDK_ENV_CACHE_DISABLED) or os.environ.get("SPSDK_CACHE_DISABLED") or False + os.environ.get(SPSDK_ENV_CACHE_DISABLED) + or os.environ.get("SPSDK_CACHE_DISABLED") + or False ) SPSDK_YML_INDENT = 2 diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py index 921b7e59..e4fd9638 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py @@ -7,6 +7,7 @@ from typing import Dict + def filepath_from_config( config: Dict, key: str, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py index cfeb84ec..e58feb61 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/certificate.py @@ -68,7 +68,11 @@ def generate_certificate( :return: certificate """ before = datetime.utcnow() if duration else datetime(2000, 1, 1) - after = datetime.utcnow() + timedelta(days=duration) if duration else datetime(9999, 12, 31) + after = ( + datetime.utcnow() + timedelta(days=duration) + if duration + else datetime(9999, 12, 31) + ) crt = x509.CertificateBuilder( subject_name=subject, issuer_name=issuer, @@ -116,7 +120,9 @@ def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: if encoding == SPSDKEncoding.NXP: return align_block(self.export(SPSDKEncoding.DER), 4, "zeros") - return self.cert.public_bytes(SPSDKEncoding.get_cryptography_encodings(encoding)) + return self.cert.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding) + ) def get_public_key(self) -> PublicKey: """Get public keys from certificate. @@ -189,7 +195,9 @@ def validate_subject(self, subject_certificate: "Certificate") -> bool: return self.get_public_key().verify_signature( subject_certificate.signature, subject_certificate.tbs_certificate_bytes, - EnumHashAlgorithm.from_label(subject_certificate.signature_hash_algorithm.name), + EnumHashAlgorithm.from_label( + subject_certificate.signature_hash_algorithm.name + ), ) def validate(self, issuer_certificate: "Certificate") -> bool: @@ -212,7 +220,9 @@ def ca(self) -> bool: :return: true/false depending whether ca flag is set or not """ - extension = self.extensions.get_extension_for_oid(SPSDKExtensionOID.BASIC_CONSTRAINTS) + 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 @@ -225,7 +235,9 @@ def raw_size(self) -> int: """Raw size of the certificate.""" return len(self.export()) - def public_key_hash(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + def public_key_hash( + self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: """Get key hash. :param algorithm: Used hash algorithm, defaults to sha256 @@ -246,7 +258,9 @@ def __str__(self) -> str: 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" Signature Algorithm: {self.signature_hash_algorithm.name}\n" + ) nfo += f" Self Issued: {'YES' if self.self_signed else 'NO'}\n" return nfo @@ -273,7 +287,11 @@ def load_der_certificate(data: bytes) -> x509.Certificate: 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": + 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 @@ -357,7 +375,9 @@ def generate_extensions(config: dict) -> List[x509.ExtensionType]: if key == "BASIC_CONSTRAINTS": ca = bool(val["ca"]) extensions.append( - x509.BasicConstraints(ca=ca, path_length=val.get("path_length") if ca else None) + x509.BasicConstraints( + ca=ca, path_length=val.get("path_length") if ca else None + ) ) if key == "WPC_QIAUTH_POLICY": extensions.append(WPCQiAuthPolicy(value=val["value"])) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py index 8f7a9a79..2d2db420 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py @@ -47,7 +47,9 @@ def cms_sign( if not (signing_key or signature_provider): raise SPSDKValueError("Private key or signature provider is not present") if signing_key and signature_provider: - raise SPSDKValueError("Only one of private key and signature provider must be specified") + raise SPSDKValueError( + "Only one of private key and signature provider must be specified" + ) if signing_key and not isinstance(signing_key, (PrivateKeyEcc, PrivateKeyRsa)): raise SPSDKTypeError(f"Unsupported private key type {type(signing_key)}.") @@ -102,7 +104,9 @@ def cms_sign( cms.CMSAttribute( { "type": "signing_time", - "values": [cms.Time(name="utc_time", value=zulu.strftime("%y%m%d%H%M%SZ"))], + "values": [ + cms.Time(name="utc_time", value=zulu.strftime("%y%m%d%H%M%SZ")) + ], } ) ) @@ -145,10 +149,14 @@ def sign_data( """ assert signing_key or signature_provider if signing_key and signature_provider: - raise SPSDKValueError("Only one of private key and signature provider must be specified") + raise SPSDKValueError( + "Only one of private key and signature provider must be specified" + ) if signing_key: return ( - signing_key.sign(data_to_sign, algorithm=EnumHashAlgorithm.SHA256, der_format=True) + signing_key.sign( + data_to_sign, algorithm=EnumHashAlgorithm.SHA256, der_format=True + ) if isinstance(signing_key, PrivateKeyEcc) else signing_key.sign(data_to_sign) ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py index a401a4ac..546ab515 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hash.py @@ -36,7 +36,9 @@ def get_hash_algorithm(algorithm: EnumHashAlgorithm) -> hashes.HashAlgorithm: :return: instance of algorithm class :raises SPSDKError: If algorithm not found """ - algo_cls = getattr(hashes, algorithm.label.upper(), None) # hack: get class object by name + algo_cls = getattr( + hashes, algorithm.label.upper(), None + ) # hack: get class object by name if algo_cls is None: raise SPSDKError(f"Unsupported algorithm: hashes.{algorithm.label.upper()}") @@ -75,7 +77,9 @@ def update_int(self, value: int) -> None: :param value: Integer value to be hashed """ - data = value.to_bytes(length=ceil(value.bit_length() / 8), byteorder=Endianness.BIG.value) + data = value.to_bytes( + length=ceil(value.bit_length() / 8), byteorder=Endianness.BIG.value + ) self.update(data) def finalize(self) -> bytes: @@ -86,7 +90,9 @@ def finalize(self) -> bytes: return self.hash_obj.finalize() -def get_hash(data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: +def get_hash( + data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 +) -> bytes: """Return a HASH from input data with specified algorithm. :param data: Input data in bytes diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py index eaaf131c..338b7ee0 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/hmac.py @@ -15,7 +15,9 @@ from .hash import EnumHashAlgorithm, get_hash_algorithm -def hmac(key: bytes, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: +def hmac( + key: bytes, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 +) -> bytes: """Return a HMAC from data with specified key and algorithm. :param key: The key in bytes format diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py index f91a8a99..3986e7b5 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py @@ -37,7 +37,6 @@ from ..exceptions import SPSDKError, SPSDKNotImplementedError, SPSDKValueError from ..utils.abstract import BaseClass from ..utils.misc import Endianness, load_binary, write_file - from .hash import EnumHashAlgorithm, get_hash, get_hash_algorithm from .oscca import IS_OSCCA_SUPPORTED from .rng import rand_below, random_hex @@ -226,7 +225,10 @@ def verify_public_key(self, public_key: "PublicKey") -> bool: def __eq__(self, obj: Any) -> bool: """Check object equality.""" - return isinstance(obj, self.__class__) and self.get_public_key() == obj.get_public_key() + return ( + isinstance(obj, self.__class__) + and self.get_public_key() == obj.get_public_key() + ) def save( self, @@ -240,7 +242,9 @@ def save( :param password: password to private key; None to store without password :param encoding: encoding type, default is PEM """ - write_file(self.export(password=password, encoding=encoding), file_path, mode="wb") + write_file( + self.export(password=password, encoding=encoding), file_path, mode="wb" + ) @classmethod def load(cls, file_path: str, password: Optional[str] = None) -> Self: @@ -393,7 +397,9 @@ def parse(cls, data: bytes) -> Self: raise SPSDKError(f"Cannot load public key: ({str(exc)})") from exc raise SPSDKError(f"Unsupported public key: ({str(public_key)})") - def key_hash(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + def key_hash( + self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: """Get key hash. :param algorithm: Used hash algorithm, defaults to sha256 @@ -403,7 +409,10 @@ def key_hash(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> b def __eq__(self, obj: Any) -> bool: """Check object equality.""" - return isinstance(obj, self.__class__) and self.public_numbers == obj.public_numbers + return ( + isinstance(obj, self.__class__) + and self.public_numbers == obj.public_numbers + ) @classmethod def create(cls, key: Any) -> Self: @@ -513,7 +522,9 @@ def export( SPSDKEncoding.get_cryptography_encodings(encoding), PrivateFormat.PKCS8, enc ) - def sign(self, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> bytes: + def sign( + self, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: """Sign input data. :param data: Input data @@ -546,7 +557,9 @@ def __repr__(self) -> str: def __str__(self) -> str: """Object description in string format.""" - ret = f"RSA{self.key_size} Private key: \nd({hex(self.key.private_numbers().d)})" + ret = ( + f"RSA{self.key_size} Private key: \nd({hex(self.key.private_numbers().d)})" + ) return ret @@ -652,7 +665,10 @@ def verify_signature( def __eq__(self, obj: Any) -> bool: """Check object equality.""" - return isinstance(obj, self.__class__) and self.public_numbers == obj.public_numbers + return ( + isinstance(obj, self.__class__) + and self.public_numbers == obj.public_numbers + ) def __repr__(self) -> str: return f"RSA{self.key_size} Public Key" @@ -810,7 +826,9 @@ def exchange(self, peer_public_key: "PublicKeyEcc") -> bytes: :param peer_public_key: Peer public key :return: Shared key """ - return self.key.exchange(algorithm=ec.ECDH(), peer_public_key=peer_public_key.key) + return self.key.exchange( + algorithm=ec.ECDH(), peer_public_key=peer_public_key.key + ) def get_public_key(self) -> "PublicKeyEcc": """Generate public key. @@ -870,7 +888,9 @@ def sign( }[self.key.key_size] ) if prehashed: - signature_algorithm = ec.ECDSA(utils.Prehashed(get_hash_algorithm(hash_name))) + signature_algorithm = ec.ECDSA( + utils.Prehashed(get_hash_algorithm(hash_name)) + ) else: signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) signature = self.key.sign(data, signature_algorithm) @@ -973,14 +993,20 @@ def verify_signature( ) if prehashed: - signature_algorithm = ec.ECDSA(utils.Prehashed(get_hash_algorithm(hash_name))) + signature_algorithm = ec.ECDSA( + utils.Prehashed(get_hash_algorithm(hash_name)) + ) else: signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) if len(signature) == self.signature_size: der_signature = utils.encode_dss_signature( - int.from_bytes(signature[:coordinate_size], byteorder=Endianness.BIG.value), - int.from_bytes(signature[coordinate_size:], byteorder=Endianness.BIG.value), + int.from_bytes( + signature[:coordinate_size], byteorder=Endianness.BIG.value + ), + int.from_bytes( + signature[coordinate_size:], byteorder=Endianness.BIG.value + ), ) else: der_signature = signature @@ -1039,7 +1065,9 @@ def recreate_from_data(cls, data: bytes, curve: Optional[EccCurve] = None) -> Se :return: ECC public key. """ - def get_curve(data_length: int, curve: Optional[EccCurve] = None) -> Tuple[EccCurve, bool]: + def get_curve( + data_length: int, curve: Optional[EccCurve] = None + ) -> Tuple[EccCurve, bool]: curve_list = [curve] if curve else list(EccCurve) for cur in curve_list: curve_obj = KeyEccCommon._get_ec_curve_object(EccCurve(cur)) @@ -1051,7 +1079,9 @@ def get_curve(data_length: int, curve: Optional[EccCurve] = None) -> Tuple[EccCu curve_sign_size += 7 if curve_sign_size <= data_length <= curve_sign_size + 2: return (cur, True) - raise SPSDKUnsupportedEccCurve(f"Cannot recreate ECC curve with {data_length} length") + raise SPSDKUnsupportedEccCurve( + f"Cannot recreate ECC curve with {data_length} length" + ) data_length = len(data) (curve, der_format) = get_curve(data_length, curve) @@ -1062,8 +1092,12 @@ def get_curve(data_length: int, curve: Optional[EccCurve] = None) -> Tuple[EccCu return cls(der) coordinate_length = data_length // 2 - coor_x = int.from_bytes(data[:coordinate_length], byteorder=Endianness.BIG.value) - coor_y = int.from_bytes(data[coordinate_length:], byteorder=Endianness.BIG.value) + coor_x = int.from_bytes( + data[:coordinate_length], byteorder=Endianness.BIG.value + ) + coor_y = int.from_bytes( + data[coordinate_length:], byteorder=Endianness.BIG.value + ) return cls.recreate(coor_x=coor_x, coor_y=coor_y, curve=curve) @classmethod @@ -1137,7 +1171,9 @@ def get_public_key(self) -> "PublicKeySM2": :return: Public key """ - return PublicKeySM2(sm2.CryptSM2(private_key=None, public_key=self.key.public_key)) + return PublicKeySM2( + sm2.CryptSM2(private_key=None, public_key=self.key.public_key) + ) def verify_public_key(self, public_key: PublicKey) -> bool: """Verify public key. @@ -1147,7 +1183,9 @@ def verify_public_key(self, public_key: PublicKey) -> bool: """ return self.get_public_key() == public_key - def sign(self, data: bytes, salt: Optional[str] = None, use_ber: bool = False) -> bytes: + def sign( + self, data: bytes, salt: Optional[str] = None, use_ber: bool = False + ) -> bytes: """Sign data using SM2 algorithm with SM3 hash. :param data: Data to sign. @@ -1175,7 +1213,9 @@ def export( ) -> bytes: """Convert key into bytes supported by NXP.""" if encoding != SPSDKEncoding.DER: - raise SPSDKNotImplementedError("Only DER enocding is supported for SM2 keys export") + raise SPSDKNotImplementedError( + "Only DER enocding is supported for SM2 keys export" + ) keys = SM2KeySet(self.key.private_key, self.key.public_key) return SM2Encoder().encode_private_key(keys) @@ -1211,7 +1251,10 @@ def __init__(self, key: sm2.CryptSM2) -> None: self.key = key def verify_signature( - self, signature: bytes, data: bytes, algorithm: Optional[EnumHashAlgorithm] = None + self, + signature: bytes, + data: bytes, + algorithm: Optional[EnumHashAlgorithm] = None, ) -> bool: """Verify signature. @@ -1233,7 +1276,9 @@ def export(self, encoding: SPSDKEncoding = SPSDKEncoding.DER) -> bytes: :return: Byte representation of key """ if encoding != SPSDKEncoding.DER: - raise SPSDKNotImplementedError("Only DER enocding is supported for SM2 keys export") + raise SPSDKNotImplementedError( + "Only DER enocding is supported for SM2 keys export" + ) keys = SM2PublicKey(self.key.public_key) return SM2Encoder().encode_public_key(keys) @@ -1287,7 +1332,11 @@ def __str__(self) -> str: class ECDSASignature: """ECDSA Signature.""" - COORDINATE_LENGTHS = {EccCurve.SECP256R1: 32, EccCurve.SECP384R1: 48, EccCurve.SECP521R1: 66} + COORDINATE_LENGTHS = { + EccCurve.SECP256R1: 32, + EccCurve.SECP384R1: 48, + EccCurve.SECP521R1: 66, + } def __init__(self, r: int, s: int, ecc_curve: EccCurve) -> None: """ECDSA Signature constructor. @@ -1325,8 +1374,12 @@ def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: :return: Signature as bytes """ if encoding == SPSDKEncoding.NXP: - r_bytes = self.r.to_bytes(self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value) - s_bytes = self.s.to_bytes(self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value) + r_bytes = self.r.to_bytes( + self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value + ) + s_bytes = self.s.to_bytes( + self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value + ) return r_bytes + s_bytes if encoding == SPSDKEncoding.DER: return utils.encode_dss_signature(self.r, self.s) @@ -1417,5 +1470,7 @@ def get_ecc_curve(key_length: int) -> EccCurve: def prompt_for_passphrase() -> str: """Prompt interactively for private key passphrase.""" - password = getpass.getpass(prompt="Private key is encrypted. Enter password: ", stream=None) + password = getpass.getpass( + prompt="Private key is encrypted. Enter password: ", stream=None + ) return password diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py index d2df6b05..d315c043 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py @@ -8,8 +8,8 @@ """Support for OSCCA SM2/SM3.""" -from ..utils.misc import Endianness from .. import SPSDK_DATA_FOLDER_COMMON +from ..utils.misc import Endianness try: # this import is to find out whether OSCCA support is installed or not @@ -28,7 +28,9 @@ from ..exceptions import SPSDKError - OSCCA_ASN_DEFINITION_FILE = os.path.join(SPSDK_DATA_FOLDER_COMMON, "crypto", "oscca.asn") + OSCCA_ASN_DEFINITION_FILE = os.path.join( + SPSDK_DATA_FOLDER_COMMON, "crypto", "oscca.asn" + ) SM2_OID = "1.2.156.10197.1.301" class SM2KeySet(NamedTuple): @@ -76,7 +78,9 @@ def decode_private_key(self, data: bytes) -> SM2KeySet: """Parse private SM2 key set from binary data.""" result = self.parser.decode("Private", data) key_set = self.parser.decode("KeySet", result["keyset"]) - return SM2KeySet(private=key_set["prk"].hex(), public=key_set["puk"][0][1:].hex()) + return SM2KeySet( + private=key_set["prk"].hex(), public=key_set["puk"][0][1:].hex() + ) def decode_public_key(self, data: bytes) -> SM2PublicKey: """Parse public SM2 key set from binary data.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py index df3e3df7..eb9b5171 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py @@ -37,7 +37,12 @@ prompt_for_passphrase, ) from ..crypto.types import SPSDKEncoding -from ..exceptions import SPSDKError, SPSDKKeyError, SPSDKUnsupportedOperation, SPSDKValueError +from ..exceptions import ( + SPSDKError, + SPSDKKeyError, + SPSDKUnsupportedOperation, + SPSDKValueError, +) from ..utils.misc import find_file from ..utils.plugins import PluginsManager, PluginType @@ -141,7 +146,9 @@ def create(cls, params: Union[str, dict]) -> Optional["SignatureProvider"]: if isinstance(params, str): params = cls.convert_params(params) sp_classes = cls.get_all_signature_providers() - for klass in sp_classes: # pragma: no branch # there always be at least one subclass + for ( + klass + ) in sp_classes: # pragma: no branch # there always be at least one subclass if klass.sp_type == params["type"]: klass.filter_params(klass, params) return klass(**params) @@ -190,7 +197,9 @@ def __init__( self.private_key = PrivateKey.load(self.file_path, password=password) self.hash_alg = self._get_hash_algorithm(hash_alg) - def _get_hash_algorithm(self, hash_alg: Optional[EnumHashAlgorithm] = None) -> HashAlgorithm: + def _get_hash_algorithm( + self, hash_alg: Optional[EnumHashAlgorithm] = None + ) -> HashAlgorithm: if hash_alg: hash_alg_name = hash_alg else: @@ -331,7 +340,9 @@ def _handle_request(self, url: str, data: Optional[Dict] = None) -> Dict: except json.JSONDecodeError as e: raise SPSDKError("Response is not a valid JSON object") from e - def _check_response(self, response: Dict, names_types: List[Tuple[str, Type]]) -> None: + def _check_response( + self, response: Dict, names_types: List[Tuple[str, Type]] + ) -> None: """Check if the response contains required data. :param response: Response to check @@ -395,7 +406,9 @@ def get_signature_provider( raise SPSDKValueError("No signature provider configuration is provided") if not signature_provider: - raise SPSDKError(f"Cannot create signature provider from: {sp_cfg or local_file_key}") + raise SPSDKError( + f"Cannot create signature provider from: {sp_cfg or local_file_key}" + ) return signature_provider @@ -407,7 +420,9 @@ def load_plugins() -> Dict[str, ModuleType]: return plugins_manager.plugins -def try_to_verify_public_key(signature_provider: SignatureProvider, public_key_data: bytes) -> None: +def try_to_verify_public_key( + signature_provider: SignatureProvider, public_key_data: bytes +) -> None: """Verify public key by signature provider if verify method is implemented. :param signature_provider: Signature provider used for verification. @@ -421,6 +436,10 @@ def try_to_verify_public_key(signature_provider: SignatureProvider, public_key_d raise SPSDKKeysNotMatchingError( "Signature verification failed, public key does not match to private key" ) - logger.debug("The verification of private key pair integrity has been successful.") + logger.debug( + "The verification of private key pair integrity has been successful." + ) except SPSDKUnsupportedOperation: - logger.warning("Signature provider could not verify the integrity of private key pair.") + logger.warning( + "Signature provider could not verify the integrity of private key pair." + ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py index 64c7d1b8..2f3324f1 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/symmetric.py @@ -98,7 +98,9 @@ def aes_ecb_decrypt(key: bytes, encrypted_data: bytes) -> bytes: return enc.update(encrypted_data) + enc.finalize() -def aes_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None) -> bytes: +def aes_cbc_encrypt( + key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: """Encrypt plain data with AES in CBC mode. :param key: The key for data encryption @@ -114,7 +116,9 @@ def aes_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = No ) init_vector = iv_data or bytes(algorithms.AES.block_size // 8) if len(init_vector) * 8 != algorithms.AES.block_size: - raise SPSDKError(f"The initial vector length must be {algorithms.AES.block_size // 8}") + raise SPSDKError( + f"The initial vector length must be {algorithms.AES.block_size // 8}" + ) cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) enc = cipher.encryptor() return ( @@ -123,7 +127,9 @@ def aes_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = No ) -def aes_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None) -> bytes: +def aes_cbc_decrypt( + key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: """Decrypt encrypted data with AES in CBC mode. :param key: The key for data decryption @@ -139,7 +145,9 @@ def aes_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] ) init_vector = iv_data or bytes(algorithms.AES.block_size) if len(init_vector) * 8 != algorithms.AES.block_size: - raise SPSDKError(f"The initial vector length must be {algorithms.AES.block_size}") + raise SPSDKError( + f"The initial vector length must be {algorithms.AES.block_size}" + ) cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) dec = cipher.decryptor() return dec.update(encrypted_data) + dec.finalize() @@ -198,7 +206,11 @@ def aes_xts_decrypt(key: bytes, encrypted_data: bytes, tweak: bytes) -> bytes: def aes_ccm_encrypt( - key: bytes, plain_data: bytes, nonce: bytes, associated_data: bytes = b"", tag_len: int = 16 + key: bytes, + plain_data: bytes, + nonce: bytes, + associated_data: bytes = b"", + tag_len: int = 16, ) -> bytes: """Encrypt plain data with AES in CCM mode (Counter with CBC). @@ -214,7 +226,11 @@ def aes_ccm_encrypt( def aes_ccm_decrypt( - key: bytes, encrypted_data: bytes, nonce: bytes, associated_data: bytes, tag_len: int = 16 + key: bytes, + encrypted_data: bytes, + nonce: bytes, + associated_data: bytes, + tag_len: int = 16, ) -> bytes: """Decrypt encrypted data with AES in CCM mode (Counter with CBC). @@ -229,7 +245,9 @@ def aes_ccm_decrypt( return aesccm.decrypt(nonce, encrypted_data, associated_data) -def sm4_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None) -> bytes: +def sm4_cbc_encrypt( + key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: """Encrypt plain data with SM4 in CBC mode. :param key: The key for data encryption @@ -245,7 +263,9 @@ def sm4_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = No ) init_vector = iv_data or bytes(algorithms.SM4.block_size // 8) if len(init_vector) * 8 != algorithms.SM4.block_size: - raise SPSDKError(f"The initial vector length must be {algorithms.SM4.block_size // 8}") + raise SPSDKError( + f"The initial vector length must be {algorithms.SM4.block_size // 8}" + ) cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) enc = cipher.encryptor() return ( @@ -254,7 +274,9 @@ def sm4_cbc_encrypt(key: bytes, plain_data: bytes, iv_data: Optional[bytes] = No ) -def sm4_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None) -> bytes: +def sm4_cbc_decrypt( + key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: """Decrypt encrypted data with SM4 in CBC mode. :param key: The key for data decryption @@ -270,7 +292,9 @@ def sm4_cbc_decrypt(key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] ) init_vector = iv_data or bytes(algorithms.SM4.block_size) if len(init_vector) * 8 != algorithms.SM4.block_size: - raise SPSDKError(f"The initial vector length must be {algorithms.SM4.block_size}") + raise SPSDKError( + f"The initial vector length must be {algorithms.SM4.block_size}" + ) cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) dec = cipher.decryptor() return dec.update(encrypted_data) + dec.finalize() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py index 3e88af2f..225b9435 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py @@ -55,7 +55,11 @@ def get_file_encodings(data: bytes) -> "SPSDKEncoding": @staticmethod def all() -> Dict[str, "SPSDKEncoding"]: """Get all supported encodings.""" - return {"NXP": SPSDKEncoding.NXP, "PEM": SPSDKEncoding.PEM, "DER": SPSDKEncoding.DER} + return { + "NXP": SPSDKEncoding.NXP, + "PEM": SPSDKEncoding.PEM, + "DER": SPSDKEncoding.DER, + } SPSDKExtensions = Extensions diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py index 2e65946c..275af21b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/utils.py @@ -16,7 +16,9 @@ from ..utils.misc import load_binary -def get_matching_key_id(public_keys: List[PublicKey], signature_provider: SignatureProvider) -> int: +def get_matching_key_id( + public_keys: List[PublicKey], signature_provider: SignatureProvider +) -> int: """Get index of public key that match to given private key. :param public_keys: List of public key used to find the match for the private key. @@ -31,7 +33,9 @@ def get_matching_key_id(public_keys: List[PublicKey], signature_provider: Signat raise SPSDKValueError("There is no match of private key in given list.") -def extract_public_key_from_data(object_data: bytes, password: Optional[str] = None) -> PublicKey: +def extract_public_key_from_data( + object_data: bytes, password: Optional[str] = None +) -> PublicKey: """Extract any kind of public key from a data that contains Certificate, Private Key or Public Key. :raises SPSDKError: Raised when file can not be loaded @@ -56,7 +60,9 @@ def extract_public_key_from_data(object_data: bytes, password: Optional[str] = N def extract_public_key( - file_path: str, password: Optional[str] = None, search_paths: Optional[List[str]] = None + file_path: str, + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, ) -> PublicKey: """Extract any kind of public key from a file that contains Certificate, Private Key or Public Key. @@ -86,6 +92,8 @@ def extract_public_keys( :return: List of public keys of any type """ return [ - extract_public_key(file_path=source, password=password, search_paths=search_paths) + extract_public_key( + file_path=source, password=password, search_paths=search_paths + ) for source in secret_files ] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py index 763594d3..09914ec0 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py @@ -40,7 +40,9 @@ def __init__( self.database = get_db(device=family, revision=revision) self.family = family self.revision = revision - self.comm_buff_addr = self.database.get_int(DatabaseManager.COMM_BUFFER, "address") + self.comm_buff_addr = self.database.get_int( + DatabaseManager.COMM_BUFFER, "address" + ) self.comm_buff_size = self.database.get_int(DatabaseManager.COMM_BUFFER, "size") logger.info( f"ELE communicator is using {self.comm_buff_size} B size buffer at " @@ -128,12 +130,18 @@ def send_message(self, msg: EleMessage) -> None: if msg.response_words_count == 0: return # 3. Read back the response - response = self.device.read_memory(msg.response_address, 4 * msg.response_words_count) + response = self.device.read_memory( + msg.response_address, 4 * msg.response_words_count + ) except SPSDKError as exc: - raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + raise SPSDKError( + f"ELE Communication failed with mBoot: {str(exc)}" + ) from exc if not response or len(response) != 4 * msg.response_words_count: - raise SPSDKLengthError("ELE Message - Invalid response read-back operation.") + raise SPSDKLengthError( + "ELE Message - Invalid response read-back operation." + ) # 4. Decode the response msg.decode_response(response) @@ -148,10 +156,14 @@ def send_message(self, msg: EleMessage) -> None: msg.response_data_address, msg.response_data_size ) except SPSDKError as exc: - raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + raise SPSDKError( + f"ELE Communication failed with mBoot: {str(exc)}" + ) from exc if not response_data or len(response_data) != msg.response_data_size: - raise SPSDKLengthError("ELE Message - Invalid response data read-back operation.") + raise SPSDKLengthError( + "ELE Message - Invalid response data read-back operation." + ) msg.decode_response_data(response_data) @@ -213,7 +225,9 @@ def send_message(self, msg: EleMessage) -> None: msg.set_buffer_params(self.comm_buff_addr, self.comm_buff_size) try: - logger.debug(f"ELE msg {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}") + logger.debug( + f"ELE msg {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}" + ) # 0. Prepare command data in target memory if required if msg.has_command_data: @@ -230,18 +244,24 @@ def send_message(self, msg: EleMessage) -> None: return if "Error" in output: - msg.abort_code, msg.status, msg.indication = self.extract_error_values(output) + msg.abort_code, msg.status, msg.indication = self.extract_error_values( + output + ) else: # 2. Read back the response stripped_output = output.splitlines()[-1].replace("u-boot=> ", "") logger.debug(f"Stripped output {stripped_output}") response = value_to_bytes("0x" + stripped_output) except (SPSDKError, IndexError) as exc: - raise SPSDKError(f"ELE Communication failed with UBoot: {str(exc)}") from exc + raise SPSDKError( + f"ELE Communication failed with UBoot: {str(exc)}" + ) from exc if not "Error" in output: if not response or len(response) != 4 * msg.response_words_count: - raise SPSDKLengthError("ELE Message - Invalid response read-back operation.") + raise SPSDKLengthError( + "ELE Message - Invalid response read-back operation." + ) # 3. Decode the response msg.decode_response(response) @@ -257,10 +277,14 @@ def send_message(self, msg: EleMessage) -> None: ) self.device.read_output() except SPSDKError as exc: - raise SPSDKError(f"ELE Communication failed with mBoot: {str(exc)}") from exc + raise SPSDKError( + f"ELE Communication failed with mBoot: {str(exc)}" + ) from exc if not response_data or len(response_data) != msg.response_data_size: - raise SPSDKLengthError("ELE Message - Invalid response data read-back operation.") + raise SPSDKLengthError( + "ELE Message - Invalid response data read-back operation." + ) msg.decode_response_data(response_data) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py index b448ce51..23427186 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py @@ -16,11 +16,23 @@ class MessageIDs(SpsdkSoftEnum): PING_REQ = (0x01, "PING_REQ", "Ping request.") ELE_FW_AUTH_REQ = (0x02, "ELE_FW_AUTH_REQ", "ELE firmware authenticate request.") ELE_DUMP_DEBUG_BUFFER_REQ = (0x21, "ELE_DUMP_DEBUG_BUFFER_REQ", "Dump the ELE logs") - ELE_OEM_CNTN_AUTH_REQ = (0x87, "ELE_OEM_CNTN_AUTH_REQ", "OEM Container authenticate") + ELE_OEM_CNTN_AUTH_REQ = ( + 0x87, + "ELE_OEM_CNTN_AUTH_REQ", + "OEM Container authenticate", + ) ELE_VERIFY_IMAGE_REQ = (0x88, "ELE_VERIFY_IMAGE_REQ", "Verify Image") - ELE_RELEASE_CONTAINER_REQ = (0x89, "ELE_RELEASE_CONTAINER_REQ", "Release Container.") + ELE_RELEASE_CONTAINER_REQ = ( + 0x89, + "ELE_RELEASE_CONTAINER_REQ", + "Release Container.", + ) WRITE_SEC_FUSE_REQ = (0x91, "WRITE_SEC_FUSE_REQ", "Write secure fuse request.") - ELE_FWD_LIFECYCLE_UP_REQ = (0x95, "ELE_FWD_LIFECYCLE_UP_REQ", "Forward Lifecycle update") + ELE_FWD_LIFECYCLE_UP_REQ = ( + 0x95, + "ELE_FWD_LIFECYCLE_UP_REQ", + "Forward Lifecycle update", + ) READ_COMMON_FUSE = (0x97, "READ_COMMON_FUSE", "Read common fuse request.") GET_FW_VERSION_REQ = (0x9D, "GET_FW_VERSION_REQ", "Get firmware version request.") RETURN_LIFECYCLE_UPDATE_REQ = ( @@ -34,12 +46,20 @@ class MessageIDs(SpsdkSoftEnum): ELE_DERIVE_KEY_REQ = (0xA9, "ELE_DERIVE_KEY_REQ", "Derive key") GENERATE_KEY_BLOB_REQ = (0xAF, "GENERATE_KEY_BLOB_REQ", "Generate KeyBlob request.") GET_FW_STATUS_REQ = (0xC5, "GET_FW_STATUS_REQ", "Get ELE FW status request.") - ELE_ENABLE_APC_REQ = (0xD2, "ELE_ENABLE_APC_REQ", "Enable APC (Application processor)") + ELE_ENABLE_APC_REQ = ( + 0xD2, + "ELE_ENABLE_APC_REQ", + "Enable APC (Application processor)", + ) ELE_ENABLE_RTC_REQ = (0xD3, "ELE_ENABLE_RTC_REQ", "Enable RTC (Runtime processor)") GET_INFO_REQ = (0xDA, "GET_INFO_REQ", "Get ELE Information request.") ELE_RESET_APC_CTX_REQ = (0xD8, "ELE_RESET_APC_CTX_REQ", "Reset APC Context") START_RNG_REQ = (0xA3, "START_RNG_REQ", "Start True Random Generator request.") - GET_TRNG_STATE_REQ = (0xA3, "GET_TRNG_STATE_REQ", "Get True Random Generator state request.") + GET_TRNG_STATE_REQ = ( + 0xA3, + "GET_TRNG_STATE_REQ", + "Get True Random Generator state request.", + ) RESET_REQ = (0xC7, "RESET_REQ", "System reset request.") WRITE_FUSE = (0xD6, "WRITE_FUSE", "Write fuse") WRITE_SHADOW_FUSE = (0xF2, "WRITE_SHADOW_FUSE", "Write shadow fuse") @@ -92,22 +112,42 @@ class ResponseIndication(SpsdkSoftEnum): "ELE_UNALIGNED_PAYLOAD_FAILURE_IND", "Un-aligned payload failure", ) - ELE_WRONG_SIZE_FAILURE_IND = (0xA7, "ELE_WRONG_SIZE_FAILURE_IND", "Wrong size failure") - ELE_ENCRYPTION_FAILURE_IND = (0xA8, "ELE_ENCRYPTION_FAILURE_IND", "Encryption failure") - ELE_DECRYPTION_FAILURE_IND = (0xA9, "ELE_DECRYPTION_FAILURE_IND", "Decryption failure") + ELE_WRONG_SIZE_FAILURE_IND = ( + 0xA7, + "ELE_WRONG_SIZE_FAILURE_IND", + "Wrong size failure", + ) + ELE_ENCRYPTION_FAILURE_IND = ( + 0xA8, + "ELE_ENCRYPTION_FAILURE_IND", + "Encryption failure", + ) + ELE_DECRYPTION_FAILURE_IND = ( + 0xA9, + "ELE_DECRYPTION_FAILURE_IND", + "Decryption failure", + ) ELE_OTP_PROGFAIL_FAILURE_IND = ( 0xAA, "ELE_OTP_PROGFAIL_FAILURE_IND", "OTP program fail failure", ) - ELE_OTP_LOCKED_FAILURE_IND = (0xAB, "ELE_OTP_LOCKED_FAILURE_IND", "OTP locked failure") + ELE_OTP_LOCKED_FAILURE_IND = ( + 0xAB, + "ELE_OTP_LOCKED_FAILURE_IND", + "OTP locked failure", + ) ELE_OTP_INVALID_IDX_FAILURE_IND = ( 0xAD, "ELE_OTP_INVALID_IDX_FAILURE_IND", "OTP Invalid IDX failure", ) ELE_TIME_OUT_FAILURE_IND = (0xB0, "ELE_TIME_OUT_FAILURE_IND", "Timeout failure") - ELE_BAD_PAYLOAD_FAILURE_IND = (0xB1, "ELE_BAD_PAYLOAD_FAILURE_IND", "Bad payload failure") + ELE_BAD_PAYLOAD_FAILURE_IND = ( + 0xB1, + "ELE_BAD_PAYLOAD_FAILURE_IND", + "Bad payload failure", + ) ELE_WRONG_ADDRESS_FAILURE_IND = ( 0xB4, "ELE_WRONG_ADDRESS_FAILURE_IND", @@ -119,7 +159,11 @@ class ResponseIndication(SpsdkSoftEnum): "ELE_DISABLED_FEATURE_FAILURE_IND", "Disabled feature failure", ) - ELE_MUST_ATTEST_FAILURE_IND = (0xB7, "ELE_MUST_ATTEST_FAILURE_IND", "Must attest failure") + ELE_MUST_ATTEST_FAILURE_IND = ( + 0xB7, + "ELE_MUST_ATTEST_FAILURE_IND", + "Must attest failure", + ) ELE_RNG_NOT_STARTED_FAILURE_IND = ( 0xB8, "ELE_RNG_NOT_STARTED_FAILURE_IND", @@ -141,7 +185,11 @@ class ResponseIndication(SpsdkSoftEnum): "ELE_RNG_INST_FAILURE_IND", "Random number generator instantiation failure", ) - ELE_LOCKED_REG_FAILURE_IND = (0xBE, "ELE_LOCKED_REG_FAILURE_IND", "Locked register failure") + ELE_LOCKED_REG_FAILURE_IND = ( + 0xBE, + "ELE_LOCKED_REG_FAILURE_IND", + "Locked register failure", + ) ELE_BAD_ID_FAILURE_IND = (0xBF, "ELE_BAD_ID_FAILURE_IND", "Bad ID failure") ELE_INVALID_OPERATION_FAILURE_IND = ( 0xC0, @@ -189,7 +237,11 @@ class ResponseIndication(SpsdkSoftEnum): "ELE_WRONG_BOOT_MODE_FAILURE_IND", "Wrong boot mode failure", ) - ELE_OLD_VERSION_FAILURE_IND = (0xCE, "ELE_OLD_VERSION_FAILURE_IND", "Old version failure") + ELE_OLD_VERSION_FAILURE_IND = ( + 0xCE, + "ELE_OLD_VERSION_FAILURE_IND", + "Old version failure", + ) ELE_CSTM_FAILURE_IND = (0xCF, "ELE_CSTM_FAILURE_IND", "CSTM failure") ELE_CORRUPTED_SRK_FAILURE_IND = ( 0xD0, @@ -208,7 +260,11 @@ class ResponseIndication(SpsdkSoftEnum): "ELE_NO_AUTHENTICATION_FAILURE_IND", "No authentication failure", ) - ELE_BAD_SRK_SET_FAILURE_IND = (0xEF, "ELE_BAD_SRK_SET_FAILURE_IND", "Bad SRK set failure") + ELE_BAD_SRK_SET_FAILURE_IND = ( + 0xEF, + "ELE_BAD_SRK_SET_FAILURE_IND", + "Bad SRK set failure", + ) ELE_BAD_SIGNATURE_FAILURE_IND = ( 0xF0, "ELE_BAD_SIGNATURE_FAILURE_IND", @@ -227,13 +283,21 @@ class ResponseIndication(SpsdkSoftEnum): "Invalid message failure", ) ELE_BAD_VALUE_FAILURE_IND = (0xF5, "ELE_BAD_VALUE_FAILURE_IND", "Bad value failure") - ELE_BAD_FUSE_ID_FAILURE_IND = (0xF6, "ELE_BAD_FUSE_ID_FAILURE_IND", "Bad fuse ID failure") + ELE_BAD_FUSE_ID_FAILURE_IND = ( + 0xF6, + "ELE_BAD_FUSE_ID_FAILURE_IND", + "Bad fuse ID failure", + ) ELE_BAD_CONTAINER_FAILURE_IND = ( 0xF7, "ELE_BAD_CONTAINER_FAILURE_IND", "Bad container failure", ) - ELE_BAD_VERSION_FAILURE_IND = (0xF8, "ELE_BAD_VERSION_FAILURE_IND", "Bad version failure") + ELE_BAD_VERSION_FAILURE_IND = ( + 0xF8, + "ELE_BAD_VERSION_FAILURE_IND", + "Bad version failure", + ) ELE_INVALID_KEY_FAILURE_IND = ( 0xF9, "ELE_INVALID_KEY_FAILURE_IND", @@ -267,15 +331,27 @@ class EleFwStatus(SpsdkSoftEnum): """ELE Firmware status.""" ELE_FW_STATUS_NOT_IN_PLACE = (0, "ELE_FW_STATUS_NOT_IN_PLACE", "Not in place") - ELE_FW_STATUS_IN_PLACE = (1, "ELE_FW_STATUS_IN_PLACE", "Authenticated and operational") + ELE_FW_STATUS_IN_PLACE = ( + 1, + "ELE_FW_STATUS_IN_PLACE", + "Authenticated and operational", + ) class EleInfo2Commit(SpsdkSoftEnum): """ELE Information type to be committed.""" - NXP_SRK_REVOCATION = (0x1 << 0, "NXP_SRK_REVOCATION", "SRK revocation of the NXP container") + NXP_SRK_REVOCATION = ( + 0x1 << 0, + "NXP_SRK_REVOCATION", + "SRK revocation of the NXP container", + ) NXP_FW_FUSE = (0x1 << 1, "NXP_FW_FUSE", "FW fuse version of the NXP container") - OEM_SRK_REVOCATION = (0x1 << 4, "OEM_SRK_REVOCATION", "SRK revocation of the OEM container") + OEM_SRK_REVOCATION = ( + 0x1 << 4, + "OEM_SRK_REVOCATION", + "SRK revocation of the OEM container", + ) OEM_FW_FUSE = (0x1 << 5, "OEM_FW_FUSE", "FW fuse version of the OEM container") @@ -310,8 +386,16 @@ class EleTrngState(SpsdkSoftEnum): "ELE_TRNG_GENERATING_ENTROPY", "TRNG is still generating entropy", ) - ELE_TRNG_READY = (0x3, "ELE_TRNG_READY", "TRNG entropy is valid and ready to be read") - ELE_TRNG_ERROR = (0x4, "ELE_TRNG_ERROR", "TRNG encounter an error while generating entropy") + ELE_TRNG_READY = ( + 0x3, + "ELE_TRNG_READY", + "TRNG entropy is valid and ready to be read", + ) + ELE_TRNG_ERROR = ( + 0x4, + "ELE_TRNG_ERROR", + "TRNG encounter an error while generating entropy", + ) class EleCsalState(SpsdkSoftEnum): @@ -327,8 +411,16 @@ class EleCsalState(SpsdkSoftEnum): "ELE_CSAL_ON_GOING", "Crypto Lib random context initialization is on-going", ) - ELE_CSAL_READY = (0x2, "ELE_CSAL_READY", "Crypto Lib random context initialization succeed") - ELE_CSAL_ERROR = (0x3, "ELE_CSAL_ERROR", "Crypto Lib random context initialization failed") + ELE_CSAL_READY = ( + 0x2, + "ELE_CSAL_READY", + "Crypto Lib random context initialization succeed", + ) + ELE_CSAL_ERROR = ( + 0x3, + "ELE_CSAL_ERROR", + "Crypto Lib random context initialization failed", + ) ELE_CSAL_PAUSE = ( 0x4, "ELE_CSAL_PAUSE", diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py index 4e016274..09cfd6aa 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py @@ -90,12 +90,16 @@ def has_command_data(self) -> bool: @property def command_data_address(self) -> int: """Command data address in target memory space.""" - return align(self.command_address + self.command_words_count * 4, self.ELE_MSG_ALIGN) + return align( + self.command_address + self.command_words_count * 4, self.ELE_MSG_ALIGN + ) @property def command_data_size(self) -> int: """Command data address in target memory space.""" - return align(len(self.command_data) or self.MAX_COMMAND_DATA_SIZE, self.ELE_MSG_ALIGN) + return align( + len(self.command_data) or self.MAX_COMMAND_DATA_SIZE, self.ELE_MSG_ALIGN + ) @property def command_data(self) -> bytes: @@ -124,7 +128,9 @@ def has_response_data(self) -> bool: @property def response_data_address(self) -> int: """Response data address in target memory space.""" - return align(self.response_address + self.response_words_count * 4, self.ELE_MSG_ALIGN) + return align( + self.response_address + self.response_words_count * 4, self.ELE_MSG_ALIGN + ) @property def response_data_size(self) -> int: @@ -134,13 +140,16 @@ def response_data_size(self) -> int: @property def free_space_address(self) -> int: """First free address after ele message in target memory space.""" - return align(self.response_data_address + self._response_data_size, self.ELE_MSG_ALIGN) + return align( + self.response_data_address + self._response_data_size, self.ELE_MSG_ALIGN + ) @property def free_space_size(self) -> int: """Free space size after ele message in target memory space.""" return align( - self.buff_size - (self.free_space_address - self.buff_addr), self.ELE_MSG_ALIGN + self.buff_size - (self.free_space_address - self.buff_addr), + self.ELE_MSG_ALIGN, ) @property @@ -195,7 +204,11 @@ def header_export( :return: Bytes representation of message header. """ return pack( - self.HEADER_FORMAT, self.VERSION, self.command_words_count, self.command, self.TAG + self.HEADER_FORMAT, + self.VERSION, + self.command_words_count, + self.command, + self.TAG, ) def export( @@ -218,11 +231,15 @@ def decode_response(self, response: bytes) -> None: if tag != self.RSP_TAG: raise SPSDKParsingError(f"Message TAG in response is invalid: {hex(tag)}") if command != self.command: - raise SPSDKParsingError(f"Message COMMAND in response is invalid: {hex(command)}") + raise SPSDKParsingError( + f"Message COMMAND in response is invalid: {hex(command)}" + ) if size not in [self.response_words_count, self.RESPONSE_HEADER_WORDS_COUNT]: raise SPSDKParsingError(f"Message SIZE in response is invalid: {hex(size)}") if version != self.VERSION: - raise SPSDKParsingError(f"Message VERSION in response is invalid: {hex(version)}") + raise SPSDKParsingError( + f"Message VERSION in response is invalid: {hex(version)}" + ) # Decode status word ( @@ -362,7 +379,10 @@ def export(self) -> bytes: """ ret = self.header_export() ret += pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, self.ele_fw_address, 0, self.ele_fw_address + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, + self.ele_fw_address, + 0, + self.ele_fw_address, ) return ret @@ -478,7 +498,9 @@ def export(self) -> bytes: :return: Bytes representation of message object. """ ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, self.lifecycle_update.tag, 0, 0) + ret += pack( + LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, self.lifecycle_update.tag, 0, 0 + ) return ret @@ -525,7 +547,9 @@ def decode_response(self, response: bytes) -> None: LITTLE_ENDIAN + UINT16 + UINT16 + "8L4s", response[8:48] ) if max_events != self.MAX_EVENT_CNT: - logger.error(f"Invalid maximal events count: {max_events}!={self.MAX_EVENT_CNT}") + logger.error( + f"Invalid maximal events count: {max_events}!={self.MAX_EVENT_CNT}" + ) crc_computed = self.get_msg_crc(response[0:44]) if crc != crc_computed: @@ -547,18 +571,25 @@ def get_cmd(event: int) -> str: def get_ind(event: int) -> str: """Get Indication in string from event.""" ind = (event >> 8) & 0xFF - return ResponseIndication.get_description(ind, f"Unknown Indication: (0x{ind:02})") or "" + return ( + ResponseIndication.get_description(ind, f"Unknown Indication: (0x{ind:02})") + or "" + ) @staticmethod def get_sts(event: int) -> str: """Get Status in string from event.""" sts = event & 0xFF - return ResponseStatus.get_description(sts, f"Unknown Status: (0x{sts:02})") or "" + return ( + ResponseStatus.get_description(sts, f"Unknown Status: (0x{sts:02})") or "" + ) def response_info(self) -> str: """Print events info.""" ret = f"Event count: {self.event_cnt}" - for i, event in enumerate(self.events[: min(self.event_cnt, self.MAX_EVENT_CNT)]): + for i, event in enumerate( + self.events[: min(self.event_cnt, self.MAX_EVENT_CNT)] + ): ret += f"\nEvent[{i}]: 0x{event:08X}" ret += f"\n IPC ID: {self.get_ipc_id(event)}" ret += f"\n Command: {self.get_cmd(event)}" @@ -710,8 +741,12 @@ def decode_response(self, response: bytes) -> None: :raises SPSDKParsingError: Response parse detect some error. """ super().decode_response(response) - self.ele_fw_version_raw = int.from_bytes(response[8:12], Endianness.LITTLE.value) - self.ele_fw_version_sha1 = int.from_bytes(response[12:16], Endianness.LITTLE.value) + self.ele_fw_version_raw = int.from_bytes( + response[8:12], Endianness.LITTLE.value + ) + self.ele_fw_version_sha1 = int.from_bytes( + response[12:16], Endianness.LITTLE.value + ) def response_info(self) -> str: """Print specific information of ELE. @@ -849,7 +884,12 @@ def decode_response_data(self, response_data: bytes) -> None: if self.info_version == 0x02: self.info_oem_srkh = response_data[92:156] self.info_oem_srkh = response_data[92:156] - (self.info_trng_state, self.info_csal_state, self.info_imem_state, _) = unpack( + ( + self.info_trng_state, + self.info_csal_state, + self.info_imem_state, + _, + ) = unpack( LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, response_data[156:160] ) @@ -1031,7 +1071,14 @@ def export(self) -> bytes: :return: Bytes representation of message object. """ payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32 + UINT32 + UINT32 + UINT16 + UINT16, + LITTLE_ENDIAN + + UINT32 + + UINT32 + + UINT32 + + UINT32 + + UINT32 + + UINT16 + + UINT16, self.key_id, 0, self.command_data_address, @@ -1099,7 +1146,9 @@ def decode_response_data(self, response_data: bytes) -> None: :param response_data: Data of response. :raises SPSDKParsingError: Invalid response detected. """ - ver, length, tag = unpack(LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, response_data[:4]) + ver, length, tag = unpack( + LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, response_data[:4] + ) if tag != self.KEYBLOB_TAG: raise SPSDKParsingError("Invalid TAG in generated KeyBlob") if ver != self.KEYBLOB_VERSION: @@ -1198,14 +1247,18 @@ def validate(self) -> None: ) if reserved != 0: - raise SPSDKValueError("Invalid OTFAD Key Identifier. Byte 2-3 must be set to 0.") + raise SPSDKValueError( + "Invalid OTFAD Key Identifier. Byte 2-3 must be set to 0." + ) # 2. validate AES counter if len(self.aes_counter) != 8: raise SPSDKValueError("Invalid AES counter length. It must be 64 bits.") # 3. start address - if self.start_address != 0 and self.start_address != align(self.start_address, 1024): + if self.start_address != 0 and self.start_address != align( + self.start_address, 1024 + ): raise SPSDKValueError( "Invalid OTFAD start address. Start address has to be aligned to 1024 bytes." ) @@ -1250,7 +1303,9 @@ def command_data(self) -> bytes: ) crc32_function = mkPredefinedCrcFun("crc-32-mpeg") crc: int = crc32_function(otfad_config) - return header + options + otfad_config + crc.to_bytes(4, Endianness.LITTLE.value) + return ( + header + options + otfad_config + crc.to_bytes(4, Endianness.LITTLE.value) + ) def info(self) -> str: """Print information including live data. @@ -1413,7 +1468,10 @@ def export(self) -> bytes: :return: Bytes representation of message object. """ payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, self.key_id, 0, self.command_data_address + LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, + self.key_id, + 0, + self.command_data_address, ) payload = self.header_export() + payload return payload @@ -1441,7 +1499,9 @@ class EleMessageWriteFuse(EleMessage): CMD = MessageIDs.WRITE_FUSE.tag COMMAND_PAYLOAD_WORDS_COUNT = 2 - def __init__(self, bit_position: int, bit_length: int, lock: bool, payload: int) -> None: + def __init__( + self, bit_position: int, bit_length: int, lock: bool, payload: int + ) -> None: """Constructor. This command allows to write to the fuses. diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py index 43f76958..0e7356a2 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py @@ -47,16 +47,22 @@ def __repr__(self) -> str: return "Base AHAB Container class: " + self.__class__.__name__ def __str__(self) -> str: - raise NotImplementedError("__str__() is not implemented in base AHAB container class") + raise NotImplementedError( + "__str__() is not implemented in base AHAB container class" + ) def export(self) -> bytes: """Serialize object into bytes array.""" - raise NotImplementedError("export() is not implemented in base AHAB container class") + raise NotImplementedError( + "export() is not implemented in base AHAB container class" + ) @classmethod def parse(cls, data: bytes) -> Self: """Deserialize object from bytes array.""" - raise NotImplementedError("parse() is not implemented in base AHAB container class") + raise NotImplementedError( + "parse() is not implemented in base AHAB container class" + ) @classmethod def format(cls) -> str: @@ -132,11 +138,17 @@ def validate_header(self) -> None: :raises SPSDKValueError: Any MAndatory field has invalid value. """ if self.tag is None or not check_range(self.tag, end=0xFF): - raise SPSDKValueError(f"AHAB: Head of Container: Invalid TAG Value: {self.tag}") + raise SPSDKValueError( + f"AHAB: Head of Container: Invalid TAG Value: {self.tag}" + ) if self.length is None or not check_range(self.length, end=0xFFFF): - raise SPSDKValueError(f"AHAB: Head of Container: Invalid Length Value: {self.length}") + raise SPSDKValueError( + f"AHAB: Head of Container: Invalid Length Value: {self.length}" + ) if self.version is None or not check_range(self.version, end=0xFF): - raise SPSDKValueError(f"AHAB: Head of Container: Invalid Version Value: {self.version}") + raise SPSDKValueError( + f"AHAB: Head of Container: Invalid Version Value: {self.version}" + ) @classmethod def parse_head(cls, binary: bytes) -> Tuple[int, int, int]: @@ -166,7 +178,9 @@ def check_container_head(cls, binary: bytes) -> None: """ cls._check_fixed_input_length(binary) data_len = len(binary) - (tag, length, version) = cls.parse_head(binary[: HeaderContainer.fixed_length()]) + (tag, length, version) = cls.parse_head( + binary[: HeaderContainer.fixed_length()] + ) if ( isinstance(cls.TAG, int) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py index abcbac85..487addf4 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py @@ -39,7 +39,12 @@ from ...crypto.types import SPSDKEncoding from ...crypto.utils import extract_public_key, get_matching_key_id from ...ele.ele_constants import KeyBlobEncryptionAlgorithm -from ...exceptions import SPSDKError, SPSDKLengthError, SPSDKParsingError, SPSDKValueError +from ...exceptions import ( + SPSDKError, + SPSDKLengthError, + SPSDKParsingError, + SPSDKValueError, +) from ...image.ahab.ahab_abstract_interfaces import ( Container, HeaderContainer, @@ -384,10 +389,14 @@ def update_fields(self) -> None: padding=0, ) if not self.image_iv and self.flags_is_encrypted: - self.image_iv = get_hash(self.plain_image, algorithm=EnumHashAlgorithm.SHA256) + self.image_iv = get_hash( + self.plain_image, algorithm=EnumHashAlgorithm.SHA256 + ) @staticmethod - def create_meta(start_cpu_id: int = 0, mu_cpu_id: int = 0, start_partition_id: int = 0) -> int: + def create_meta( + start_cpu_id: int = 0, mu_cpu_id: int = 0, start_partition_id: int = 0 + ) -> int: """Create meta data field. :param start_cpu_id: ID of CPU to start, defaults to 0 @@ -425,7 +434,9 @@ def create_flags( EnumHashAlgorithm.SHA512: 0x2, EnumHashAlgorithm.SM3: 0x3, }[hash_type] << ImageArrayEntry.FLAGS_HASH_OFFSET - flags_data |= 1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET if is_encrypted else 0 + flags_data |= ( + 1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET if is_encrypted else 0 + ) flags_data |= boot_flags << ImageArrayEntry.FLAGS_BOOT_FLAGS_OFFSET return flags_data @@ -498,9 +509,9 @@ def metadata_start_cpu_id(self) -> int: :return: Start CPU ID """ - return (self.image_meta_data >> ImageArrayEntry.METADATA_START_CPU_ID_OFFSET) & ( - (1 << ImageArrayEntry.METADATA_START_CPU_ID_SIZE) - 1 - ) + return ( + self.image_meta_data >> ImageArrayEntry.METADATA_START_CPU_ID_OFFSET + ) & ((1 << ImageArrayEntry.METADATA_START_CPU_ID_SIZE) - 1) @property def metadata_mu_cpu_id(self) -> int: @@ -518,9 +529,9 @@ def metadata_start_partition_id(self) -> int: :return: Start Partition ID """ - return (self.image_meta_data >> ImageArrayEntry.METADATA_START_PARTITION_ID_OFFSET) & ( - (1 << ImageArrayEntry.METADATA_START_PARTITION_ID_SIZE) - 1 - ) + return ( + self.image_meta_data >> ImageArrayEntry.METADATA_START_PARTITION_ID_OFFSET + ) & ((1 << ImageArrayEntry.METADATA_START_PARTITION_ID_SIZE) - 1) def export(self) -> bytes: """Serializes container object into bytes in little endian. @@ -554,18 +565,36 @@ def validate(self) -> None: """ if self.image is None or self._get_valid_size(self.image) != self.image_size: raise SPSDKValueError("Image Entry: Invalid Image binary.") - if self.image_offset is None or not check_range(self.image_offset, end=(1 << 32) - 1): - raise SPSDKValueError(f"Image Entry: Invalid Image Offset: {self.image_offset}") - if self.image_size is None or not check_range(self.image_size, end=(1 << 32) - 1): + if self.image_offset is None or not check_range( + self.image_offset, end=(1 << 32) - 1 + ): + raise SPSDKValueError( + f"Image Entry: Invalid Image Offset: {self.image_offset}" + ) + if self.image_size is None or not check_range( + self.image_size, end=(1 << 32) - 1 + ): raise SPSDKValueError(f"Image Entry: Invalid Image Size: {self.image_size}") - if self.load_address is None or not check_range(self.load_address, end=(1 << 64) - 1): - raise SPSDKValueError(f"Image Entry: Invalid Image Load address: {self.load_address}") - if self.entry_point is None or not check_range(self.entry_point, end=(1 << 64) - 1): - raise SPSDKValueError(f"Image Entry: Invalid Image Entry point: {self.entry_point}") + if self.load_address is None or not check_range( + self.load_address, end=(1 << 64) - 1 + ): + raise SPSDKValueError( + f"Image Entry: Invalid Image Load address: {self.load_address}" + ) + if self.entry_point is None or not check_range( + self.entry_point, end=(1 << 64) - 1 + ): + raise SPSDKValueError( + f"Image Entry: Invalid Image Entry point: {self.entry_point}" + ) if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): raise SPSDKValueError(f"Image Entry: Invalid Image Flags: {self.flags}") - if self.image_meta_data is None or not check_range(self.image_meta_data, end=(1 << 32) - 1): - raise SPSDKValueError(f"Image Entry: Invalid Image Meta data: {self.image_meta_data}") + if self.image_meta_data is None or not check_range( + self.image_meta_data, end=(1 << 32) - 1 + ): + raise SPSDKValueError( + f"Image Entry: Invalid Image Meta data: {self.image_meta_data}" + ) if ( self.image_hash is None or not any(self.image_hash) @@ -634,7 +663,9 @@ def parse(cls, data: bytes, parent: "AHABContainer") -> Self: # type: ignore # f"Image entry record has end of image at {hex(iae.image_offset + image_size - iae_offset)}," f" but the loaded image length has only {hex(binary_size)}B size." ) - image = data[iae.image_offset - iae_offset : iae.image_offset - iae_offset + image_size] + image = data[ + iae.image_offset - iae_offset : iae.image_offset - iae_offset + image_size + ] image_hash_cmp = extend_block( get_hash(image, algorithm=ImageArrayEntry.get_hash_from_flags(flags)), ImageArrayEntry.HASH_LEN, @@ -646,7 +677,9 @@ def parse(cls, data: bytes, parent: "AHABContainer") -> Self: # type: ignore # return iae @staticmethod - def load_from_config(parent: "AHABContainer", config: Dict[str, Any]) -> "ImageArrayEntry": + def load_from_config( + parent: "AHABContainer", config: Dict[str, Any] + ) -> "ImageArrayEntry": """Converts the configuration option into an AHAB image array entry object. "config" content of container configurations. @@ -683,7 +716,9 @@ def load_from_config(parent: "AHABContainer", config: Dict[str, Any]) -> "ImageA image_iv=None, # IV data are updated by UpdateFields function ) - def create_config(self, index: int, image_index: int, data_path: str) -> Dict[str, Any]: + def create_config( + self, index: int, image_index: int, data_path: str + ) -> Dict[str, Any]: """Create configuration of the AHAB Image data blob. :param index: Container index. @@ -694,13 +729,17 @@ def create_config(self, index: int, image_index: int, data_path: str) -> Dict[st ret_cfg: Dict[str, Union[str, int, bool]] = {} image_name = "N/A" if self.plain_image: - image_name = f"container{index}_image{image_index}_{self.flags_image_type}.bin" + image_name = ( + f"container{index}_image{image_index}_{self.flags_image_type}.bin" + ) write_file(self.plain_image, os.path.join(data_path, image_name), "wb") if self.encrypted_image: - image_name_encrypted = ( - f"container{index}_image{image_index}_{self.flags_image_type}_encrypted.bin" + image_name_encrypted = f"container{index}_image{image_index}_{self.flags_image_type}_encrypted.bin" + write_file( + self.encrypted_image, + os.path.join(data_path, image_name_encrypted), + "wb", ) - write_file(self.encrypted_image, os.path.join(data_path, image_name_encrypted), "wb") if image_name == "N/A": image_name = image_name_encrypted @@ -709,8 +748,12 @@ def create_config(self, index: int, image_index: int, data_path: str) -> Dict[st ret_cfg["load_address"] = hex(self.load_address) ret_cfg["entry_point"] = hex(self.entry_point) ret_cfg["image_type"] = self.flags_image_type - core_ids = self.parent.parent._database.get_dict(DatabaseManager.AHAB, "core_ids") - ret_cfg["core_id"] = core_ids.get(self.flags_core_id, f"Unknown ID: {self.flags_core_id}") + core_ids = self.parent.parent._database.get_dict( + DatabaseManager.AHAB, "core_ids" + ) + ret_cfg["core_id"] = core_ids.get( + self.flags_core_id, f"Unknown ID: {self.flags_core_id}" + ) ret_cfg["is_encrypted"] = bool(self.flags_is_encrypted) ret_cfg["boot_flags"] = self.flags_boot_flags ret_cfg["meta_data_start_cpu_id"] = self.metadata_start_cpu_id @@ -790,7 +833,11 @@ class SRKRecord(HeaderContainerInversed): EnumHashAlgorithm.SHA512: 0x2, EnumHashAlgorithm.SM3: 0x3, } - ECC_KEY_TYPE = {EccCurve.SECP521R1: 0x3, EccCurve.SECP384R1: 0x2, EccCurve.SECP256R1: 0x1} + ECC_KEY_TYPE = { + EccCurve.SECP521R1: 0x3, + EccCurve.SECP384R1: 0x2, + EccCurve.SECP256R1: 0x1, + } RSA_KEY_TYPE = {2048: 0x5, 4096: 0x7} SM2_KEY_TYPE = 0x8 KEY_SIZES = { @@ -913,7 +960,9 @@ def validate(self) -> None: """ self.validate_header() if self.hash_algorithm is None or not check_range(self.hash_algorithm, end=2): - raise SPSDKValueError(f"SRK record: Invalid Hash algorithm: {self.hash_algorithm}") + raise SPSDKValueError( + f"SRK record: Invalid Hash algorithm: {self.hash_algorithm}" + ) if self.srk_flags is None or not check_range(self.srk_flags, end=0xFF): raise SPSDKValueError(f"SRK record: Invalid Flags: {self.srk_flags}") @@ -934,7 +983,9 @@ def validate(self) -> None: f"SRK record: Invalid Key size in match to SM2 signing algorithm: {self.key_size}" ) else: - raise SPSDKValueError(f"SRK record: Invalid Signing algorithm: {self.version}") + raise SPSDKValueError( + f"SRK record: Invalid Signing algorithm: {self.version}" + ) # Check lengths @@ -984,10 +1035,12 @@ def create_from_key(public_key: PublicKey, srk_flags: int = 0) -> "SRKRecord": key_size=key_size, srk_flags=srk_flags, crypto_param1=par_n.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][0], byteorder=Endianness.BIG.value + length=SRKRecord.KEY_SIZES[key_size][0], + byteorder=Endianness.BIG.value, ), crypto_param2=par_e.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][1], byteorder=Endianness.BIG.value + length=SRKRecord.KEY_SIZES[key_size][1], + byteorder=Endianness.BIG.value, ), ) @@ -1012,16 +1065,24 @@ def create_from_key(public_key: PublicKey, srk_flags: int = 0) -> "SRKRecord": key_size=key_size, srk_flags=srk_flags, crypto_param1=par_x.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][0], byteorder=Endianness.BIG.value + length=SRKRecord.KEY_SIZES[key_size][0], + byteorder=Endianness.BIG.value, ), crypto_param2=par_y.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][1], byteorder=Endianness.BIG.value + length=SRKRecord.KEY_SIZES[key_size][1], + byteorder=Endianness.BIG.value, ), ) - assert isinstance(public_key, PublicKeySM2), "Unsupported public key for SRK record" - param1: bytes = value_to_bytes("0x" + public_key.public_numbers[:64], byte_cnt=32) - param2: bytes = value_to_bytes("0x" + public_key.public_numbers[64:], byte_cnt=32) + assert isinstance( + public_key, PublicKeySM2 + ), "Unsupported public key for SRK record" + param1: bytes = value_to_bytes( + "0x" + public_key.public_numbers[:64], byte_cnt=32 + ) + param2: bytes = value_to_bytes( + "0x" + public_key.public_numbers[64:], byte_cnt=32 + ) assert len(param1 + param2) == 64, "Invalid length of the SM2 key" key_size = SRKRecord.SM2_KEY_TYPE return SRKRecord( @@ -1077,7 +1138,9 @@ def parse(cls, data: bytes) -> Self: ] return cls( - signing_algorithm=get_key_by_val(SRKRecord.VERSION_ALGORITHMS, signing_algo), + signing_algorithm=get_key_by_val( + SRKRecord.VERSION_ALGORITHMS, signing_algo + ), hash_type=get_key_by_val(SRKRecord.HASH_ALGORITHM, hash_algo), key_size=key_size_curve, srk_flags=srk_flags, @@ -1114,7 +1177,10 @@ def get_public_key(self, encoding: SPSDKEncoding = SPSDKEncoding.PEM) -> bytes: # ECDSA Key to store curve = get_key_by_val(self.ECC_KEY_TYPE, self.key_size) key = PublicKeyEcc.recreate(par1, par2, curve=curve) - elif get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2" and IS_OSCCA_SUPPORTED: + elif ( + get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2" + and IS_OSCCA_SUPPORTED + ): encoding = SPSDKEncoding.DER key = PublicKeySM2.recreate(self.crypto_param1 + self.crypto_param2) @@ -1249,7 +1315,9 @@ def validate(self, data: Dict[str, Any]) -> None: """ self.validate_header() if self._srk_records is None or len(self._srk_records) != self.SRK_RECORDS_CNT: - raise SPSDKValueError(f"SRK table: Invalid SRK records: {self._srk_records}") + raise SPSDKValueError( + f"SRK table: Invalid SRK records: {self._srk_records}" + ) # Validate individual SRK records for srk_rec in self._srk_records: @@ -1261,7 +1329,13 @@ def validate(self, data: Dict[str, Any]) -> None: for x in self._srk_records ] - messages = ["Signing algorithm", "Hash algorithm", "Key Size", "Length", "Flags"] + messages = [ + "Signing algorithm", + "Hash algorithm", + "Key Size", + "Length", + "Flags", + ] for i in range(4): if not all(srk_records_info[0][i] == x[i] for x in srk_records_info): raise SPSDKValueError( @@ -1270,7 +1344,9 @@ def validate(self, data: Dict[str, Any]) -> None: if "srkh_sha_supports" in data.keys(): if ( - get_key_by_val(SRKRecord.HASH_ALGORITHM, self._srk_records[0].hash_algorithm).label + get_key_by_val( + SRKRecord.HASH_ALGORITHM, self._srk_records[0].hash_algorithm + ).label not in data["srkh_sha_supports"] ): raise SPSDKValueError( @@ -1297,7 +1373,9 @@ def parse(cls, data: bytes) -> Self: _, container_length, _ = unpack(SRKTable.format(), data[:srk_rec_offset]) if ((container_length - srk_rec_offset) % SRKTable.SRK_RECORDS_CNT) != 0: raise SPSDKLengthError("SRK table: Invalid length of SRK records data.") - srk_rec_size = math.ceil((container_length - srk_rec_offset) / SRKTable.SRK_RECORDS_CNT) + srk_rec_size = math.ceil( + (container_length - srk_rec_offset) / SRKTable.SRK_RECORDS_CNT + ) # try to parse records srk_records: List[SRKRecord] = [] @@ -1318,11 +1396,19 @@ def create_config(self, index: int, data_path: str) -> Dict[str, Any]: ret_cfg: Dict[str, Union[List, bool]] = {} cfg_srks = [] - ret_cfg["flag_ca"] = bool(self._srk_records[0].srk_flags & SRKRecord.FLAGS_CA_MASK) + ret_cfg["flag_ca"] = bool( + self._srk_records[0].srk_flags & SRKRecord.FLAGS_CA_MASK + ) for ix_srk, srk in enumerate(self._srk_records): - filename = f"container{index}_srk_public_key{ix_srk}_{srk.get_key_name()}.PEM" - write_file(data=srk.get_public_key(), path=os.path.join(data_path, filename), mode="wb") + filename = ( + f"container{index}_srk_public_key{ix_srk}_{srk.get_key_name()}.PEM" + ) + write_file( + data=srk.get_public_key(), + path=os.path.join(data_path, filename), + mode="wb", + ) cfg_srks.append(filename) ret_cfg["srk_array"] = cfg_srks @@ -1397,7 +1483,9 @@ def __eq__(self, other: object) -> bool: return False def __len__(self) -> int: - if (not self._signature_data or len(self._signature_data) == 0) and self.signature_provider: + if ( + not self._signature_data or len(self._signature_data) == 0 + ) and self.signature_provider: return super().__len__() + self.signature_provider.signature_length sign_data_len = len(self._signature_data) @@ -1596,7 +1684,9 @@ def __init__( :param signature_provider: Signature provider for certificate. Signature is calculated over all data from beginning of the certificate up to, but not including the signature. """ - tag = AHABTags.CERTIFICATE_UUID.tag if uuid else AHABTags.CERTIFICATE_NON_UUID.tag + tag = ( + AHABTags.CERTIFICATE_UUID.tag if uuid else AHABTags.CERTIFICATE_NON_UUID.tag + ) super().__init__(tag=tag, length=-1, version=self.VERSION) self._permissions = permissions self.signature_offset = -1 @@ -1726,10 +1816,14 @@ def update_fields(self) -> None: assert self.public_key self.public_key.update_fields() self.tag = ( - AHABTags.CERTIFICATE_UUID.tag if self._uuid else AHABTags.CERTIFICATE_NON_UUID.tag + AHABTags.CERTIFICATE_UUID.tag + if self._uuid + else AHABTags.CERTIFICATE_NON_UUID.tag ) self.signature_offset = ( - super().__len__() + (len(self._uuid) if self._uuid else 0) + len(self.public_key) + super().__len__() + + (len(self._uuid) if self._uuid else 0) + + len(self.public_key) ) self.length = len(self) self.signature.sign(self.get_signature_data()) @@ -1766,7 +1860,9 @@ def validate(self) -> None: """ self.validate_header() if self._permissions is None or not check_range(self._permissions, end=0xFF): - raise SPSDKValueError(f"Certificate: Invalid Permission data: {self._permissions}") + raise SPSDKValueError( + f"Certificate: Invalid Permission data: {self._permissions}" + ) if self.public_key is None: raise SPSDKValueError("Certificate: Missing public key.") self.public_key.validate() @@ -1777,7 +1873,9 @@ def validate(self) -> None: self.signature.validate() expected_signature_offset = ( - super().__len__() + (len(self._uuid) if self._uuid else 0) + len(self.public_key) + super().__len__() + + (len(self._uuid) if self._uuid else 0) + + len(self.public_key) ) if self.signature_offset != expected_signature_offset: raise SPSDKValueError( @@ -1815,7 +1913,9 @@ def parse(cls, data: bytes) -> Self: uuid = None if AHABTags.CERTIFICATE_UUID == tag: - uuid = data[certificate_data_offset : certificate_data_offset + Certificate.UUID_LEN] + uuid = data[ + certificate_data_offset : certificate_data_offset + Certificate.UUID_LEN + ] certificate_data_offset += Certificate.UUID_LEN public_key = SRKRecord.parse(data[certificate_data_offset:]) @@ -1830,7 +1930,9 @@ def parse(cls, data: bytes) -> Self: cert.signature = signature return cert - def create_config(self, index: int, data_path: str, srk_set: str = "oem") -> Dict[str, Any]: + def create_config( + self, index: int, data_path: str, srk_set: str = "oem" + ) -> Dict[str, Any]: """Create configuration of the AHAB Image Certificate. :param index: Container Index. @@ -1845,7 +1947,9 @@ def create_config(self, index: int, data_path: str, srk_set: str = "oem") -> Dic ret_cfg["uuid"] = "0x" + self._uuid.hex() filename = f"container{index}_certificate_public_key_{self.public_key.get_key_name()}.PEM" write_file( - data=self.public_key.get_public_key(), path=os.path.join(data_path, filename), mode="wb" + data=self.public_key.get_public_key(), + path=os.path.join(data_path, filename), + mode="wb", ) ret_cfg["public_key"] = filename ret_cfg["signature_provider"] = "N/A" @@ -1869,7 +1973,9 @@ def load_from_config( cert_uuid = value_to_bytes(cert_uuid_raw) if cert_uuid_raw else None cert_public_key_path = config.get("public_key") assert isinstance(cert_public_key_path, str) - cert_public_key_path = find_file(cert_public_key_path, search_paths=search_paths) + cert_public_key_path = find_file( + cert_public_key_path, search_paths=search_paths + ) cert_public_key = extract_public_key(cert_public_key_path) cert_srk_rec = SRKRecord.create_from_key(cert_public_key) cert_signature_provider = get_signature_provider( @@ -1890,7 +1996,11 @@ def get_validation_schemas() -> List[Dict[str, Any]]: :return: Validation list of schemas. """ - return [DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)["ahab_certificate"]] + return [ + DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)[ + "ahab_certificate" + ] + ] @staticmethod def generate_config_template() -> str: @@ -2046,9 +2156,9 @@ def validate(self) -> None: raise SPSDKValueError("AHAB Blob: Invalid algorithm.") if self.dek and len(self.dek) != self._size // 8: raise SPSDKValueError("AHAB Blob: Invalid DEK key size.") - if self.dek_keyblob is None or len(self.dek_keyblob) != self.compute_keyblob_size( - self._size - ): + if self.dek_keyblob is None or len( + self.dek_keyblob + ) != self.compute_keyblob_size(self._size): raise SPSDKValueError("AHAB Blob: Invalid Wrapped key.") @classmethod @@ -2347,7 +2457,8 @@ def export(self) -> bytes: ] = self.signature.export() if self.certificate: signature_block[ - self._certificate_offset : self._certificate_offset + len(self.certificate) + self._certificate_offset : self._certificate_offset + + len(self.certificate) ] = self.certificate.export() if self.blob: signature_block[ @@ -2379,11 +2490,17 @@ def check_offset(name: str, min_offset: int, offset: int) -> None: f"Signature Block: Invalid block length: {self.length} != {len(self)}" ) if bool(self._srk_table_offset) != bool(self.srk_table): - raise SPSDKValueError("Signature Block: Invalid setting of SRK table offset.") + raise SPSDKValueError( + "Signature Block: Invalid setting of SRK table offset." + ) if bool(self.signature_offset) != bool(self.signature): - raise SPSDKValueError("Signature Block: Invalid setting of Signature offset.") + raise SPSDKValueError( + "Signature Block: Invalid setting of Signature offset." + ) if bool(self._certificate_offset) != bool(self.certificate): - raise SPSDKValueError("Signature Block: Invalid setting of Certificate offset.") + raise SPSDKValueError( + "Signature Block: Invalid setting of Certificate offset." + ) if bool(self._blob_offset) != bool(self.blob): raise SPSDKValueError("Signature Block: Invalid setting of Blob offset.") @@ -2414,7 +2531,9 @@ def check_offset(name: str, min_offset: int, offset: int) -> None: ): # Container is signed by SRK key. Get the matching key and verify that the private key # belongs to the public key in SRK - srk_pair_id = get_matching_key_id(public_keys, self.signature.signature_provider) + srk_pair_id = get_matching_key_id( + public_keys, self.signature.signature_provider + ) if srk_pair_id != data["flag_used_srk_id"]: raise SPSDKValueError( f"Signature Block: Configured SRK ID ({data['flag_used_srk_id']})" @@ -2423,7 +2542,8 @@ def check_offset(name: str, min_offset: int, offset: int) -> None: elif self.certificate and self.certificate.permission_to_sign_container: # In this case the certificate is signed by the key with given SRK ID if not public_keys[data["flag_used_srk_id"]].verify_signature( - self.certificate.signature.signature_data, self.certificate.get_signature_data() + self.certificate.signature.signature_data, + self.certificate.get_signature_data(), ): raise SPSDKValueError( f"Certificate signature cannot be verified with the key with SRK ID {data['flag_used_srk_id']} " @@ -2456,10 +2576,14 @@ def parse(cls, data: bytes) -> Self: Certificate.parse(data[certificate_offset:]) if certificate_offset else None ) signature_block.signature = ( - ContainerSignature.parse(data[signature_offset:]) if signature_offset else None + ContainerSignature.parse(data[signature_offset:]) + if signature_offset + else None ) try: - signature_block.blob = Blob.parse(data[blob_offset:]) if blob_offset else None + signature_block.blob = ( + Blob.parse(data[blob_offset:]) if blob_offset else None + ) if signature_block.blob: signature_block.blob.key_identifier = key_identifier except SPSDKParsingError as exc: @@ -2488,13 +2612,17 @@ def load_from_config( # SRK Table srk_table_cfg = config.get("srk_table") signature_block.srk_table = ( - SRKTable.load_from_config(srk_table_cfg, search_paths) if srk_table_cfg else None + SRKTable.load_from_config(srk_table_cfg, search_paths) + if srk_table_cfg + else None ) # Container Signature srk_set = config.get("srk_set", "none") signature_block.signature = ( - ContainerSignature.load_from_config(config, search_paths) if srk_set != "none" else None + ContainerSignature.load_from_config(config, search_paths) + if srk_set != "none" + else None ) # Certificate Block @@ -2505,7 +2633,9 @@ def load_from_config( try: cert_cfg = load_configuration(certificate_cfg) check_config( - cert_cfg, Certificate.get_validation_schemas(), search_paths=search_paths + cert_cfg, + Certificate.get_validation_schemas(), + search_paths=search_paths, ) signature_block.certificate = Certificate.load_from_config(cert_cfg) except SPSDKError: @@ -2516,7 +2646,9 @@ def load_from_config( # DEK blob blob_cfg = config.get("blob") - signature_block.blob = Blob.load_from_config(blob_cfg, search_paths) if blob_cfg else None + signature_block.blob = ( + Blob.load_from_config(blob_cfg, search_paths) if blob_cfg else None + ) return signature_block @@ -2610,7 +2742,9 @@ def flag_srk_set(self) -> str: :return: Name of SRK Set flag. """ - srk_set = (self.flags >> self.FLAGS_SRK_SET_OFFSET) & ((1 << self.FLAGS_SRK_SET_SIZE) - 1) + srk_set = (self.flags >> self.FLAGS_SRK_SET_OFFSET) & ( + (1 << self.FLAGS_SRK_SET_SIZE) - 1 + ) return get_key_by_val(self.FLAGS_SRK_SET_VAL, srk_set) @property @@ -2667,8 +2801,11 @@ def header_length(self) -> int: :return: Length in bytes of AHAB Container header. """ - return super().__len__() + len( # This returns the fixed length of the container header - self.signature_block + return ( + super().__len__() + + len( # This returns the fixed length of the container header + self.signature_block + ) ) @classmethod @@ -2744,7 +2881,9 @@ def get_signature_data(self) -> bytes: "Can't retrieve data block to sign. Signature or SRK table is missing!" ) - signature_offset = self._signature_block_offset + self.signature_block.signature_offset + signature_offset = ( + self._signature_block_offset + self.signature_block.signature_offset + ) return self._export()[:signature_offset] def _export(self) -> bytes: @@ -2775,9 +2914,15 @@ def validate(self, data: Dict[str, Any]) -> None: if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): raise SPSDKValueError(f"Container Header: Invalid flags: {hex(self.flags)}") - if self.sw_version is None or not check_range(self.sw_version, end=(1 << 16) - 1): - raise SPSDKValueError(f"Container Header: Invalid SW version: {hex(self.sw_version)}") - if self.fuse_version is None or not check_range(self.fuse_version, end=(1 << 8) - 1): + if self.sw_version is None or not check_range( + self.sw_version, end=(1 << 16) - 1 + ): + raise SPSDKValueError( + f"Container Header: Invalid SW version: {hex(self.sw_version)}" + ) + if self.fuse_version is None or not check_range( + self.fuse_version, end=(1 << 8) - 1 + ): raise SPSDKValueError( f"Container Header: Invalid Fuse version: {hex(self.fuse_version)}" ) @@ -2805,7 +2950,13 @@ def _parse(binary: bytes) -> Tuple[int, int, int, int, int]: _, # reserved ) = unpack(image_format, binary[: AHABContainer.fixed_length()]) - return (flags, sw_version, fuse_version, number_of_images, signature_block_offset) + return ( + flags, + sw_version, + fuse_version, + number_of_images, + signature_block_offset, + ) def _create_config(self, index: int, data_path: str) -> Dict[str, Any]: """Create configuration of the AHAB Image. @@ -2824,7 +2975,9 @@ def _create_config(self, index: int, data_path: str) -> Dict[str, Any]: cfg["signing_key"] = "N/A" if self.signature_block.srk_table: - cfg["srk_table"] = self.signature_block.srk_table.create_config(index, data_path) + cfg["srk_table"] = self.signature_block.srk_table.create_config( + index, data_path + ) if self.signature_block.certificate: cert_cfg = self.signature_block.certificate.create_config( @@ -2976,7 +3129,8 @@ def _signature_block_offset(self) -> int: """ # Constant size of Container header + Image array Entry table return align( - super().fixed_length() + len(self.image_array) * ImageArrayEntry.fixed_length(), + super().fixed_length() + + len(self.image_array) * ImageArrayEntry.fixed_length(), CONTAINER_ALIGNMENT, ) @@ -2987,7 +3141,9 @@ def __len__(self) -> int: """ # Get image which has biggest offset possible_sizes = [self.header_length()] - possible_sizes.extend([align(x.image_offset + x.image_size) for x in self.image_array]) + possible_sizes.extend( + [align(x.image_offset + x.image_size) for x in self.image_array] + ) return align(max(possible_sizes), CONTAINER_ALIGNMENT) @@ -3104,7 +3260,9 @@ def validate(self, data: Dict[str, Any]) -> None: f"Container 0x{self.container_offset:04X} Header: Invalid Image Array: {self.image_array}" ) - for container, offset in zip(self.parent.ahab_containers, self.parent.ahab_address_map): + for container, offset in zip( + self.parent.ahab_containers, self.parent.ahab_address_map + ): if self == container: if self.container_offset != offset: raise SPSDKValueError( @@ -3113,7 +3271,9 @@ def validate(self, data: Dict[str, Any]) -> None: if self.signature_block.srk_table and self.signature_block.signature: # Get public key with the SRK ID - key = self.signature_block.srk_table.get_source_keys()[self.flag_used_srk_id] + key = self.signature_block.srk_table.get_source_keys()[ + self.flag_used_srk_id + ] if self.signature_block.certificate: # Verify signature of certificate if not key.verify_signature( @@ -3133,7 +3293,9 @@ def validate(self, data: Dict[str, Any]) -> None: assert ( self.signature_block.certificate.public_key ), "Certificate must contain public key" - key = PublicKey.parse(self.signature_block.certificate.public_key.get_public_key()) + key = PublicKey.parse( + self.signature_block.certificate.public_key.get_public_key() + ) if not key.verify_signature( self.signature_block.signature.signature_data, self.get_signature_data() @@ -3180,11 +3342,15 @@ def parse(cls, data: bytes, parent: "AHABImage", container_id: int) -> Self: # sw_version=sw_version, container_offset=parent.ahab_address_map[container_id], ) - parsed_container.signature_block = SignatureBlock.parse(data[signature_block_offset:]) + parsed_container.signature_block = SignatureBlock.parse( + data[signature_block_offset:] + ) for i in range(number_of_images): image_array_entry = ImageArrayEntry.parse( - data[AHABContainer.fixed_length() + i * ImageArrayEntry.fixed_length() :], + data[ + AHABContainer.fixed_length() + i * ImageArrayEntry.fixed_length() : + ], parsed_container, ) parsed_container.image_array.append(image_array_entry) @@ -3246,7 +3412,9 @@ def image_info(self) -> BinaryImage: size=self.header_length(), offset=0, binary=self.export(), - description=(f"AHAB Container for {self.flag_srk_set}" f"_SWver:{self.sw_version}"), + description=( + f"AHAB Container for {self.flag_srk_set}" f"_SWver:{self.sw_version}" + ), ) return ret @@ -3291,14 +3459,20 @@ def __init__( self.search_paths = search_paths self._database = get_db(family, revision) self.revision = self._database.name - self.ahab_address_map: List[int] = self._database.get_list(DatabaseManager.AHAB, "ahab_map") + self.ahab_address_map: List[int] = self._database.get_list( + DatabaseManager.AHAB, "ahab_map" + ) self.start_image_address = ( START_IMAGE_ADDRESS_NAND if target_memory in [TARGET_MEMORY_NAND_2K, TARGET_MEMORY_NAND_4K] else START_IMAGE_ADDRESS ) - self.containers_max_cnt = self._database.get_int(DatabaseManager.AHAB, "containers_max_cnt") - self.images_max_cnt = self._database.get_int(DatabaseManager.AHAB, "oem_images_max_cnt") + self.containers_max_cnt = self._database.get_int( + DatabaseManager.AHAB, "containers_max_cnt" + ) + self.images_max_cnt = self._database.get_int( + DatabaseManager.AHAB, "oem_images_max_cnt" + ) self.srkh_sha_supports: List[str] = self._database.get_list( DatabaseManager.AHAB, "srkh_sha_supports" ) @@ -3544,7 +3718,10 @@ def load_from_config( f"The obsolete key 'image_type':{image_type} has been converted into 'target_memory':{target_memory}" ) ahab = AHABImage( - family=family, revision=revision, target_memory=target_memory, search_paths=search_paths + family=family, + revision=revision, + target_memory=target_memory, + search_paths=search_paths, ) i = 0 for container_config in containers_config: @@ -3558,7 +3735,9 @@ def load_from_config( try: ahab.add_container( AHABContainer.parse( - ahab_bin[ahab.ahab_address_map[j] :], parent=ahab, container_id=i + ahab_bin[ahab.ahab_address_map[j] :], + parent=ahab, + container_id=i, ) ) i += 1 @@ -3572,7 +3751,9 @@ def load_from_config( else: ahab.add_container( - AHABContainer.load_from_config(ahab, container_config["container"], i) + AHABContainer.load_from_config( + ahab, container_config["container"], i + ) ) i += 1 @@ -3587,7 +3768,9 @@ def parse(self, binary: bytes) -> None: for i, address in enumerate(self.ahab_address_map): try: - container = AHABContainer.parse(binary[address:], parent=self, container_id=i) + container = AHABContainer.parse( + binary[address:], parent=self, container_id=i + ) self.ahab_containers.append(container) except SPSDKParsingError as exc: logger.debug(f"AHAB Image parsing error:\n{str(exc)}") @@ -3610,7 +3793,9 @@ def get_validation_schemas() -> List[Dict[str, Any]]: :return: Validation list of schemas. """ - sch = DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)["whole_ahab_image"] + sch = DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)[ + "whole_ahab_image" + ] sch["properties"]["family"]["enum"] = AHABImage.get_supported_families() return [sch] @@ -3625,7 +3810,8 @@ def generate_config_template(family: str) -> Dict[str, Any]: val_schemas[0]["properties"]["family"]["template_value"] = family yaml_data = CommentedConfig( - f"Advanced High-Assurance Boot Configuration template for {family}.", val_schemas + f"Advanced High-Assurance Boot Configuration template for {family}.", + val_schemas, ).get_template() return {f"{family}_ahab": yaml_data} diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py index 3922e69a..9c33910b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py @@ -33,7 +33,13 @@ ) from ...utils.database import DatabaseManager from ...utils.images import BinaryImage -from ...utils.misc import Endianness, align_block, check_range, load_hex_string, value_to_int +from ...utils.misc import ( + Endianness, + align_block, + check_range, + load_hex_string, + value_to_int, +) from ...utils.schema_validator import CommentedConfig from ...utils.spsdk_enum import SpsdkEnum @@ -175,12 +181,16 @@ def validate(self) -> None: f"Message: Invalid certificate version: {hex(self.cert_ver) if self.cert_ver else 'None'}" ) - if self.permissions is None or not check_range(self.permissions, end=(1 << 8) - 1): + if self.permissions is None or not check_range( + self.permissions, end=(1 << 8) - 1 + ): raise SPSDKValueError( f"Message: Invalid certificate permission: {hex(self.permissions) if self.permissions else 'None'}" ) - if self.issue_date is None or not check_range(self.issue_date, start=1, end=(1 << 16) - 1): + if self.issue_date is None or not check_range( + self.issue_date, start=1, end=(1 << 16) - 1 + ): raise SPSDKValueError( f"Message: Invalid issue date: {hex(self.issue_date) if self.issue_date else 'None'}" ) @@ -239,7 +249,9 @@ def load_from_config( return msg_cls.load_from_config(config, search_paths=search_paths) @staticmethod - def load_from_config_generic(config: Dict[str, Any]) -> Tuple[int, int, Optional[int], bytes]: + def load_from_config_generic( + config: Dict[str, Any] + ) -> Tuple[int, int, Optional[int], bytes]: """Converts the general configuration option into an message object. "config" content of container configurations. @@ -400,9 +412,13 @@ def load_from_config( raise SPSDKError(f"Invalid config field command: {command}") command_name = list(command.keys())[0] if MessageCommands.from_label(command_name) != MessageReturnLifeCycle.TAG: - raise SPSDKError("Invalid configuration for Return Life Cycle Request command.") + raise SPSDKError( + "Invalid configuration for Return Life Cycle Request command." + ) - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( + config + ) life_cycle = command.get("RETURN_LIFECYCLE_UPDATE_REQ") assert isinstance(life_cycle, int) @@ -431,7 +447,9 @@ def validate(self) -> None: """Validate general message properties.""" super().validate() if self.life_cycle is None: - raise SPSDKValueError("Message Return Life Cycle request: Invalid life cycle") + raise SPSDKValueError( + "Message Return Life Cycle request: Invalid life cycle" + ) class MessageWriteSecureFuse(Message): @@ -530,9 +548,13 @@ def load_from_config( raise SPSDKError(f"Invalid config field command: {command}") command_name = list(command.keys())[0] if MessageCommands.from_label(command_name) != MessageWriteSecureFuse.TAG: - raise SPSDKError("Invalid configuration for Write secure fuse Request command.") + raise SPSDKError( + "Invalid configuration for Write secure fuse Request command." + ) - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( + config + ) secure_fuse = command.get("WRITE_SEC_FUSE_REQ") assert isinstance(secure_fuse, dict) @@ -576,7 +598,9 @@ def validate(self) -> None: """Validate general message properties.""" super().validate() if self.fuse_data is None: - raise SPSDKValueError("Message Write secure fuse request: Missing fuse data") + raise SPSDKValueError( + "Message Write secure fuse request: Missing fuse data" + ) if len(self.fuse_data) != self.length: raise SPSDKValueError( "Message Write secure fuse request: The fuse value list " @@ -652,9 +676,13 @@ def parse_payload(self, data: bytes) -> None: :param data: Binary data with Payload to parse. """ - self.flags, self.target, self.reserved, self.monotonic_counter, self.user_sab_id = unpack( - self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH] - ) + ( + self.flags, + self.target, + self.reserved, + self.monotonic_counter, + self.user_sab_id, + ) = unpack(self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH]) def validate(self) -> None: """Validate general message properties.""" @@ -685,10 +713,10 @@ def validate(self) -> None: def __str__(self) -> str: ret = super().__str__() + ret += f" Monotonic counter value: 0x{self.monotonic_counter:08X}, {self.monotonic_counter}\n" ret += ( - f" Monotonic counter value: 0x{self.monotonic_counter:08X}, {self.monotonic_counter}\n" + f" User SAB id: 0x{self.user_sab_id:08X}, {self.user_sab_id}" ) - ret += f" User SAB id: 0x{self.user_sab_id:08X}, {self.user_sab_id}" return ret @staticmethod @@ -708,10 +736,17 @@ def load_from_config( if not isinstance(command, dict) or len(command) != 1: raise SPSDKError(f"Invalid config field command: {command}") command_name = list(command.keys())[0] - if MessageCommands.from_label(command_name) != MessageKeyStoreReprovisioningEnable.TAG: - raise SPSDKError("Invalid configuration for Write secure fuse Request command.") + if ( + MessageCommands.from_label(command_name) + != MessageKeyStoreReprovisioningEnable.TAG + ): + raise SPSDKError( + "Invalid configuration for Write secure fuse Request command." + ) - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( + config + ) keystore_repr_en = command.get("KEYSTORE_REPROVISIONING_ENABLE_REQ") assert isinstance(keystore_repr_en, dict) @@ -787,7 +822,11 @@ class DerivedKeyType(SpsdkEnum): AES = (0x2400, "AES SHA256", "Possible bit widths: 128/192/256") HMAC = (0x1100, "HMAC SHA384", "Possible bit widths: 224/256/384/512") - OEM_IMPORT_MK_SK = (0x9200, "OEM_IMPORT_MK_SK", "Possible bit widths: 128/192/256") + OEM_IMPORT_MK_SK = ( + 0x9200, + "OEM_IMPORT_MK_SK", + "Possible bit widths: 128/192/256", + ) class LifeCycle(SpsdkEnum): """Chip life cycle valid values.""" @@ -863,7 +902,11 @@ class DerivedKeyUsage(SpsdkEnum): "signature verification operation. Setting this permission automatically sets the Verify Message usage." ), ) - DERIVE = (0x00004000, "Derive", "Permission to derive other keys from this key.") + DERIVE = ( + 0x00004000, + "Derive", + "Permission to derive other keys from this key.", + ) def __init__( self, @@ -1065,7 +1108,9 @@ def parse_payload(self, data: bytes) -> None: ) = unpack(self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH]) # Do some post process - self.key_exchange_algorithm = self.KeyExchangeAlgorithm.from_tag(key_exchange_algorithm) + self.key_exchange_algorithm = self.KeyExchangeAlgorithm.from_tag( + key_exchange_algorithm + ) self.derived_key_type = self.DerivedKeyType.from_tag(derived_key_type) self.derived_key_lifetime = self.LifeTime.from_tag(derived_key_lifetime) self.derived_key_permitted_algorithm = self.KeyDerivationAlgorithm.from_tag( @@ -1105,7 +1150,9 @@ def __str__(self) -> str: ret += f" Derived key bit size value: 0x{self.derived_key_size_bits:08X}, {self.derived_key_size_bits}\n" ret += f" Derived key type value: {self.derived_key_type.label}\n" ret += f" Derived key life time value: {self.derived_key_lifetime.label}\n" - ret += f" Derived key usage value: {[x.label for x in self.derived_key_usage]}\n" + ret += ( + f" Derived key usage value: {[x.label for x in self.derived_key_usage]}\n" + ) ret += f" Derived key permitted algorithm value: {self.derived_key_permitted_algorithm.label}\n" ret += f" Derived key life cycle value: {self.derived_key_lifecycle.label}\n" ret += f" Derived key ID value: 0x{self.derived_key_id:08X}, {self.derived_key_id}\n" @@ -1134,7 +1181,9 @@ def load_from_config( if MessageCommands.from_label(command_name) != MessageKeyExchange.TAG: raise SPSDKError("Invalid configuration forKey Exchange Request command.") - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic(config) + cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( + config + ) key_exchange = command.get("KEY_EXCHANGE_REQ") assert isinstance(key_exchange, dict) @@ -1145,7 +1194,9 @@ def load_from_config( ) salt_flags = value_to_int(key_exchange.get("salt_flags", 0)) derived_key_grp = value_to_int(key_exchange.get("derived_key_grp", 0)) - derived_key_size_bits = value_to_int(key_exchange.get("derived_key_size_bits", 128)) + derived_key_size_bits = value_to_int( + key_exchange.get("derived_key_size_bits", 128) + ) derived_key_type = MessageKeyExchange.DerivedKeyType.from_attr( key_exchange.get("derived_key_type", "AES SHA256") ) @@ -1156,8 +1207,10 @@ def load_from_config( MessageKeyExchange.DerivedKeyUsage.from_attr(x) for x in key_exchange.get("derived_key_usage", []) ] - derived_key_permitted_algorithm = MessageKeyExchange.KeyDerivationAlgorithm.from_attr( - key_exchange.get("derived_key_permitted_algorithm", "HKDF SHA256") + derived_key_permitted_algorithm = ( + MessageKeyExchange.KeyDerivationAlgorithm.from_attr( + key_exchange.get("derived_key_permitted_algorithm", "HKDF SHA256") + ) ) derived_key_lifecycle = MessageKeyExchange.LifeCycle.from_attr( key_exchange.get("derived_key_lifecycle", "OPEN") @@ -1211,14 +1264,18 @@ def create_config(self) -> Dict[str, Any]: key_exchange_cfg["derived_key_size_bits"] = self.derived_key_size_bits key_exchange_cfg["derived_key_type"] = self.derived_key_type.label key_exchange_cfg["derived_key_lifetime"] = self.derived_key_lifetime.label - key_exchange_cfg["derived_key_usage"] = [x.label for x in self.derived_key_usage] + key_exchange_cfg["derived_key_usage"] = [ + x.label for x in self.derived_key_usage + ] key_exchange_cfg[ "derived_key_permitted_algorithm" ] = self.derived_key_permitted_algorithm.label key_exchange_cfg["derived_key_lifecycle"] = self.derived_key_lifecycle.label key_exchange_cfg["derived_key_id"] = self.derived_key_id key_exchange_cfg["private_key_id"] = self.private_key_id - key_exchange_cfg["input_peer_public_key_digest"] = self.input_peer_public_key_digest.hex() + key_exchange_cfg[ + "input_peer_public_key_digest" + ] = self.input_peer_public_key_digest.hex() key_exchange_cfg["input_user_fixed_info_digest"] = ( self.input_user_fixed_info_digest.hex() if self.input_user_fixed_info_digest @@ -1384,7 +1441,9 @@ def _export(self) -> bytes: assert self.message signed_message += self.message.export() # Add Signature Block - signed_message += align_block(self.signature_block.export(), CONTAINER_ALIGNMENT) + signed_message += align_block( + self.signature_block.export(), CONTAINER_ALIGNMENT + ) return signed_message def export(self) -> bytes: @@ -1451,7 +1510,9 @@ def parse(cls, data: bytes) -> Self: sw_version=sw_version, encrypt_iv=iv if bool(descriptor_flags & 0x01) else None, ) - parsed_signed_msg.signature_block = SignatureBlock.parse(data[signature_block_offset:]) + parsed_signed_msg.signature_block = SignatureBlock.parse( + data[signature_block_offset:] + ) # Parse also Message itself parsed_signed_msg.message = Message.parse( @@ -1495,7 +1556,9 @@ def load_from_config( message = config.get("message") assert isinstance(message, dict) - signed_msg.message = Message.load_from_config(message, search_paths=search_paths) + signed_msg.message = Message.load_from_config( + message, search_paths=search_paths + ) return signed_msg @@ -1511,7 +1574,9 @@ def image_info(self) -> BinaryImage: size=len(self), offset=0, binary=self.export(), - description=(f"Signed Message for {MessageCommands.get_label(self.message.TAG)}"), + description=( + f"Signed Message for {MessageCommands.get_label(self.message.TAG)}" + ), ) return ret @@ -1544,10 +1609,12 @@ def generate_config_template( ) if message: - for cmd_sch in val_schemas[0]["properties"]["message"]["properties"]["command"][ - "oneOf" - ]: - cmd_sch["skip_in_template"] = bool(message.label not in cmd_sch["properties"]) + for cmd_sch in val_schemas[0]["properties"]["message"]["properties"][ + "command" + ]["oneOf"]: + cmd_sch["skip_in_template"] = bool( + message.label not in cmd_sch["properties"] + ) yaml_data = CommentedConfig( f"Signed message Configuration template for {family}.", val_schemas diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py index 8d12aeae..e7044b36 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py @@ -10,7 +10,12 @@ from typing import Optional from ...apps.utils.utils import SPSDKError -from ...image.ahab.ahab_container import AHABContainerBase, AHABImage, Blob, SignatureBlock +from ...image.ahab.ahab_container import ( + AHABContainerBase, + AHABImage, + Blob, + SignatureBlock, +) from ...utils.database import DatabaseManager, get_db from ...utils.misc import load_binary @@ -52,7 +57,9 @@ def ahab_update_keyblob( raise SPSDKError(f"No container ID {container_id}") from exc with open(binary, "r+b") as f: - logger.debug(f"Trying to find AHAB container header at offset {hex(address + offset)}") + logger.debug( + f"Trying to find AHAB container header at offset {hex(address + offset)}" + ) f.seek(address + offset) data = f.read(DATA_READ) ( diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py index 04e5557a..d168dfdd 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py @@ -72,7 +72,9 @@ def size(self) -> int: """Header size in bytes.""" return self.SIZE - def __init__(self, tag: int = 0, param: int = 0, length: Optional[int] = None) -> None: + def __init__( + self, tag: int = 0, param: int = 0, length: Optional[int] = None + ) -> None: """Constructor. :param tag: section tag @@ -97,7 +99,9 @@ def tag_name(self) -> str: return SegTag.get_label(self.tag) def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.tag_name}, {self.param}, {self.length})" + return ( + f"{self.__class__.__name__}({self.tag_name}, {self.param}, {self.length})" + ) def __str__(self) -> str: return ( diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py index 43ff9f85..d592c340 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py @@ -13,7 +13,6 @@ from ..exceptions import SPSDKError from ..utils.registers import value_to_int - from .header import Header @@ -63,7 +62,9 @@ def read_raw_data( try: data = stream.read(length) except Exception as exc: - raise StreamReadFailed(f" stream.read() failed, requested {length} bytes") from exc + raise StreamReadFailed( + f" stream.read() failed, requested {length} bytes" + ) from exc if len(data) != length: raise NotEnoughBytesException( @@ -77,7 +78,9 @@ def read_raw_data( def read_raw_segment( - buffer: Union[io.BufferedReader, io.BytesIO], segment_tag: int, index: Optional[int] = None + buffer: Union[io.BufferedReader, io.BytesIO], + segment_tag: int, + index: Optional[int] = None, ) -> bytes: """Read raw segment.""" hrdata = read_raw_data(buffer, Header.SIZE, index) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py index 4148d6ec..11e78bba 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py @@ -21,7 +21,6 @@ from ..utils.abstract import BaseClass from ..utils.misc import Endianness from ..utils.spsdk_enum import SpsdkEnum - from .header import Header, SegTag from .misc import hexdump_fmt, modulus_fmt @@ -183,9 +182,7 @@ def __iter__(self) -> Iterator[int]: return self._data.__iter__() def __repr__(self) -> str: - return ( - f"Certificate " - ) + return f"Certificate " def __str__(self) -> str: """String representation of the CertificateImg.""" @@ -729,7 +726,9 @@ def flag(self, value: int) -> None: raise SPSDKError("Incorrect flag") self._flag = value - def __init__(self, key_size: int, x_coordinate: int, y_coordinate: int, flag: int = 0) -> None: + def __init__( + self, key_size: int, x_coordinate: int, y_coordinate: int, flag: int = 0 + ) -> None: """Initialize the srk table item.""" self._header = Header(tag=EnumSRK.KEY_PUBLIC.tag, param=EnumAlgorithm.ECDSA.tag) self.x_coordinate = x_coordinate @@ -739,8 +738,16 @@ def __init__(self, key_size: int, x_coordinate: int, y_coordinate: int, flag: in self.flag = flag self._header.length += ( 8 - + len(self.x_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value)) - + len(self.y_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value)) + + len( + self.x_coordinate.to_bytes( + self.coordinate_size, byteorder=Endianness.BIG.value + ) + ) + + len( + self.y_coordinate.to_bytes( + self.coordinate_size, byteorder=Endianness.BIG.value + ) + ) ) def __repr__(self) -> str: @@ -773,10 +780,22 @@ def export(self) -> bytes: data = self._header.export() curve_id = self.ECC_KEY_TYPE[get_ecc_curve(self.key_size // 8)] data += pack( - ">8B", 0, 0, 0, self.flag, curve_id, 0, self.key_size >> 8 & 0xFF, self.key_size & 0xFF + ">8B", + 0, + 0, + 0, + self.flag, + curve_id, + 0, + self.key_size >> 8 & 0xFF, + self.key_size & 0xFF, + ) + data += self.x_coordinate.to_bytes( + self.coordinate_size, byteorder=Endianness.BIG.value + ) + data += self.y_coordinate.to_bytes( + self.coordinate_size, byteorder=Endianness.BIG.value ) - data += self.x_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value) - data += self.y_coordinate.to_bytes(self.coordinate_size, byteorder=Endianness.BIG.value) return data @classmethod diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py index 5c64e02a..063733a1 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/commands.py @@ -13,7 +13,6 @@ from ..utils.interfaces.commands import CmdPacketBase, CmdResponseBase from ..utils.spsdk_enum import SpsdkEnum - from .error_codes import StatusCode from .exceptions import McuBootError @@ -232,7 +231,9 @@ def from_bytes(cls, data: bytes, offset: int = 0) -> "CmdHeader": :raises McuBootError: Invalid data format """ if len(data) < 4: - raise McuBootError(f"Invalid format of RX packet (data length is {len(data)} bytes)") + raise McuBootError( + f"Invalid format of RX packet (data length is {len(data)} bytes)" + ) return cls(*unpack_from("4B", data, offset)) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py index cfe78be7..ea7320df 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/exceptions.py @@ -9,7 +9,6 @@ """Exceptions used in the MBoot module.""" from ..exceptions import SPSDKError - from .error_codes import StatusCode ######################################################################################################################## diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py index d543cbec..92a7e93b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py @@ -56,10 +56,16 @@ class SpiModeCommand(Enum): version = 0x01 # 00000001 - Enter raw SPI mode, display version string chip_select = 0x02 # 0000001x - CS high (1) or low (0) sniff = 0x0C # 000011XX - Sniff SPI traffic when CS low(10)/all(01) - bulk_transfer = 0x10 # 0001xxxx - Bulk SPI transfer, send/read 1-16 bytes (0=1byte!) - config_periph = 0x40 # 0100wxyz - Configure peripherals w=power, x=pull-ups, y=AUX, z=CS + bulk_transfer = ( + 0x10 # 0001xxxx - Bulk SPI transfer, send/read 1-16 bytes (0=1byte!) + ) + config_periph = ( + 0x40 # 0100wxyz - Configure peripherals w=power, x=pull-ups, y=AUX, z=CS + ) set_speed = 0x60 # 01100xxx - SPI speed - config_spi = 0x80 # 1000wxyz - SPI config, w=HiZ/3.3v, x=CKP idle, y=CKE edge, z=SMP sample + config_spi = ( + 0x80 # 1000wxyz - SPI config, w=HiZ/3.3v, x=CKP idle, y=CKE edge, z=SMP sample + ) write_then_read = 0x04 # 00000100 - Write then read extended command @@ -206,7 +212,9 @@ def _check_port_buspal( """ props = props if props is not None else [] try: - device = SerialDevice(port=port, timeout=timeout, baudrate=cls.default_baudrate) + device = SerialDevice( + port=port, timeout=timeout, baudrate=cls.default_baudrate + ) interface = cls(device) interface.open() interface._configure(props) @@ -255,7 +263,9 @@ def _send_command_check_response(self, command: bytes, response: bytes) -> None: format_received == format_expected ), f"Received data '{format_received}' but expected '{format_expected}'" - def _read_frame_header(self, expected_frame_type: Optional[FPType] = None) -> Tuple[int, int]: + def _read_frame_header( + self, expected_frame_type: Optional[FPType] = None + ) -> Tuple[int, int]: """Read frame header and frame type. Return them as tuple of integers. :param expected_frame_type: Check if the frame_type is exactly as expected @@ -333,9 +343,13 @@ def _configure(self, props: List[str]) -> None: spi_props: Dict[str, Any] = dict(zip(self.TARGET_SETTINGS, props)) speed = int(spi_props.get("speed", 100)) - polarity = SpiClockPolarity(spi_props.get("polarity", SpiClockPolarity.active_low)) + polarity = SpiClockPolarity( + spi_props.get("polarity", SpiClockPolarity.active_low) + ) phase = SpiClockPhase(spi_props.get("phase", SpiClockPhase.second_edge)) - direction = SpiShiftDirection(spi_props.get("direction", SpiShiftDirection.msb_first)) + direction = SpiShiftDirection( + spi_props.get("direction", SpiShiftDirection.msb_first) + ) # set SPI config logger.debug("Set SPI config") @@ -343,12 +357,16 @@ def _configure(self, props: List[str]) -> None: spi_data |= phase.value << SpiConfigShift.phase.value spi_data |= direction.value << SpiConfigShift.direction.value spi_data |= SpiModeCommand.config_spi.value - self._send_command_check_response(bytes([spi_data]), bytes([BBConstants.response_ok.value])) + self._send_command_check_response( + bytes([spi_data]), bytes([BBConstants.response_ok.value]) + ) # set SPI speed logger.debug(f"Set SPI speed to {speed}bps") spi_speed = struct.pack(" None: """Send data to BUSPAL I2C device. @@ -358,7 +376,10 @@ def _send_frame(self, data: bytes, wait_for_ack: bool = True) -> None: self._send_frame_retry(data, wait_for_ack, self.HDR_FRAME_RETRY_CNT) def _send_frame_retry( - self, data: bytes, wait_for_ack: bool = True, retry_cnt: int = HDR_FRAME_RETRY_CNT + self, + data: bytes, + wait_for_ack: bool = True, + retry_cnt: int = HDR_FRAME_RETRY_CNT, ) -> None: """Send a frame to BUSPAL SPI device. @@ -385,7 +406,9 @@ def _send_frame_retry( retry_cnt -= 1 self._send_frame_retry(data, wait_for_ack, retry_cnt) else: - raise SPSDKError("Failed retrying reading the SPI header frame") from error + raise SPSDKError( + "Failed retrying reading the SPI header frame" + ) from error def _read(self, size: int, timeout: Optional[int] = None) -> bytes: """Read 'length' amount of bytes from BUSPAL SPI device. @@ -394,7 +417,9 @@ def _read(self, size: int, timeout: Optional[int] = None) -> bytes: """ size = min(size, BBConstants.bulk_transfer_max.value) command = struct.pack(" None: # set I2C address logger.debug(f"Set I2C address to {address}") i2c_data = struct.pack(" None: """Send data to BUSPAL I2C device. @@ -515,7 +549,9 @@ def _send_frame_retry( retry_cnt -= 1 self._send_frame_retry(data, wait_for_ack, retry_cnt) else: - raise SPSDKError("Failed retrying reading the I2C header frame") from error + raise SPSDKError( + "Failed retrying reading the I2C header frame" + ) from error def _read(self, size: int, timeout: Optional[int] = None) -> bytes: """Read 'length' amount of bytes from BUSPAL I2C device. @@ -524,5 +560,7 @@ def _read(self, size: int, timeout: Optional[int] = None) -> bytes: """ size = min(size, BBConstants.bulk_transfer_max.value) command = struct.pack(" Union[CmdResponse, bytes]: return parse_cmd_response(data) return data - def _read_frame_header(self, expected_frame_type: Optional[FPType] = None) -> Tuple[int, int]: + def _read_frame_header( + self, expected_frame_type: Optional[FPType] = None + ) -> Tuple[int, int]: """Read frame header and frame type. Return them as tuple of integers. :param expected_frame_type: Check if the frame_type is exactly as expected diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py index bcd26e1d..5436fc0f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py @@ -34,7 +34,8 @@ def parse(cls, params: str) -> Self: """ port_parts = params.split(",") return cls( - port=port_parts.pop(0), baudrate=int(port_parts.pop(), 0) if port_parts else None + port=port_parts.pop(0), + baudrate=int(port_parts.pop(), 0) if port_parts else None, ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py index 78d72ad2..41d3123d 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py @@ -11,7 +11,11 @@ from typing_extensions import Self from ...mboot.protocol.serial_protocol import MbootSerialProtocol -from ...utils.interfaces.device.usbsio_device import ScanArgs, UsbSioI2CDevice, UsbSioSPIDevice +from ...utils.interfaces.device.usbsio_device import ( + ScanArgs, + UsbSioI2CDevice, + UsbSioSPIDevice, +) class MbootUsbSioI2CInterface(MbootSerialProtocol): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py index 99ba4c57..4bb402d6 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/mcuboot.py @@ -16,7 +16,6 @@ from ..mboot.protocol.base import MbootProtocolBase from ..utils.interfaces.device.usb_device import UsbDevice - from .commands import ( CmdPacket, CmdResponse, @@ -73,7 +72,9 @@ def is_opened(self) -> bool: """Return True if the device is open.""" return self._interface.is_opened - def __init__(self, interface: MbootProtocolBase, cmd_exception: bool = False) -> None: + def __init__( + self, interface: MbootProtocolBase, cmd_exception: bool = False + ) -> None: """Initialize the McuBoot object. :param interface: The instance of communication interface class @@ -129,7 +130,9 @@ def _process_cmd(self, cmd_packet: CmdPacket) -> CmdResponse: self._status_code = response.status if self._cmd_exception and self._status_code != StatusCode.SUCCESS: - raise McuBootCommandError(CommandTag.get_label(cmd_packet.header.tag), response.status) + raise McuBootCommandError( + CommandTag.get_label(cmd_packet.header.tag), response.status + ) logger.info(f"CMD: Status: {self.status_string}") return response @@ -183,7 +186,9 @@ def _read_data( if self._status_code in StatusCode.tags() else f"0x{self._status_code:08X}" ) - logger.debug(f"CMD: Received {len(data)} from {length} Bytes, {status_info}") + logger.debug( + f"CMD: Received {len(data)} from {length} Bytes, {status_info}" + ) if self._cmd_exception: assert isinstance(response, CmdResponse) raise McuBootCommandError(cmd_tag.label, response.status) @@ -264,7 +269,9 @@ def _get_max_packet_size(self) -> int: """ packet_size_property = None try: - packet_size_property = self.get_property(prop_tag=PropertyTag.MAX_PACKET_SIZE) + packet_size_property = self.get_property( + prop_tag=PropertyTag.MAX_PACKET_SIZE + ) except McuBootError: pass if packet_size_property is None: @@ -284,7 +291,9 @@ def _split_data(self, data: bytes) -> List[bytes]: return [data] max_packet_size = self._get_max_packet_size() logger.info(f"CMD: Max Packet Size = {max_packet_size}") - return [data[i : i + max_packet_size] for i in range(0, len(data), max_packet_size)] + return [ + data[i : i + max_packet_size] for i in range(0, len(data), max_packet_size) + ] def open(self) -> None: """Connect to the device.""" @@ -418,11 +427,15 @@ def _get_ext_memories(self) -> List[ExtMemRegion]: for mem_id in ext_mem_ids: try: - values = self.get_property(PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES, mem_id) + values = self.get_property( + PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES, mem_id + ) except McuBootCommandError: values = None - if not values: # pragma: no cover # corner-cases are currently untestable without HW + if ( + not values + ): # pragma: no cover # corner-cases are currently untestable without HW if self._status_code == StatusCode.UNKNOWN_PROPERTY: break @@ -519,7 +532,9 @@ def read_memory( :param progress_callback: Callback for updating the caller about the progress :return: Data read from the memory; None in case of a failure """ - logger.info(f"CMD: ReadMemory(address=0x{address:08X}, length={length}, mem_id={mem_id})") + logger.info( + f"CMD: ReadMemory(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) mem_id = _clamp_down_memory_id(memory_id=mem_id) # workaround for better USB-HID reliability @@ -551,7 +566,9 @@ def read_memory( if progress_callback: progress_callback(len(data), length) if self._status_code == StatusCode.NO_RESPONSE: - logger.warning(f"CMD: NO RESPONSE, received {len(data)}/{length} B") + logger.warning( + f"CMD: NO RESPONSE, received {len(data)}/{length} B" + ) return data else: return b"" @@ -564,7 +581,9 @@ def read_memory( cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: assert isinstance(cmd_response, ReadMemoryResponse) - return self._read_data(CommandTag.READ_MEMORY, cmd_response.length, progress_callback) + return self._read_data( + CommandTag.READ_MEMORY, cmd_response.length, progress_callback + ) return None def write_memory( @@ -588,10 +607,16 @@ def write_memory( data_chunks = self._split_data(data=data) mem_id = _clamp_down_memory_id(memory_id=mem_id) cmd_packet = CmdPacket( - CommandTag.WRITE_MEMORY, CommandFlag.HAS_DATA_PHASE.tag, address, len(data), mem_id + CommandTag.WRITE_MEMORY, + CommandFlag.HAS_DATA_PHASE.tag, + address, + len(data), + mem_id, ) if self._process_cmd(cmd_packet).status == StatusCode.SUCCESS: - return self._send_data(CommandTag.WRITE_MEMORY, data_chunks, progress_callback) + return self._send_data( + CommandTag.WRITE_MEMORY, data_chunks, progress_callback + ) return False def fill_memory(self, address: int, length: int, pattern: int = 0xFFFFFFFF) -> bool: @@ -623,11 +648,15 @@ def flash_security_disable(self, backdoor_key: bytes) -> bool: key_high = backdoor_key[0:4][::-1] key_low = backdoor_key[4:8][::-1] cmd_packet = CmdPacket( - CommandTag.FLASH_SECURITY_DISABLE, CommandFlag.NONE.tag, data=key_high + key_low + CommandTag.FLASH_SECURITY_DISABLE, + CommandFlag.NONE.tag, + data=key_high + key_low, ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS - def get_property(self, prop_tag: PropertyTag, index: int = 0) -> Optional[List[int]]: + def get_property( + self, prop_tag: PropertyTag, index: int = 0 + ) -> Optional[List[int]]: """Get specified property value. :param prop_tag: Property TAG (see Properties Enum) @@ -636,12 +665,16 @@ def get_property(self, prop_tag: PropertyTag, index: int = 0) -> Optional[List[i :raises McuBootError: If received invalid get-property response """ logger.info(f"CMD: GetProperty({prop_tag.label}, index={index!r})") - cmd_packet = CmdPacket(CommandTag.GET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, index) + cmd_packet = CmdPacket( + CommandTag.GET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, index + ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: if isinstance(cmd_response, GetPropertyResponse): return cmd_response.values - raise McuBootError(f"Received invalid get-property response: {str(cmd_response)}") + raise McuBootError( + f"Received invalid get-property response: {str(cmd_response)}" + ) return None def set_property(self, prop_tag: PropertyTag, value: int) -> bool: @@ -652,7 +685,9 @@ def set_property(self, prop_tag: PropertyTag, value: int) -> bool: :return: False in case of any problem; True otherwise """ logger.info(f"CMD: SetProperty({prop_tag.label}, value=0x{value:08X})") - cmd_packet = CmdPacket(CommandTag.SET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, value) + cmd_packet = CmdPacket( + CommandTag.SET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, value + ) cmd_response = self._process_cmd(cmd_packet) return cmd_response.status == StatusCode.SUCCESS @@ -698,12 +733,16 @@ def receive_sb_file( # self._pause_point = sb3_header.image_total_length # except SPSDKError: # pass - result = self._send_data(CommandTag.RECEIVE_SB_FILE, data_chunks, progress_callback) + result = self._send_data( + CommandTag.RECEIVE_SB_FILE, data_chunks, progress_callback + ) self.enable_data_abort = False return result return False - def execute(self, address: int, argument: int, sp: int) -> bool: # pylint: disable=invalid-name + def execute( + self, address: int, argument: int, sp: int + ) -> bool: # pylint: disable=invalid-name """Execute program on a given address using the stack pointer. :param address: Jump address (must be word aligned) @@ -714,7 +753,9 @@ def execute(self, address: int, argument: int, sp: int) -> bool: # pylint: disa logger.info( f"CMD: Execute(address=0x{address:08X}, argument=0x{argument:08X}, SP=0x{sp:08X})" ) - cmd_packet = CmdPacket(CommandTag.EXECUTE, CommandFlag.NONE.tag, address, argument, sp) + cmd_packet = CmdPacket( + CommandTag.EXECUTE, CommandFlag.NONE.tag, address, argument, sp + ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS def call(self, address: int, argument: int) -> bool: @@ -772,7 +813,9 @@ def flash_erase_all_unsecure(self) -> bool: :return: False in case of any problem; True otherwise """ logger.info("CMD: FlashEraseAllUnsecure") - cmd_packet = CmdPacket(CommandTag.FLASH_ERASE_ALL_UNSECURE, CommandFlag.NONE.tag) + cmd_packet = CmdPacket( + CommandTag.FLASH_ERASE_ALL_UNSECURE, CommandFlag.NONE.tag + ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS def efuse_read_once(self, index: int) -> Optional[int]: @@ -782,7 +825,9 @@ def efuse_read_once(self, index: int) -> Optional[int]: :return: read value (32-bit int); None if operation failed """ logger.info(f"CMD: FlashReadOnce(index={index})") - cmd_packet = CmdPacket(CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, 4) + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, 4 + ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: assert isinstance(cmd_response, FlashReadOnceResponse) @@ -801,7 +846,9 @@ def efuse_program_once(self, index: int, value: int, verify: bool = False) -> bo f"CMD: FlashProgramOnce(index={index}, value=0x{value:X}) " f"with{'' if verify else 'out'} verification." ) - cmd_packet = CmdPacket(CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, 4, value) + cmd_packet = CmdPacket( + CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, 4, value + ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status != StatusCode.SUCCESS: return False @@ -830,7 +877,9 @@ def flash_read_once(self, index: int, count: int = 4) -> Optional[bytes]: if count not in (4, 8): raise SPSDKError("Invalid count of bytes. Must be 4 or 8") logger.info(f"CMD: FlashReadOnce(index={index}, bytes={count})") - cmd_packet = CmdPacket(CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, count) + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, count + ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: assert isinstance(cmd_response, FlashReadOnceResponse) @@ -849,11 +898,17 @@ def flash_program_once(self, index: int, data: bytes) -> bool: raise SPSDKError("Invalid length of data. Must be aligned to 4 or 8 bytes") logger.info(f"CMD: FlashProgramOnce(index={index!r}, data={data!r})") cmd_packet = CmdPacket( - CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, len(data), data=data + CommandTag.FLASH_PROGRAM_ONCE, + CommandFlag.NONE.tag, + index, + len(data), + data=data, ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS - def flash_read_resource(self, address: int, length: int, option: int = 1) -> Optional[bytes]: + def flash_read_resource( + self, address: int, length: int, option: int = 1 + ) -> Optional[bytes]: """Read resource of flash module. :param address: Start address @@ -863,12 +918,18 @@ def flash_read_resource(self, address: int, length: int, option: int = 1) -> Opt :return: Data from the resource; None in case of an failure """ if length % 4: - raise McuBootError("The number of bytes to read is not aligned to the 4 bytes") + raise McuBootError( + "The number of bytes to read is not aligned to the 4 bytes" + ) logger.info( f"CMD: FlashReadResource(address=0x{address:08X}, length={length}, option={option})" ) cmd_packet = CmdPacket( - CommandTag.FLASH_READ_RESOURCE, CommandFlag.NONE.tag, address, length, option + CommandTag.FLASH_READ_RESOURCE, + CommandFlag.NONE.tag, + address, + length, + option, ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: @@ -884,7 +945,9 @@ def configure_memory(self, address: int, mem_id: int) -> bool: :return: False in case of any problem; True otherwise """ logger.info(f"CMD: ConfigureMemory({mem_id}, address=0x{address:08X})") - cmd_packet = CmdPacket(CommandTag.CONFIGURE_MEMORY, CommandFlag.NONE.tag, mem_id, address) + cmd_packet = CmdPacket( + CommandTag.CONFIGURE_MEMORY, CommandFlag.NONE.tag, mem_id, address + ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS def reliable_update(self, address: int) -> bool: @@ -894,7 +957,9 @@ def reliable_update(self, address: int) -> bool: :return: False in case of any problem; True otherwise """ logger.info(f"CMD: ReliableUpdate(address=0x{address:08X})") - cmd_packet = CmdPacket(CommandTag.RELIABLE_UPDATE, CommandFlag.NONE.tag, address) + cmd_packet = CmdPacket( + CommandTag.RELIABLE_UPDATE, CommandFlag.NONE.tag, address + ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS def generate_key_blob( @@ -928,7 +993,9 @@ def generate_key_blob( if not self._send_data(CommandTag.GENERATE_KEY_BLOB, data_chunks): return None cmd_response = self._process_cmd( - CmdPacket(CommandTag.GENERATE_KEY_BLOB, CommandFlag.NONE.tag, key_sel, count, 1) + CmdPacket( + CommandTag.GENERATE_KEY_BLOB, CommandFlag.NONE.tag, key_sel, count, 1 + ) ) if cmd_response.status == StatusCode.SUCCESS: assert isinstance(cmd_response, ReadMemoryResponse) @@ -942,7 +1009,9 @@ def kp_enroll(self) -> bool: """ logger.info("CMD: [KeyProvisioning] Enroll") cmd_packet = CmdPacket( - CommandTag.KEY_PROVISIONING, CommandFlag.NONE.tag, KeyProvOperation.ENROLL.tag + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.ENROLL.tag, ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS @@ -953,7 +1022,9 @@ def kp_set_intrinsic_key(self, key_type: int, key_size: int) -> bool: :param key_size: Size of the key :return: False in case of any problem; True otherwise """ - logger.info(f"CMD: [KeyProvisioning] SetIntrinsicKey(type={key_type}, key_size={key_size})") + logger.info( + f"CMD: [KeyProvisioning] SetIntrinsicKey(type={key_type}, key_size={key_size})" + ) cmd_packet = CmdPacket( CommandTag.KEY_PROVISIONING, CommandFlag.NONE.tag, @@ -1001,7 +1072,8 @@ def kp_set_user_key(self, key_type: int, key_data: bytes) -> bool: :return: False in case of any problem; True otherwise """ logger.info( - f"CMD: [KeyProvisioning] SetUserKey(key_type={key_type}, " f"key_len={len(key_data)})" + f"CMD: [KeyProvisioning] SetUserKey(key_type={key_type}, " + f"key_len={len(key_data)})" ) data_chunks = self._split_data(data=key_data) cmd_packet = CmdPacket( @@ -1040,7 +1112,9 @@ def kp_read_key_store(self) -> Optional[bytes]: """Key provisioning: Read key data from key store area.""" logger.info("CMD: [KeyProvisioning] ReadKeyStore") cmd_packet = CmdPacket( - CommandTag.KEY_PROVISIONING, CommandFlag.NONE.tag, KeyProvOperation.READ_KEY_STORE.tag + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.READ_KEY_STORE.tag, ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: @@ -1049,7 +1123,9 @@ def kp_read_key_store(self) -> Optional[bytes]: return None def load_image( - self, data: bytes, progress_callback: Optional[Callable[[int, int], None]] = None + self, + data: bytes, + progress_callback: Optional[Callable[[int, int], None]] = None, ) -> bool: """Load a boot image to the device. @@ -1080,7 +1156,8 @@ def tp_prove_genuinity(self, address: int, buffer_size: int) -> Optional[int]: address_msb = (address >> 32) & 0xFFFF_FFFF address_lsb = address & 0xFFFF_FFFF sentinel_cmd = _tp_sentinel_frame( - TrustProvOperation.PROVE_GENUINITY.tag, args=[address_msb, address_lsb, buffer_size] + TrustProvOperation.PROVE_GENUINITY.tag, + args=[address_msb, address_lsb, buffer_size], ) cmd_packet = CmdPacket( CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, data=sentinel_cmd @@ -1091,7 +1168,9 @@ def tp_prove_genuinity(self, address: int, buffer_size: int) -> Optional[int]: return cmd_response.values[0] return None - def tp_set_wrapped_data(self, address: int, stage: int = 0x4B, control: int = 1) -> bool: + def tp_set_wrapped_data( + self, address: int, stage: int = 0x4B, control: int = 1 + ) -> bool: """Start the process of setting OEM data. :param address: Address where the wrapped data container on target @@ -1130,7 +1209,11 @@ def fuse_program(self, address: int, data: bytes, mem_id: int = 0) -> bool: data_chunks = self._split_data(data=data) mem_id = _clamp_down_memory_id(memory_id=mem_id) cmd_packet = CmdPacket( - CommandTag.FUSE_PROGRAM, CommandFlag.HAS_DATA_PHASE.tag, address, len(data), mem_id + CommandTag.FUSE_PROGRAM, + CommandFlag.HAS_DATA_PHASE.tag, + address, + len(data), + mem_id, ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover @@ -1146,9 +1229,13 @@ def fuse_read(self, address: int, length: int, mem_id: int = 0) -> Optional[byte :param mem_id: Memory ID :return: Data read from the fuse; None in case of a failure """ - logger.info(f"CMD: ReadFuse(address=0x{address:08X}, length={length}, mem_id={mem_id})") + logger.info( + f"CMD: ReadFuse(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) mem_id = _clamp_down_memory_id(memory_id=mem_id) - cmd_packet = CmdPacket(CommandTag.FUSE_READ, CommandFlag.NONE.tag, address, length, mem_id) + cmd_packet = CmdPacket( + CommandTag.FUSE_READ, CommandFlag.NONE.tag, address, length, mem_id + ) cmd_response = self._process_cmd(cmd_packet) if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover # command is not supported in any device, thus we can't measure coverage @@ -1163,7 +1250,9 @@ def update_life_cycle(self, life_cycle: int) -> bool: :return: False in case of any problems, True otherwise. """ logger.info(f"CMD: UpdateLifeCycle (life cycle=0x{life_cycle:02X})") - cmd_packet = CmdPacket(CommandTag.UPDATE_LIFE_CYCLE, CommandFlag.NONE.tag, life_cycle) + cmd_packet = CmdPacket( + CommandTag.UPDATE_LIFE_CYCLE, CommandFlag.NONE.tag, life_cycle + ) return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS def ele_message( @@ -1327,7 +1416,9 @@ def tp_oem_get_cust_cert_dice_puk( :param oem_cust_cert_dice_puk_output_size: The output buffer size in byte :return: The byte count of the OEM Customer Certificate Public Key for DICE """ - logger.info("CMD: [TrustProvisioning] Creates the initial trust provisioning keys") + logger.info( + "CMD: [TrustProvisioning] Creates the initial trust provisioning keys" + ) cmd_packet = CmdPacket( CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, @@ -1669,7 +1760,9 @@ def dsc_hsm_enc_sign( #################### -def _tp_sentinel_frame(command: int, args: List[int], tag: int = 0x17, version: int = 0) -> bytes: +def _tp_sentinel_frame( + command: int, args: List[int], tag: int = 0x17, version: int = 0 +) -> bytes: """Prepare frame used by sentinel.""" data = struct.pack("<4B", command, len(args), version, tag) for item in args: @@ -1680,5 +1773,7 @@ def _tp_sentinel_frame(command: int, args: List[int], tag: int = 0x17, version: def _clamp_down_memory_id(memory_id: int) -> int: if memory_id > 255 or memory_id == 0: return memory_id - logger.warning("Note: memoryId is not required when accessing mapped external memory") + logger.warning( + "Note: memoryId is not required when accessing mapped external memory" + ) return 0 diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py index 599461b3..15a4c828 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py @@ -81,7 +81,11 @@ class ExtMemId(MemIdEnum): class MemId(MemIdEnum): """McuBoot Internal/External Memory Property Tags.""" - INTERNAL_MEMORY = (0, "RAM/FLASH", "Internal RAM/FLASH (Used for the PRINCE configuration)") + INTERNAL_MEMORY = ( + 0, + "RAM/FLASH", + "Internal RAM/FLASH (Used for the PRINCE configuration)", + ) QUAD_SPI0 = (1, "QSPI", "Quad SPI Memory 0") IFR = (4, "IFR0", "Nonvolatile information register 0 (only used by SB loader)") FUSE = (4, "FUSE", "Nonvolatile information register 0 (only used by SB loader)") @@ -131,7 +135,9 @@ def __repr__(self) -> str: return f"Memory region, start: {hex(self.start)}" def __str__(self) -> str: - return f"0x{self.start:08X} - 0x{self.end:08X}; Total Size: {size_fmt(self.size)}" + return ( + f"0x{self.start:08X} - 0x{self.end:08X}; Total Size: {size_fmt(self.size)}" + ) class RamRegion(MemoryRegion): @@ -198,11 +204,19 @@ def __init__(self, mem_id: int, raw_values: Optional[List[int]] = None) -> None: raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None ) self.total_size = ( - raw_values[2] * 1024 if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag else None + raw_values[2] * 1024 + if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag + else None + ) + self.page_size = ( + raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + ) + self.sector_size = ( + raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + ) + self.block_size = ( + raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None ) - self.page_size = raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None - self.sector_size = raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None - self.block_size = raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None self.value = raw_values[0] @property diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py index b8f6f004..8bf30890 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/properties.py @@ -17,7 +17,6 @@ from ..mboot.exceptions import McuBootError from ..utils.misc import Endianness from ..utils.spsdk_enum import SpsdkEnum - from .commands import CommandTag from .error_codes import StatusCode from .memories import ExtMemPropTags, MemoryRegion @@ -227,7 +226,9 @@ class PropertyValueBase: __slots__ = ("tag", "name", "desc") - def __init__(self, tag: int, name: Optional[str] = None, desc: Optional[str] = None) -> None: + def __init__( + self, tag: int, name: Optional[str] = None, desc: Optional[str] = None + ) -> None: """Initialize the base of property. :param tag: Property tag, see: `PropertyTag` @@ -260,7 +261,9 @@ class IntValue(PropertyValueBase): "_fmt", ) - def __init__(self, tag: int, raw_values: List[int], str_format: str = "dec") -> None: + def __init__( + self, tag: int, raw_values: List[int], str_format: str = "dec" + ) -> None: """Initialize the integer-based property object. :param tag: Property tag, see: `PropertyTag` @@ -335,7 +338,9 @@ def to_int(self) -> int: def to_str(self) -> str: """Get stringified property representation.""" - return self._true_string if self.value in self._true_values else self._false_string + return ( + self._true_string if self.value in self._true_values else self._false_string + ) class EnumValue(PropertyValueBase): @@ -410,7 +415,10 @@ def __init__(self, tag: int, raw_values: List[int]) -> None: """ super().__init__(tag) self.value = b"".join( - [int.to_bytes(val, length=4, byteorder=Endianness.LITTLE.value) for val in raw_values] + [ + int.to_bytes(val, length=4, byteorder=Endianness.LITTLE.value) + for val in raw_values + ] ) def to_int(self) -> int: @@ -445,7 +453,9 @@ def __str__(self) -> str: def to_str(self) -> str: """Get stringified property representation.""" - return "\n".join([f" Region {i}: {region}" for i, region in enumerate(self.regions)]) + return "\n".join( + [f" Region {i}: {region}" for i, region in enumerate(self.regions)] + ) class AvailablePeripheralsValue(PropertyValueBase): @@ -546,9 +556,7 @@ def __bool__(self) -> bool: def to_str(self) -> str: """Get stringified property representation.""" - return ( - f"IRQ Port[{self.port}], Pin[{self.pin}] is {'enabled' if self.enabled else 'disabled'}" - ) + return f"IRQ Port[{self.port}], Pin[{self.pin}] is {'enabled' if self.enabled else 'disabled'}" class ExternalMemoryAttributesValue(PropertyValueBase): @@ -577,11 +585,19 @@ def __init__(self, tag: int, raw_values: List[int], mem_id: int = 0) -> None: raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None ) self.total_size = ( - raw_values[2] * 1024 if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag else None + raw_values[2] * 1024 + if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag + else None + ) + self.page_size = ( + raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + ) + self.sector_size = ( + raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + ) + self.block_size = ( + raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None ) - self.page_size = raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None - self.sector_size = raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None - self.block_size = raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None self.value = raw_values[0] def to_str(self) -> str: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py index 1d7f0eb7..49385825 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py @@ -104,7 +104,9 @@ def open(self) -> None: logger.debug(f"Opening interface failed with: {repr(e)}") except Exception as exc: self.close() - raise McuBootConnectionError("UART Interface open operation fails.") from exc + raise McuBootConnectionError( + "UART Interface open operation fails." + ) from exc raise McuBootConnectionError( f"Cannot open UART interface after {self.MAX_UART_OPEN_ATTEMPTS} attempts." ) @@ -213,7 +215,9 @@ def _calc_crc(data: bytes) -> int: crc_function = mkPredefinedCrcFun("xmodem") return crc_function(data) - def _read_frame_header(self, expected_frame_type: Optional[FPType] = None) -> Tuple[int, int]: + def _read_frame_header( + self, expected_frame_type: Optional[FPType] = None + ) -> Tuple[int, int]: """Read frame header and frame type. Return them as tuple of integers. :param expected_frame_type: Check if the frame_type is exactly as expected @@ -307,7 +311,9 @@ def _ping(self) -> None: self.options = response.options @contextmanager - def ping_timeout(self, timeout: int = PING_TIMEOUT_MS) -> Generator[None, None, None]: + def ping_timeout( + self, timeout: int = PING_TIMEOUT_MS + ) -> Generator[None, None, None]: """Context manager for changing UART's timeout. :param timeout: New temporary timeout in milliseconds, defaults to PING_TIMEOUT_MS (500ms) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py index 306eb888..e7bee059 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py @@ -9,8 +9,8 @@ from typing import List, Optional from ..exceptions import SPSDKError -from .protocol.base import MbootProtocolBase from ..utils.interfaces.scanner_helper import InterfaceParams, parse_plugin_config +from .protocol.base import MbootProtocolBase def get_mboot_interface( @@ -43,7 +43,9 @@ def get_mboot_interface( interface_params.extend( [ InterfaceParams(identifier="usb", is_defined=bool(usb), params=usb), - InterfaceParams(identifier="uart", is_defined=bool(port and not buspal), params=port), + InterfaceParams( + identifier="uart", is_defined=bool(port and not buspal), params=port + ), InterfaceParams( identifier="buspal_spi", is_defined=bool(port and buspal and "spi" in buspal), @@ -68,7 +70,9 @@ def get_mboot_interface( ), InterfaceParams(identifier="sdio", is_defined=bool(sdio), params=sdio), InterfaceParams( - identifier=plugin_params[0], is_defined=bool(plugin), params=plugin_params[1] + identifier=plugin_params[0], + is_defined=bool(plugin), + params=plugin_params[1], ), ] ) @@ -89,7 +93,9 @@ def get_mboot_interface( timeout=timeout, ) if len(devices) == 0: - raise SPSDKError(f"Selected '{interface_params[0].identifier}' device not found.") + raise SPSDKError( + f"Selected '{interface_params[0].identifier}' device not found." + ) if len(devices) > 1: raise SPSDKError( f"Multiple '{interface_params[0].identifier}' devices found: {len(devices)}" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py index 8728ae10..09046b63 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/misc.py @@ -199,5 +199,7 @@ def unpack_timestamp(value: int) -> datetime: assert isinstance(value, int) if value < 0 or value > 0xFFFFFFFFFFFFFFFF: raise SPSDKError("Incorrect result of conversion") - start = int(datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() * 1000000) + start = int( + datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() * 1000000 + ) return datetime.fromtimestamp((start + value) / 1000000) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py index 4009a66b..7ca87ae9 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/commands.py @@ -53,7 +53,11 @@ class EnumCmdTag(SpsdkEnum): "WR_KEYSTORE_TO_NV", "Restore key-store restore to non-volatile memory", ) - WR_KEYSTORE_FROM_NV = (0xD, "WR_KEYSTORE_FROM_NV", "Backup key-store from non-volatile memory") + WR_KEYSTORE_FROM_NV = ( + 0xD, + "WR_KEYSTORE_FROM_NV", + "Backup key-store from non-volatile memory", + ) class EnumSectionFlag(SpsdkEnum): @@ -97,7 +101,9 @@ def __repr__(self) -> str: def __str__(self) -> str: tag = ( - EnumCmdTag.get_label(self.tag) if self.tag in EnumCmdTag.tags() else f"0x{self.tag:02X}" + EnumCmdTag.get_label(self.tag) + if self.tag in EnumCmdTag.tags() + else f"0x{self.tag:02X}" ) return ( f"tag={tag}, flags=0x{self.flags:04X}, " @@ -110,7 +116,9 @@ def _raw_data(self, crc: int) -> bytes: :param crc: value to be used :return: binary representation of the header """ - return pack(self.FORMAT, crc, self.tag, self.flags, self.address, self.count, self.data) + return pack( + self.FORMAT, crc, self.tag, self.flags, self.address, self.count, self.data + ) def export(self) -> bytes: """Export command header as bytes.""" @@ -128,7 +136,9 @@ def parse(cls, data: bytes) -> Self: if calcsize(cls.FORMAT) > len(data): raise SPSDKError("Incorrect size") obj = cls(EnumCmdTag.NOP.tag) - (crc, obj.tag, obj.flags, obj.address, obj.count, obj.data) = unpack_from(cls.FORMAT, data) + (crc, obj.tag, obj.flags, obj.address, obj.count, obj.data) = unpack_from( + cls.FORMAT, data + ) if crc != obj.crc: raise SPSDKError("CRC does not match") return obj @@ -164,7 +174,9 @@ def raw_size(self) -> int: return CmdHeader.SIZE # this is default implementation def __repr__(self) -> str: - return "Command: " + str(self._header) # default implementation: use command name + return "Command: " + str( + self._header + ) # default implementation: use command name def __str__(self) -> str: """Return text info about the instance.""" @@ -316,8 +328,12 @@ def parse(cls, data: bytes) -> Self: crc32_function = mkPredefinedCrcFun("crc-32-mpeg") if header.data != crc32_function(cmd_data, 0xFFFFFFFF): raise SPSDKError("Invalid CRC in the command header") - device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT - group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT mem_id = get_memory_id(device_id, group_id) obj = cls(header.address, cmd_data, mem_id) obj.header.data = header.data @@ -352,7 +368,9 @@ def raw_size(self) -> int: size += CmdHeader.SIZE - (size % CmdHeader.SIZE) return size - def __init__(self, address: int, pattern: int, length: Optional[int] = None) -> None: + def __init__( + self, address: int, pattern: int, length: Optional[int] = None + ) -> None: """Initialize Command Fill. :param address: to write data @@ -462,7 +480,9 @@ def spreg(self, value: Optional[int] = None) -> None: self._header.flags = 2 self._header.count = value - def __init__(self, address: int = 0, argument: int = 0, spreg: Optional[int] = None) -> None: + def __init__( + self, address: int = 0, argument: int = 0, spreg: Optional[int] = None + ) -> None: """Initialize Command Jump.""" super().__init__(EnumCmdTag.JUMP) self.address = address @@ -576,7 +596,9 @@ def flags(self, value: int) -> None: """Set command's flag.""" self._header.flags = value - def __init__(self, address: int = 0, length: int = 0, flags: int = 0, mem_id: int = 0) -> None: + def __init__( + self, address: int = 0, length: int = 0, flags: int = 0, mem_id: int = 0 + ) -> None: """Initialize Command Erase.""" super().__init__(EnumCmdTag.ERASE) self.address = address @@ -612,8 +634,12 @@ def parse(cls, data: bytes) -> Self: header = CmdHeader.parse(data) if header.tag != EnumCmdTag.ERASE: raise SPSDKError("Invalid header tag") - device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT - group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT mem_id = get_memory_id(device_id, group_id) return cls(header.address, header.count, header.flags, mem_id) @@ -712,8 +738,12 @@ def parse(cls, data: bytes) -> Self: header = CmdHeader.parse(data) if header.tag != EnumCmdTag.MEM_ENABLE: raise SPSDKError("Invalid header tag") - device_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT - group_id = (header.flags & cls.ROM_MEM_GROUP_ID_MASK) >> cls.ROM_MEM_GROUP_ID_SHIFT + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT mem_id = get_memory_id(device_id, group_id) return cls(header.address, header.count, mem_id) @@ -781,7 +811,12 @@ def data_word2(self, value: int) -> None: self._header.data = value def __init__( - self, address: int, mem_id: int, data_word1: int, data_word2: int = 0, flags: int = 0 + self, + address: int, + mem_id: int, + data_word1: int, + data_word2: int = 0, + flags: int = 0, ) -> None: """Initialize CMD Prog.""" super().__init__(EnumCmdTag.PROG) @@ -821,7 +856,9 @@ def parse(cls, data: bytes) -> Self: header = CmdHeader.parse(data) if header.tag != EnumCmdTag.PROG: raise SPSDKError("Invalid header tag") - mem_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + mem_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT return cls(header.address, mem_id, header.count, header.data, header.flags) @@ -916,7 +953,8 @@ def __init__(self, address: int, controller_id: ExtMemId): if controller_id.tag < 0 or controller_id.tag > 0xFF: raise SPSDKError("Invalid ID of memory") self.header.flags = (self.header.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( - (controller_id.tag << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + (controller_id.tag << self.ROM_MEM_DEVICE_ID_SHIFT) + & self.ROM_MEM_DEVICE_ID_MASK ) self.header.count = ( 4 # this is useless, but it is kept for backward compatibility with elftosb @@ -930,7 +968,9 @@ def address(self) -> int: @property def controller_id(self) -> int: """Return controller ID of the memory to backup key-store or source memory to load key-store back.""" - return (self.header.flags & self.ROM_MEM_DEVICE_ID_MASK) >> self.ROM_MEM_DEVICE_ID_SHIFT + return ( + self.header.flags & self.ROM_MEM_DEVICE_ID_MASK + ) >> self.ROM_MEM_DEVICE_ID_SHIFT @classmethod def parse(cls, data: bytes) -> Self: @@ -944,7 +984,9 @@ def parse(cls, data: bytes) -> Self: if header.tag != cls.cmd_id(): raise SPSDKError("Invalid header tag") address = header.address - controller_id = (header.flags & cls.ROM_MEM_DEVICE_ID_MASK) >> cls.ROM_MEM_DEVICE_ID_SHIFT + controller_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT return cls(address, ExtMemId.from_tag(controller_id)) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py index 0da45af4..ad262b90 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/headers.py @@ -94,7 +94,9 @@ def __str__(self) -> str: nfo += f" Header Blocks: {self.header_blocks}\n" nfo += f" Sections MAC Count: {self.max_section_mac_count}\n" nfo += f" Key Blob Block Count: {self.key_blob_block_count}\n" - nfo += f" Timestamp: {self.timestamp.strftime('%H:%M:%S (%d.%m.%Y)')}\n" + nfo += ( + f" Timestamp: {self.timestamp.strftime('%H:%M:%S (%d.%m.%Y)')}\n" + ) nfo += f" Product Version: {self.product_version}\n" nfo += f" Component Version: {self.component_version}\n" nfo += f" Build Number: {self.build_number}\n" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py index 8d8bb0d4..b0748dc6 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py @@ -39,7 +39,6 @@ write_file, ) from ...utils.schema_validator import CommentedConfig, check_config - from . import sly_bd_parser as bd_parser from .commands import CmdHeader from .headers import ImageHeaderV2 @@ -155,7 +154,8 @@ def __init__( self._dek: bytes = advanced_params.dek self._mac: bytes = advanced_params.mac if ( - len(self._dek) != self.HEADER_MAC_SIZE and len(self._mac) != self.HEADER_MAC_SIZE + len(self._dek) != self.HEADER_MAC_SIZE + and len(self._mac) != self.HEADER_MAC_SIZE ): # pragma: no cover # condition checked in SBV2xAdvancedParams constructor raise SPSDKError("Invalid dek or mac") self._header = ImageHeaderV2( @@ -223,7 +223,9 @@ def cert_block(self, value: Optional[CertBlockV1]) -> None: """ if value is not None: if not self.signed: - raise SPSDKError("Certificate block cannot be used unless SB file is signed") + raise SPSDKError( + "Certificate block cannot be used unless SB file is signed" + ) self._cert_section = CertSectionV2(value) if value else None @property @@ -258,7 +260,9 @@ def raw_size(self) -> int: if self.signed: cert_block = self.cert_block - if not cert_block: # pragma: no cover # already checked in raw_size_without_signature + if ( + not cert_block + ): # pragma: no cover # already checked in raw_size_without_signature raise SPSDKError("Certificate block not present") size += cert_block.signature_size @@ -287,14 +291,18 @@ def update(self) -> None: self._header.first_boot_tag_block = SecBootBlckSize.to_num_blocks(data_size) # ... self._header.flags = 0x08 if self.signed else 0x04 - self._header.image_blocks = SecBootBlckSize.to_num_blocks(self.raw_size_without_signature) + self._header.image_blocks = SecBootBlckSize.to_num_blocks( + self.raw_size_without_signature + ) self._header.header_blocks = SecBootBlckSize.to_num_blocks(self._header.SIZE) self._header.max_section_mac_count = 0 if self.signed: self._header.offset_to_certificate_block = ( self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE ) - self._header.offset_to_certificate_block += CmdHeader.SIZE + CertSectionV2.HMAC_SIZE * 2 + self._header.offset_to_certificate_block += ( + CmdHeader.SIZE + CertSectionV2.HMAC_SIZE * 2 + ) self._header.max_section_mac_count = 1 for boot_sect in self._boot_sections: boot_sect.is_last = True # this is unified with elftosb @@ -332,7 +340,9 @@ def add_boot_section(self, section: BootSectionV2) -> None: """ if not isinstance(section, BootSectionV2): raise SPSDKError("Section is not instance of BootSectionV2 class") - duplicate_uid = find_first(self._boot_sections, lambda bs: bs.uid == section.uid) + duplicate_uid = find_first( + self._boot_sections, lambda bs: bs.uid == section.uid + ) if duplicate_uid is not None: raise SPSDKError(f"Boot section with duplicate UID: {str(section.uid)}") self._boot_sections.append(section) @@ -371,7 +381,9 @@ def export(self, padding: Optional[bytes] = None) -> bytes: counter = Counter(self._header.nonce) counter.increment(SecBootBlckSize.to_num_blocks(len(data))) if self._cert_section is not None: - cert_sect_bin = self._cert_section.export(dek=self.dek, mac=self.mac, counter=counter) + cert_sect_bin = self._cert_section.export( + dek=self.dek, mac=self.mac, counter=counter + ) counter.increment(SecBootBlckSize.to_num_blocks(len(cert_sect_bin))) data += cert_sect_bin # Add Boot Sections data @@ -380,7 +392,9 @@ def export(self, padding: Optional[bytes] = None) -> bytes: # Add Signature data if self.signed: if self.signature_provider is None: - raise SPSDKError("Signature provider is not assigned, cannot sign the image.") + raise SPSDKError( + "Signature provider is not assigned, cannot sign the image." + ) if self.cert_block is None: raise SPSDKError("Certificate block is not assigned.") @@ -446,15 +460,21 @@ def parse(cls, data: bytes, kek: bytes = bytes()) -> Self: ) # Parse Certificate section if header.flags == 0x08: - cert_sect = CertSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) + cert_sect = CertSectionV2.parse( + data, index, dek=dek, mac=mac, counter=counter + ) obj._cert_section = cert_sect index += cert_sect.raw_size # Check Signature - if not cert_sect.cert_block.verify_data(data[image_size:], data[:image_size]): + if not cert_sect.cert_block.verify_data( + data[image_size:], data[:image_size] + ): raise SPSDKError("Parsing Certification section failed") # Parse Boot Sections while index < (image_size): - boot_section = BootSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) + boot_section = BootSectionV2.parse( + data, index, dek=dek, mac=mac, counter=counter + ) obj.add_boot_section(boot_section) index += boot_section.raw_size return obj @@ -676,7 +696,9 @@ def export(self, padding: Optional[bytes] = None) -> bytes: if self.cert_block is None: raise SPSDKError("Certificate is not assigned") if self.signature_provider is None: - raise SPSDKError("Signature provider is not assigned, cannot sign the image") + raise SPSDKError( + "Signature provider is not assigned, cannot sign the image" + ) # Update internals self.update() # Export Boot Sections @@ -700,7 +722,9 @@ def export(self, padding: Optional[bytes] = None) -> bytes: signed_data = self._header.export(padding=padding) # Add HMAC data first_bs_hmac_count = self.boot_sections[0].hmac_count - hmac_data = bs_data[CmdHeader.SIZE : CmdHeader.SIZE + (first_bs_hmac_count * 32) + 32] + hmac_data = bs_data[ + CmdHeader.SIZE : CmdHeader.SIZE + (first_bs_hmac_count * 32) + 32 + ] hmac_bytes = hmac(self.mac, hmac_data) signed_data += hmac_bytes # Add KeyBlob data @@ -826,7 +850,9 @@ def get_supported_families() -> List[str]: return get_families(DatabaseManager.SB21) @classmethod - def get_commands_validation_schemas(cls, family: Optional[str] = None) -> List[Dict[str, Any]]: + def get_commands_validation_schemas( + cls, family: Optional[str] = None + ) -> List[Dict[str, Any]]: """Create the list of validation schemas. :param family: Device family filter, if None all commands are returned. @@ -839,13 +865,13 @@ def get_commands_validation_schemas(cls, family: Optional[str] = None) -> List[D db = get_db(family, "latest") # remove unused command for current family supported_commands = db.get_list(DatabaseManager.SB21, "supported_commands") - list_of_commands: List[Dict] = schemas[0]["properties"]["sections"]["items"][ - "properties" - ]["commands"]["items"]["oneOf"] + list_of_commands: List[Dict] = schemas[0]["properties"]["sections"][ + "items" + ]["properties"]["commands"]["items"]["oneOf"] - schemas[0]["properties"]["sections"]["items"]["properties"]["commands"]["items"][ - "oneOf" - ] = [ + schemas[0]["properties"]["sections"]["items"]["properties"]["commands"][ + "items" + ]["oneOf"] = [ command for command in list_of_commands if list(command["properties"].keys())[0] in supported_commands @@ -854,7 +880,9 @@ def get_commands_validation_schemas(cls, family: Optional[str] = None) -> List[D return schemas @classmethod - def get_validation_schemas(cls, family: Optional[str] = None) -> List[Dict[str, Any]]: + def get_validation_schemas( + cls, family: Optional[str] = None + ) -> List[Dict[str, Any]]: """Create the list of validation schemas. :param family: Device family @@ -865,7 +893,9 @@ def get_validation_schemas(cls, family: Optional[str] = None) -> List[Dict[str, schemas: List[Dict[str, Any]] = [] schemas.extend([mbi_schema[x] for x in ["signature_provider", "cert_block_v1"]]) - schemas.extend([sb2_schema[x] for x in ["sb2_output", "sb2_family", "common", "sb2"]]) + schemas.extend( + [sb2_schema[x] for x in ["sb2_output", "sb2_family", "common", "sb2"]] + ) add_keyblob = True @@ -922,7 +952,9 @@ def parse_sb21_config( parser = bd_parser.BDParser() parsed_conf = parser.parse(text=bd_file_content, extern=external_files) if parsed_conf is None: - raise SPSDKError("Invalid bd file, secure binary file generation terminated") + raise SPSDKError( + "Invalid bd file, secure binary file generation terminated" + ) except SPSDKError: parsed_conf = load_configuration(config_path) config_dir = os.path.dirname(config_path) @@ -959,7 +991,9 @@ def load_from_config( :return: Instance of Secure Binary V2.1 class """ flags = config["options"].get( - "flags", BootImageV21.FLAGS_SHA_PRESENT_BIT | BootImageV21.FLAGS_ENCRYPTED_SIGNED_BIT + "flags", + BootImageV21.FLAGS_SHA_PRESENT_BIT + | BootImageV21.FLAGS_ENCRYPTED_SIGNED_BIT, ) # Flags may be a hex string flags = value_to_int(flags) @@ -1024,7 +1058,9 @@ def load_from_config( secure_binary.cert_block = cert_block if not signature_provider: - signing_key_path = config.get("signPrivateKey", config.get("mainCertPrivateKeyFile")) + signing_key_path = config.get( + "signPrivateKey", config.get("mainCertPrivateKeyFile") + ) signature_provider = get_signature_provider( sp_cfg=config.get("signProvider"), local_file_key=signing_key_path, @@ -1034,7 +1070,9 @@ def load_from_config( secure_binary.signature_provider = signature_provider if not rkth_out_path: - rkth_out_path = config.get("RKTHOutputPath", os.path.join(os.getcwd(), "hash.bin")) + rkth_out_path = config.get( + "RKTHOutputPath", os.path.join(os.getcwd(), "hash.bin") + ) assert isinstance(rkth_out_path, str), "Hash of hashes path must be string" write_file(secure_binary.cert_block.rkth, rkth_out_path, mode="wb") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py index eb5e6789..9c5c870b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sb_21_helper.py @@ -213,7 +213,9 @@ def _prog(self, cmd_args: dict) -> CmdProg: else: raise SPSDKError("Unsupported program command arguments") - return CmdProg(address=address, data_word1=data_word1, data_word2=data_word2, mem_id=mem_id) + return CmdProg( + address=address, data_word1=data_word1, data_word2=data_word2, mem_id=mem_id + ) def _erase_cmd_handler(self, cmd_args: dict) -> CmdErase: """Returns a CmdErase object initialized based on cmd_args. @@ -317,10 +319,14 @@ def _encrypt(self, cmd_args: dict) -> CmdLoad: counter = bytes.fromhex(valid_keyblob["keyblob_content"][0]["counter"]) byte_swap = valid_keyblob["keyblob_content"][0].get("byte_swap", False) - keyblob = KeyBlob(start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter) + keyblob = KeyBlob( + start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter + ) # Encrypt only if the ADE and VLD flags are set - if bool(end_addr & keyblob.KEY_FLAG_ADE) and bool(end_addr & keyblob.KEY_FLAG_VLD): + if bool(end_addr & keyblob.KEY_FLAG_ADE) and bool( + end_addr & keyblob.KEY_FLAG_VLD + ): encoded_data = keyblob.encrypt_image( base_address=address, data=align_block(data, 512), byte_swap=byte_swap ) @@ -363,7 +369,9 @@ def _keywrap(self, cmd_args: dict) -> CmdLoad: key = bytes.fromhex(valid_keyblob["keyblob_content"][0]["key"]) counter = bytes.fromhex(valid_keyblob["keyblob_content"][0]["counter"]) - blob = KeyBlob(start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter) + blob = KeyBlob( + start_addr=start_addr, end_addr=end_addr, key=key, counter_iv=counter + ) encoded_keyblob = blob.export(kek=otfad_key) logger.info(f"Creating wrapped keyblob: \n{str(blob)}") @@ -446,7 +454,9 @@ def _validate_keyblob(self, keyblobs: List, keyblob_id: Number) -> Optional[Dict for key in ["start", "end", "key", "counter"]: if key not in kb_content[0]: - raise SPSDKError(f"Keyblob {keyblob_id} is missing '{key}' definition!") + raise SPSDKError( + f"Keyblob {keyblob_id} is missing '{key}' definition!" + ) return keyblob diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py index a9cf938a..4fe41a88 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sections.py @@ -16,8 +16,13 @@ from ...sbfile.misc import SecBootBlckSize from ...utils.abstract import BaseClass from ...utils.crypto.cert_blocks import CertBlockV1 - -from .commands import CmdBaseClass, CmdHeader, EnumCmdTag, EnumSectionFlag, parse_command +from .commands import ( + CmdBaseClass, + CmdHeader, + EnumCmdTag, + EnumSectionFlag, + parse_command, +) ######################################################################################################################## # Boot Image Sections @@ -59,7 +64,9 @@ def hmac_count(self) -> int: raw_size += cmd.raw_size if raw_size > 0: block_count = (raw_size + 15) // 16 - hmac_count = self._hmac_count if block_count >= self._hmac_count else block_count + hmac_count = ( + self._hmac_count if block_count >= self._hmac_count else block_count + ) return hmac_count @property @@ -158,7 +165,9 @@ def export( # Encrypt commands encrypted_commands = b"" for index in range(0, len(commands_data), 16): - encrypted_block = aes_ctr_encrypt(dek, commands_data[index : index + 16], counter.value) + encrypted_block = aes_ctr_encrypt( + dek, commands_data[index : index + 16], counter.value + ) encrypted_commands += encrypted_block counter.increment() # Calculate HMAC of commands @@ -206,7 +215,9 @@ def parse( raise SPSDKError("Invalid type of counter") # Get Header specific data header_encrypted = data[offset : offset + CmdHeader.SIZE] - header_hmac_data = data[offset + CmdHeader.SIZE : offset + CmdHeader.SIZE + cls.HMAC_SIZE] + header_hmac_data = data[ + offset + CmdHeader.SIZE : offset + CmdHeader.SIZE + cls.HMAC_SIZE + ] offset += CmdHeader.SIZE + cls.HMAC_SIZE # Check header HMAC if header_hmac_data != hmac(mac, header_encrypted): @@ -241,7 +252,9 @@ def parse( for hmac_index in range(0, len(encrypted_commands), 16): encr_block = encrypted_commands[hmac_index : hmac_index + 16] decrypted_block = ( - encr_block if plain_sect else aes_ctr_decrypt(dek, encr_block, counter.value) + encr_block + if plain_sect + else aes_ctr_decrypt(dek, encr_block, counter.value) ) decrypted_commands += decrypted_block counter.increment() @@ -281,7 +294,8 @@ def __init__(self, cert_block: CertBlockV1): """Initialize CertBlockV1.""" assert isinstance(cert_block, CertBlockV1) self._header = CmdHeader( - EnumCmdTag.TAG.tag, EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag + EnumCmdTag.TAG.tag, + EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag, ) self._header.address = self.SECT_MARK self._header.count = cert_block.raw_size // 16 @@ -367,7 +381,9 @@ def parse( header = CmdHeader.parse(header_encrypted) if header.tag != EnumCmdTag.TAG: raise SPSDKError(f"Invalid Header TAG: 0x{header.tag:02X}") - if header.flags != (EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag): + if header.flags != ( + EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag + ): raise SPSDKError(f"Invalid Header FLAGS: 0x{header.flags:02X}") if header.address != cls.SECT_MARK: raise SPSDKError(f"Invalid Section Mark: 0x{header.address:08X}") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py index cdc8283a..594502ce 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_lexer.py @@ -44,7 +44,8 @@ def __str__(self) -> str: """ return f"{self.name}, {self.t}, {self.value}" -class BDLexer(Lexer): # type: ignore + +class BDLexer(Lexer): # type: ignore """Lexer for bd files.""" def __init__(self) -> None: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py index 932f912e..d8281180 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/sly_bd_parser.py @@ -16,7 +16,6 @@ from sly.yacc import YaccProduction from ...exceptions import SPSDKError - from . import sly_bd_lexer as bd_lexer @@ -26,7 +25,7 @@ # is disabled. # too-many-lines : the class can't be shortened, as all the methods represent # rules. -class BDParser(Parser): # type: ignore +class BDParser(Parser): # type: ignore """Command (BD) file parser. The parser is based on SLY framework (python implementation of Lex/YACC) @@ -198,7 +197,9 @@ def option_def(self, token: YaccProduction) -> Dict: # column = BDParser._find_column(self._input, token) # print(f"Unknown option in options block at {token.lineno}/{column}: {token.IDENT}") # self.error(token) - self._variables.append(bd_lexer.Variable(token.IDENT, "option", token.const_expr)) + self._variables.append( + bd_lexer.Variable(token.IDENT, "option", token.const_expr) + ) token.option_def["options"].update({token.IDENT: token.const_expr}) return token.option_def @@ -229,7 +230,9 @@ def constant_def(self, token: YaccProduction): :param token: object holding the content defined in decorator. """ - self._variables.append(bd_lexer.Variable(token.IDENT, "constant", token.bool_expr)) + self._variables.append( + bd_lexer.Variable(token.IDENT, "constant", token.bool_expr) + ) @_("empty") # type: ignore def constant_def(self, token: YaccProduction) -> Dict: @@ -352,7 +355,10 @@ def keyblob_block(self, token: YaccProduction) -> Dict: :param token: object holding the content defined in decorator. :return: dictionary holding the content of keyblob block. """ - dictionary = {"keyblob_id": token.int_const_expr, "keyblob_content": token.keyblob_contents} + dictionary = { + "keyblob_id": token.int_const_expr, + "keyblob_content": token.keyblob_contents, + } dictionary["keyblob_id"] = token.int_const_expr dictionary["keyblob_content"] = token.keyblob_contents self._keyblobs.append(dictionary) @@ -660,7 +666,10 @@ def load_stmt(self, token: YaccProduction) -> Dict: :return: dictionary holding the content of a load statement. """ # pattern with load options means load -> program command - if token.load_data.get("pattern") is not None and token.load_opt.get("load_opt") is None: + if ( + token.load_data.get("pattern") is not None + and token.load_opt.get("load_opt") is None + ): cmd = "fill" else: cmd = "load" @@ -705,7 +714,10 @@ def load_data(self, token: YaccProduction) -> Dict: :return: dictionary holding the content of load data. """ if isinstance(token.int_const_expr, str): - self.error(token, f": identifier '{token.int_const_expr}' is not a source identifier.") + self.error( + token, + f": identifier '{token.int_const_expr}' is not a source identifier.", + ) retval = {"N/A": "N/A"} else: retval = {"pattern": token.int_const_expr} diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py index 9342a703..9337d045 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py @@ -67,7 +67,9 @@ def calc_crc(self, data: bytes, address: int, count: int) -> None: def open(self) -> None: """Open uboot device.""" - self._device = Serial(port=self.port, timeout=self.timeout, baudrate=self.baudrate) + self._device = Serial( + port=self.port, timeout=self.timeout, baudrate=self.baudrate + ) self.is_opened = True def close(self) -> None: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py index d3314663..df587e66 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/cert_blocks.py @@ -23,7 +23,11 @@ from ...crypto.keys import PrivateKeyRsa, PublicKeyEcc from ...crypto.signature_provider import SignatureProvider, get_signature_provider from ...crypto.types import SPSDKEncoding -from ...crypto.utils import extract_public_key, extract_public_key_from_data, get_matching_key_id +from ...crypto.utils import ( + extract_public_key, + extract_public_key_from_data, + get_matching_key_id, +) from ...exceptions import ( SPSDKError, SPSDKNotImplementedError, @@ -95,7 +99,9 @@ def get_cert_block_class(cls, family: str) -> Type["CertBlock"]: for cert_block_class in cls.get_cert_block_classes(): if family in cert_block_class.get_supported_families(): return cert_block_class - raise SPSDKError(f"Family '{family}' is not supported in any certification block.") + raise SPSDKError( + f"Family '{family}' is not supported in any certification block." + ) @classmethod def get_all_supported_families(cls) -> List[str]: @@ -105,7 +111,10 @@ def get_all_supported_families(cls) -> List[str]: return [ family for family in families - if "srk" not in get_db(family, "latest").get_str(DatabaseManager.CERT_BLOCK, "rot_type") + if "srk" + not in get_db(family, "latest").get_str( + DatabaseManager.CERT_BLOCK, "rot_type" + ) ] @classmethod @@ -141,7 +150,9 @@ def get_root_private_key_file(cls, config: Dict[str, Any]) -> Optional[str]: :param config: Configuration to be searched. :return: Root private key file path. """ - private_key_file = config.get("signPrivateKey", config.get("mainRootCertPrivateKeyFile")) + private_key_file = config.get( + "signPrivateKey", config.get("mainRootCertPrivateKeyFile") + ) if private_key_file and not isinstance(private_key_file, str): raise SPSDKTypeError("Root private key file must be a string type") return private_key_file @@ -169,7 +180,9 @@ def find_main_cert_index( public_keys = [] for root_crt_file in root_certificates: try: - public_key = extract_public_key(root_crt_file, search_paths=search_paths) + public_key = extract_public_key( + root_crt_file, search_paths=search_paths + ) public_keys.append(public_key) except SPSDKError: continue @@ -195,11 +208,17 @@ def get_main_cert_index( """ root_cert_id = config.get("mainRootCertId") cert_chain_id = config.get("mainCertChainId") - if root_cert_id is not None and cert_chain_id is not None and root_cert_id != cert_chain_id: + if ( + root_cert_id is not None + and cert_chain_id is not None + and root_cert_id != cert_chain_id + ): raise SPSDKError( "The mainRootCertId and mainRootCertId are specified and have different values." ) - found_cert_id = cls.find_main_cert_index(config=config, search_paths=search_paths) + found_cert_id = cls.find_main_cert_index( + config=config, search_paths=search_paths + ) if root_cert_id is None and cert_chain_id is None: if found_cert_id is not None: return found_cert_id @@ -209,7 +228,9 @@ def get_main_cert_index( try: cert_id = int(cert_id) except ValueError as exc: - raise SPSDKValueError(f"A certificate index is not a number: {cert_id}") from exc + raise SPSDKValueError( + f"A certificate index is not a number: {cert_id}" + ) from exc if found_cert_id is not None and found_cert_id != cert_id: logger.warning("Defined certificate does not match the private key.") return cert_id @@ -225,7 +246,9 @@ class CertBlockHeader(BaseClass): SIZE = calcsize(FORMAT) SIGNATURE = b"cert" - def __init__(self, version: str = "1.0", flags: int = 0, build_number: int = 0) -> None: + def __init__( + self, version: str = "1.0", flags: int = 0, build_number: int = 0 + ) -> None: """Constructor. :param version: Version of the certificate in format n.n @@ -421,7 +444,9 @@ def image_length(self, value: int) -> None: raise SPSDKError("Invalid image length") self._header.image_length = value - def __init__(self, version: str = "1.0", flags: int = 0, build_number: int = 0) -> None: + def __init__( + self, version: str = "1.0", flags: int = 0, build_number: int = 0 + ) -> None: """Constructor. :param version: of the certificate in format n.n @@ -436,7 +461,9 @@ def __init__(self, version: str = "1.0", flags: int = 0, build_number: int = 0) def __len__(self) -> int: return len(self._cert) - def set_root_key_hash(self, index: int, key_hash: Union[bytes, bytearray, Certificate]) -> None: + def set_root_key_hash( + self, index: int, key_hash: Union[bytes, bytearray, Certificate] + ) -> None: """Add Root Key Hash into RKHT. Note: Multiple root public keys are supported to allow for key revocation. @@ -469,14 +496,20 @@ def add_certificate(self, cert: Union[bytes, Certificate]) -> None: else: raise SPSDKError("Invalid parameter type (cert)") if cert_obj.version.name != "v3": - raise SPSDKError("Expected certificate v3 but received: " + cert_obj.version.name) + raise SPSDKError( + "Expected certificate v3 but received: " + cert_obj.version.name + ) if self._cert: # chain certificate? last_cert = self._cert[-1] # verify that it is signed by parent key if not cert_obj.validate(last_cert): - raise SPSDKError("Chain certificate cannot be verified using parent public key") + raise SPSDKError( + "Chain certificate cannot be verified using parent public key" + ) else: # root certificate if not cert_obj.self_signed: - raise SPSDKError(f"Root certificate must be self-signed.\n{str(cert_obj)}") + raise SPSDKError( + f"Root certificate must be self-signed.\n{str(cert_obj)}" + ) self._cert.append(cert_obj) self._header.cert_count += 1 self._header.cert_table_length += cert_obj.raw_size + 4 @@ -490,9 +523,7 @@ def __str__(self) -> str: nfo += " Public Root Keys Hash e.g. RKH (SHA256):\n" rkh_index = self.rkh_index for index, root_key in enumerate(self._rkht.rkh_list): - nfo += ( - f" {index}) {root_key.hex().upper()} {'<- Used' if index == rkh_index else ''}\n" - ) + nfo += f" {index}) {root_key.hex().upper()} {'<- Used' if index == rkh_index else ''}\n" rkth = self.rkth nfo += f" RKTH (SHA256): {rkth.hex().upper()}\n" for index, fuse in enumerate(self.rkth_fuses): @@ -537,7 +568,9 @@ def export(self) -> bytes: if self._cert[-1].ca: raise SPSDKError("The last chain certificate must not be CA.") if not all(cert.ca for cert in self._cert[:-1]): - raise SPSDKError("All certificates except the last chain certificate must be CA") + raise SPSDKError( + "All certificates except the last chain certificate must be CA" + ) # Export data = self.header.export() for cert in self._cert: @@ -559,16 +592,24 @@ def parse(cls, data: bytes) -> Self: """ header = CertBlockHeader.parse(data) offset = CertBlockHeader.SIZE - if len(data) < (header.cert_table_length + (RKHTv1.RKHT_SIZE * RKHTv1.RKH_SIZE)): - raise SPSDKError("Length of the data doesn't match Certificate Block length") - obj = cls(version=header.version, flags=header.flags, build_number=header.build_number) + if len(data) < ( + header.cert_table_length + (RKHTv1.RKHT_SIZE * RKHTv1.RKH_SIZE) + ): + raise SPSDKError( + "Length of the data doesn't match Certificate Block length" + ) + obj = cls( + version=header.version, flags=header.flags, build_number=header.build_number + ) for _ in range(header.cert_count): cert_len = unpack_from(" str: """Generate configuration for certification block v1.""" val_schemas = CertBlockV1.get_validation_schemas() val_schemas.append( - DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)[ + "cert_block_output" + ] ) - return CommentedConfig("Certification Block V1 template", val_schemas).get_template() + return CommentedConfig( + "Certification Block V1 template", val_schemas + ).get_template() def create_config(self, data_path: str) -> str: """Create configuration of the Certification block Image.""" @@ -641,7 +686,9 @@ def from_config( search_paths.append(os.path.dirname(cert_block)) else: search_paths = [os.path.dirname(cert_block)] - return cls.from_config(load_configuration(cert_block, search_paths), search_paths) + return cls.from_config( + load_configuration(cert_block, search_paths), search_paths + ) image_build_number = value_to_int(config.get("imageBuildNumber", 0)) root_certificates: List[List[str]] = [[] for _ in range(4)] @@ -653,7 +700,9 @@ def from_config( root_certificates[3].append(config.get("rootCertificate3File", None)) main_cert_chain_id = cls.get_main_cert_index(config, search_paths=search_paths) if root_certificates[main_cert_chain_id][0] is None: - raise SPSDKError(f"A key rootCertificate{main_cert_chain_id}File must be defined") + raise SPSDKError( + f"A key rootCertificate{main_cert_chain_id}File must be defined" + ) # get all certificate chain related keys from config pattern = f"chainCertificate{main_cert_chain_id}File[0-3]" @@ -676,7 +725,9 @@ def from_config( for cert_idx, cert_path_list in enumerate(root_certificates): if cert_path_list[0]: if empty_rec: - raise SPSDKError("There are gaps in rootCertificateXFile definition") + raise SPSDKError( + "There are gaps in rootCertificateXFile definition" + ) cert_data = Certificate.load( find_file(str(cert_path_list[0]), search_paths=search_paths) ).export(SPSDKEncoding.DER) @@ -707,11 +758,13 @@ def create_certificate_cfg(root_id: int, chain_id: int) -> Optional[str]: assert used_cert_id is not None cfg["mainRootCertId"] = used_cert_id - cfg[f"rootCertificate{used_cert_id}File"] = create_certificate_cfg(used_cert_id, 0) + cfg[f"rootCertificate{used_cert_id}File"] = create_certificate_cfg( + used_cert_id, 0 + ) for chain_ix in range(4): - cfg[f"chainCertificate{used_cert_id}File{chain_ix}"] = create_certificate_cfg( - used_cert_id, chain_ix + 1 - ) + cfg[ + f"chainCertificate{used_cert_id}File{chain_ix}" + ] = create_certificate_cfg(used_cert_id, chain_ix + 1) return cfg @@ -863,7 +916,9 @@ def __str__(self) -> str: if self._rkht.rkh_list: info += f"CTRK Hash table: {self._rkht.export().hex()}\n" if self.root_public_key: - info += f"Root public key: {str(convert_to_ecc_key(self.root_public_key))}\n" + info += ( + f"Root public key: {str(convert_to_ecc_key(self.root_public_key))}\n" + ) return info @@ -894,7 +949,9 @@ def calculate(self) -> None: """ # pylint: disable=invalid-name if not self.root_certs_input: - raise SPSDKError("Root Key Record: The root of trust certificates are not specified.") + raise SPSDKError( + "Root Key Record: The root of trust certificates are not specified." + ) self.root_certs = [convert_to_ecc_key(cert) for cert in self.root_certs_input] self.flags = self._calculate_flags() self._rkht = RKHTv21.from_keys(keys=self.root_certs) @@ -932,7 +989,9 @@ def parse(cls, data: bytes) -> Self: used_rot_ix = (flags & 0xF00) >> 8 number_of_hashes = (flags & 0xF0) >> 4 rotkh_len = {0x0: 32, 0x1: 32, 0x2: 48}[flags & 0xF] - root_key_record = cls(ca_flag=ca_flag, root_certs=[], used_root_cert=used_rot_ix) + root_key_record = cls( + ca_flag=ca_flag, root_certs=[], used_root_cert=used_rot_ix + ) root_key_record.flags = flags offset = 4 # move offset just after FLAGS if number_of_hashes > 1: @@ -943,7 +1002,13 @@ def parse(cls, data: bytes) -> Self: root_key_record._rkht = ( RKHTv21.parse(rkht, cls.get_hash_algorithm(flags)) if number_of_hashes > 1 - else RKHTv21([get_hash(root_key_record.root_public_key, cls.get_hash_algorithm(flags))]) + else RKHTv21( + [ + get_hash( + root_key_record.root_public_key, cls.get_hash_algorithm(flags) + ) + ] + ) ) return root_key_record @@ -980,12 +1045,18 @@ def __init__( raise SPSDKError( f"ISK user data is too big ({len(self.user_data)} B). Max size is: {isk_data_limit} B." ) - isk_data_alignment = db.get_int(DatabaseManager.CERT_BLOCK, "isk_data_alignment") + isk_data_alignment = db.get_int( + DatabaseManager.CERT_BLOCK, "isk_data_alignment" + ) if len(self.user_data) % isk_data_alignment: - raise SPSDKError(f"ISK user data is not aligned to {isk_data_alignment} B.") + raise SPSDKError( + f"ISK user data is not aligned to {isk_data_alignment} B." + ) self.signature = bytes() self.coordinate_length = ( - self.signature_provider.signature_length // 2 if self.signature_provider else 0 + self.signature_provider.signature_length // 2 + if self.signature_provider + else 0 ) self.isk_public_key_data = self.isk_cert.export() if self.isk_cert else bytes() @@ -1008,7 +1079,9 @@ def expected_size(self) -> int: self.signature_provider.signature_length if self.signature_provider else 0 ) pub_key_len = ( - self.isk_cert.coordinate_size * 2 if self.isk_cert else len(self.isk_public_key_data) + self.isk_cert.coordinate_size * 2 + if self.isk_cert + else len(self.isk_public_key_data) ) offset = 4 if self.offset_present else 0 @@ -1059,7 +1132,9 @@ def create_isk_signature(self, key_record_data: bytes, force: bool = False) -> N if self.signature and not force: return if not self.signature_provider: - raise SPSDKError("ISK Certificate: The signature provider is not specified.") + raise SPSDKError( + "ISK Certificate: The signature provider is not specified." + ) if self.offset_present: data = key_record_data + pack( "<3L", self.signature_offset, self.constraints, self.flags @@ -1095,7 +1170,9 @@ def parse(cls, data: bytes, signature_size: int) -> Self: # type: ignore # pyli """ (signature_offset, constraints, isk_flags) = unpack_from("<3L", data) header_word_cnt = 3 - if signature_offset & 0xFFFF == 0x4D43: # This means that certificate has no offset + if ( + signature_offset & 0xFFFF == 0x4D43 + ): # This means that certificate has no offset (constraints, isk_flags) = unpack_from("<2L", data) signature_offset = 72 header_word_cnt = 2 @@ -1176,7 +1253,9 @@ def create_isk_signature( if self.signature and not force: return if not signature_provider: - raise SPSDKError("ISK Certificate: The signature provider is not specified.") + raise SPSDKError( + "ISK Certificate: The signature provider is not specified." + ) data = pack(self.HEADER_FORMAT, self.MAGIC, self.VERSION, self.constraints) data += self.isk_public_key_data @@ -1377,7 +1456,8 @@ def from_config( ) from e root_certs = [ - load_binary(cert_file, search_paths=search_paths) for cert_file in root_certificates + load_binary(cert_file, search_paths=search_paths) + for cert_file in root_certificates ] user_data = None @@ -1393,7 +1473,9 @@ def from_config( search_paths=search_paths, ) - isk_public_key = config.get("iskPublicKey", config.get("signingCertificateFile")) + isk_public_key = config.get( + "iskPublicKey", config.get("signingCertificateFile") + ) isk_cert = load_binary(isk_public_key, search_paths=search_paths) isk_sign_data_path = config.get("iskCertData", config.get("signCertData")) @@ -1401,7 +1483,10 @@ def from_config( user_data = load_binary(isk_sign_data_path, search_paths=search_paths) isk_constraint = value_to_int( - config.get("iskCertificateConstraint", config.get("signingCertificateConstraint", "0")) + config.get( + "iskCertificateConstraint", + config.get("signingCertificateConstraint", "0"), + ) ) family = config.get("family") cert_block = cls( @@ -1425,7 +1510,9 @@ def validate(self) -> None: """ self.header.parse(self.header.export()) if self.isk_certificate and not self.isk_certificate.signature: - if not isinstance(self.isk_certificate.signature_provider, SignatureProvider): + if not isinstance( + self.isk_certificate.signature_provider, SignatureProvider + ): raise SPSDKError("Invalid ISK certificate.") @staticmethod @@ -1433,7 +1520,9 @@ def generate_config_template(family: Optional[str] = None) -> str: """Generate configuration for certification block v21.""" val_schemas = CertBlockV21.get_validation_schemas() val_schemas.append( - DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)[ + "cert_block_output" + ] ) if family: @@ -1442,7 +1531,9 @@ def generate_config_template(family: Optional[str] = None) -> str: if "properties" in schema and "family" in schema["properties"]: schema["properties"]["family"]["template_value"] = family break - return CommentedConfig("Certification Block V21 template", val_schemas).get_template() + return CommentedConfig( + "Certification Block V21 template", val_schemas + ).get_template() def get_config(self, output_folder: str) -> Dict[str, Any]: """Create configuration dictionary of the Certification block Image. @@ -1458,10 +1549,15 @@ def get_config(self, output_folder: str) -> Dict[str, Any]: if i == self.root_key_record.used_root_cert: key = convert_to_ecc_key(self.root_key_record.root_public_key) else: - if i < len(self.root_key_record.root_certs) and self.root_key_record.root_certs[i]: + if ( + i < len(self.root_key_record.root_certs) + and self.root_key_record.root_certs[i] + ): key = convert_to_ecc_key(self.root_key_record.root_certs[i]) if key: - key_file_name = os.path.join(output_folder, f"rootCertificate{i}File.pub") + key_file_name = os.path.join( + output_folder, f"rootCertificate{i}File.pub" + ) key.save(key_file_name) cfg[f"rootCertificate{i}File"] = f"rootCertificate{i}File.pub" else: @@ -1535,7 +1631,9 @@ def __init__( ) -> None: """The Constructor for Certificate block.""" self.isk_cert_hash = bytes(self.ISK_CERT_HASH_LENGTH) - self.isk_certificate = IskCertificateLite(pub_key=isk_cert, constraints=int(self_signed)) + self.isk_certificate = IskCertificateLite( + pub_key=isk_cert, constraints=int(self_signed) + ) self.signature_provider = signature_provider @property @@ -1613,11 +1711,15 @@ def from_config( try: return cls.parse(load_binary(cert_block, search_paths)) except Exception: - return cls.from_config(load_configuration(cert_block, search_paths), search_paths) + return cls.from_config( + load_configuration(cert_block, search_paths), search_paths + ) main_root_private_key_file = cls.get_root_private_key_file(config) signature_provider = config.get("signProvider", config.get("iskSignProvider")) - isk_certificate = config.get("iskPublicKey", config.get("signingCertificateFile")) + isk_certificate = config.get( + "iskPublicKey", config.get("signingCertificateFile") + ) signature_provider = get_signature_provider( signature_provider, @@ -1648,9 +1750,13 @@ def generate_config_template(_family: Optional[str] = None) -> str: """Generate configuration for certification block vX.""" val_schemas = CertBlockVx.get_validation_schemas() val_schemas.append( - DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)["cert_block_output"] + DatabaseManager().db.get_schema_file(DatabaseManager.CERT_BLOCK)[ + "cert_block_output" + ] ) - return CommentedConfig("Certification Block Vx template", val_schemas).get_template() + return CommentedConfig( + "Certification Block Vx template", val_schemas + ).get_template() @classmethod def get_supported_families(cls) -> List[str]: @@ -1728,16 +1834,24 @@ def get_keys_or_rotkh_from_certblock_config( config_data["certBlock"], search_paths=[config_dir] ) except SPSDKError: - cert_block = load_binary(config_data["certBlock"], search_paths=[config_dir]) - parsed_cert_block = CertBlock.get_cert_block_class(family).parse(cert_block) + cert_block = load_binary( + config_data["certBlock"], search_paths=[config_dir] + ) + parsed_cert_block = CertBlock.get_cert_block_class(family).parse( + cert_block + ) rotkh = parsed_cert_block.rkth public_keys = find_root_certificates(config_data) - root_of_trust = tuple((find_file(x, search_paths=[config_dir]) for x in public_keys)) + root_of_trust = tuple( + (find_file(x, search_paths=[config_dir]) for x in public_keys) + ) except SPSDKError: logger.debug("Parsing ROT from config did not succeed, trying it as binary") try: cert_block = load_binary(rot, search_paths=[config_dir]) - parsed_cert_block = CertBlock.get_cert_block_class(family).parse(cert_block) + parsed_cert_block = CertBlock.get_cert_block_class(family).parse( + cert_block + ) rotkh = parsed_cert_block.rkth except SPSDKError as e: raise SPSDKError(f"Parsing of binary cert block failed with {e}") from e diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py index 0741328a..e2e9789e 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py @@ -140,7 +140,9 @@ def export(self) -> bytes: :return: serialized binary data """ - return pack(self._FORMAT, self.lock.tag, self.key_attribute.tag, self.aes_mode.tag, 0) + return pack( + self._FORMAT, self.lock.tag, self.key_attribute.tag, self.aes_mode.tag, 0 + ) class IeeKeyBlob: @@ -252,7 +254,9 @@ def plain_data(self) -> bytes: result += align_block(self.key1, 32) result += align_block(self.key2, 32) result += pack(" bytes: key = reverse_bytes_in_longs(self.key1) nonce = reverse_bytes_in_longs(self.key2) - counter = Counter(nonce, ctr_value=base_address >> 4, ctr_byteorder_encoding=Endianness.BIG) + counter = Counter( + nonce, ctr_value=base_address >> 4, ctr_byteorder_encoding=Endianness.BIG + ) for block in split_data(bytearray(data), self._ENCRYPTION_BLOCK_SIZE): encrypted_block = aes_ctr_encrypt( @@ -333,7 +339,9 @@ def encrypt_image(self, base_address: int, data: bytes) -> bytes: :raises NotImplementedError: AES-CTR is not implemented yet """ if base_address % 16 != 0: - raise SPSDKError("Invalid start address") # Start address has to be 16 byte aligned + raise SPSDKError( + "Invalid start address" + ) # Start address has to be 16 byte aligned data = align_block(data, self._ENCRYPTION_BLOCK_SIZE) # align data length data_len = len(data) @@ -483,7 +491,9 @@ def __init__( self.db = get_db(family, "latest") self.blobs_min_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_min_cnt") self.blobs_max_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_max_cnt") - self.generate_keyblob = self.db.get_bool(DatabaseManager.IEE, "generate_keyblob") + self.generate_keyblob = self.db.get_bool( + DatabaseManager.IEE, "generate_keyblob" + ) if key_blobs: for key_blob in key_blobs: @@ -531,13 +541,17 @@ def get_blhost_script_otp_kek(self) -> str: logger.debug(f"The {self.family} has no IEE KEK fuses") return "" - xml_fuses = self.db.get_file_path(DatabaseManager.IEE, "reg_fuses", default=None) + xml_fuses = self.db.get_file_path( + DatabaseManager.IEE, "reg_fuses", default=None + ) if not xml_fuses: logger.debug(f"The {self.family} has no IEE fuses definition") return "" fuses = Registers(self.family, base_endianness=Endianness.LITTLE) - grouped_regs = self.db.get_list(DatabaseManager.IEE, "grouped_registers", default=None) + grouped_regs = self.db.get_list( + DatabaseManager.IEE, "grouped_registers", default=None + ) fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) fuses.find_reg("USER_KEY1").set_value(self.ibkek1) @@ -591,7 +605,9 @@ def get_blhost_script_otp_kek(self) -> str: ret += f"efuse-program-once {hex(ibkek_lock.offset)} 0x{ibkek_lock.get_hex_value(raw=True)} --no-verify\n" ret += f"\n\n# {boot_cfg.name} fuse.\n" - ret += "WARNING!! Check SRM and set all desired bitfields for boot configuration" + ret += ( + "WARNING!! Check SRM and set all desired bitfields for boot configuration" + ) for bitfield in boot_cfg.get_bitfields(): ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" ret += ( @@ -619,7 +635,9 @@ def binary_image( iee = BinaryImage(image_name, offset=self.keyblob_address) if self.generate_keyblob: # Add mandatory IEE keyblob - iee_keyblobs = self.get_key_blobs() if plain_data else self.export_key_blobs() + iee_keyblobs = ( + self.get_key_blobs() if plain_data else self.export_key_blobs() + ) iee.add_image( BinaryImage( keyblob_name, @@ -692,9 +710,13 @@ def generate_config_template(family: str) -> Dict[str, Any]: template_note = database.get_str( DatabaseManager.IEE, "additional_template_text", default="" ) - title = f"IEE: Inline Encryption Engine Configuration template for {family}." + title = ( + f"IEE: Inline Encryption Engine Configuration template for {family}." + ) - yaml_data = CommentedConfig(title, val_schemas, note=template_note).get_template() + yaml_data = CommentedConfig( + title, val_schemas, note=template_note + ).get_template() return {f"{family}_iee": yaml_data} @@ -702,7 +724,9 @@ def generate_config_template(family: str) -> Dict[str, Any]: @staticmethod def load_from_config( - config: Dict[str, Any], config_dir: str, search_paths: Optional[List[str]] = None + config: Dict[str, Any], + config_dir: str, + search_paths: Optional[List[str]] = None, ) -> "IeeNxp": """Converts the configuration option into an IEE image object. @@ -713,7 +737,9 @@ def load_from_config( :param search_paths: List of paths where to search for the file, defaults to None :return: initialized IEE object. """ - iee_config: List[Dict[str, Any]] = config.get("key_blobs", [config.get("key_blob")]) + iee_config: List[Dict[str, Any]] = config.get( + "key_blobs", [config.get("key_blob")] + ) family = config["family"] ibkek1 = load_hex_string( config.get( @@ -744,12 +770,21 @@ def load_from_config( # start address to calculate offset from keyblob, min from keyblob or data blob address # pylint: disable-next=nested-min-max start_address = min( - min([value_to_int(addr.get("address", 0xFFFFFFFF)) for addr in data_blobs]), + min( + [ + value_to_int(addr.get("address", 0xFFFFFFFF)) + for addr in data_blobs + ] + ), start_address, ) binaries = BinaryImage( filepath_from_config( - config, "encrypted_name", "encrypted_blobs", config_dir, config["output_folder"] + config, + "encrypted_name", + "encrypted_blobs", + config_dir, + config["output_folder"], ), offset=start_address - keyblob_address, alignment=IeeKeyBlob._ENCRYPTION_BLOCK_SIZE, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py index 06d9158f..cb49f703 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py @@ -166,7 +166,9 @@ def plain_data(self) -> bytes: result += pack(" bytes: ) encrypted_data[ addr - base_addr : len(block) + addr - base_addr - ] = key_blob.encrypt_image(addr, block, byte_swap, counter_value=addr) + ] = key_blob.encrypt_image( + addr, block, byte_swap, counter_value=addr + ) addr += len(block) return bytes(encrypted_data) @@ -430,7 +440,9 @@ def encrypt_key_blobs( """ if isinstance(kek, str): kek = bytes.fromhex(kek) - scramble_enabled = key_scramble_mask is not None and key_scramble_align is not None + scramble_enabled = ( + key_scramble_mask is not None and key_scramble_align is not None + ) if scramble_enabled: assert key_scramble_mask and key_scramble_align if key_scramble_mask >= 1 << 32: @@ -517,8 +529,12 @@ def __init__( self.blobs_min_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_min_cnt") self.blobs_max_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_max_cnt") self.byte_swap = self.db.get_bool(DatabaseManager.OTFAD, "byte_swap") - self.key_blob_rec_size = self.db.get_int(DatabaseManager.OTFAD, "key_blob_rec_size") - self.keyblob_byte_swap_cnt = self.db.get_int(DatabaseManager.OTFAD, "keyblob_byte_swap_cnt") + self.key_blob_rec_size = self.db.get_int( + DatabaseManager.OTFAD, "key_blob_rec_size" + ) + self.keyblob_byte_swap_cnt = self.db.get_int( + DatabaseManager.OTFAD, "keyblob_byte_swap_cnt" + ) assert self.keyblob_byte_swap_cnt in [0, 2, 4, 8, 16] self.binaries = binaries @@ -551,13 +567,17 @@ def get_blhost_script_otp_keys( :return: BLHOST script that loads the keys into fuses. """ database = get_db(family, "latest") - xml_fuses = database.get_file_path(DatabaseManager.OTFAD, "reg_fuses", default=None) + xml_fuses = database.get_file_path( + DatabaseManager.OTFAD, "reg_fuses", default=None + ) if not xml_fuses: logger.debug(f"The {family} has no OTFAD fuses definition") return "" fuses = Registers(family, base_endianness=Endianness.LITTLE) - grouped_regs = database.get_list(DatabaseManager.OTFAD, "grouped_registers", default=None) + grouped_regs = database.get_list( + DatabaseManager.OTFAD, "grouped_registers", default=None + ) fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) reg_omk = fuses.find_reg("OTP_MASTER_KEY") reg_oks = fuses.find_reg("OTFAD_KEK_SEED") @@ -607,14 +627,18 @@ def get_blhost_script_otp_kek(self, index: int = 1) -> str: return "" filter_out_list = [f"OTFAD{i}" for i in peripheral_list if str(index) != i] - xml_fuses = self.db.get_file_path(DatabaseManager.OTFAD, "reg_fuses", default=None) + xml_fuses = self.db.get_file_path( + DatabaseManager.OTFAD, "reg_fuses", default=None + ) if not xml_fuses: logger.debug(f"The {self.family} has no OTFAD fuses definition") return "" fuses = Registers(self.family, base_endianness=Endianness.LITTLE) - grouped_regs = self.db.get_list(DatabaseManager.OTFAD, "grouped_registers", default=None) + grouped_regs = self.db.get_list( + DatabaseManager.OTFAD, "grouped_registers", default=None + ) fuses.load_registers_from_xml(xml_fuses, filter_out_list, grouped_regs) @@ -652,14 +676,18 @@ def get_blhost_script_otp_kek(self, index: int = 1) -> str: ) otfad_cfg.find_bitfield( self._replace_idx_value( - self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_enable_bitfield"), + self.db.get_str( + DatabaseManager.OTFAD, "otfad_scramble_enable_bitfield" + ), index, ) ).set_value(1) if scramble_align_standalone: fuses.find_reg(scramble_align).set_value(self.key_scramble_align) else: - otfad_cfg.find_bitfield(scramble_align).set_value(self.key_scramble_align) + otfad_cfg.find_bitfield(scramble_align).set_value( + self.key_scramble_align + ) fuses.find_reg(scramble_key).set_value(self.key_scramble_mask) ret = ( @@ -712,10 +740,14 @@ def export_image( binaries: BinaryImage = deepcopy(self.binaries) for binary in binaries.sub_images: if binary.binary: - binary.binary = align_block(binary.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE) + binary.binary = align_block( + binary.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE + ) for segment in binary.sub_images: if segment.binary: - segment.binary = align_block(segment.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE) + segment.binary = align_block( + segment.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE + ) binaries.validate() @@ -841,7 +873,9 @@ def generate_config_template(family: str) -> Dict[str, Any]: ) title = f"On-The-Fly AES decryption Configuration template for {family}." - yaml_data = CommentedConfig(title, val_schemas, note=template_note).get_template() + yaml_data = CommentedConfig( + title, val_schemas, note=template_note + ).get_template() return {f"{family}_otfad": yaml_data} @@ -849,7 +883,9 @@ def generate_config_template(family: str) -> Dict[str, Any]: @staticmethod def load_from_config( - config: Dict[str, Any], config_dir: str, search_paths: Optional[List[str]] = None + config: Dict[str, Any], + config_dir: str, + search_paths: Optional[List[str]] = None, ) -> "OtfadNxp": """Converts the configuration option into an OTFAD image object. @@ -863,14 +899,20 @@ def load_from_config( otfad_config: List[Dict[str, Any]] = config["key_blobs"] family = config["family"] database = get_db(family, "latest") - kek = load_hex_string(config["kek"], expected_size=16, search_paths=search_paths) + kek = load_hex_string( + config["kek"], expected_size=16, search_paths=search_paths + ) logger.debug(f"Loaded KEK: {kek.hex()}") table_address = value_to_int(config["otfad_table_address"]) - start_address = min([value_to_int(addr["start_address"]) for addr in otfad_config]) + start_address = min( + [value_to_int(addr["start_address"]) for addr in otfad_config] + ) key_scramble_mask = None key_scramble_align = None - if database.get_bool(DatabaseManager.OTFAD, "supports_key_scrambling", default=False): + if database.get_bool( + DatabaseManager.OTFAD, "supports_key_scrambling", default=False + ): if "key_scramble" in config.keys(): key_scramble = config["key_scramble"] key_scramble_mask = value_to_int(key_scramble["key_scramble_mask"]) @@ -886,7 +928,11 @@ def load_from_config( ) binaries = BinaryImage( filepath_from_config( - config, "encrypted_name", "encrypted_blobs", config_dir, config["output_folder"] + config, + "encrypted_name", + "encrypted_blobs", + config_dir, + config["output_folder"], ), offset=start_address - table_address, ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py index 795a1a98..c27aa719 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rkht.py @@ -39,7 +39,9 @@ def __init__(self, rkh_list: List[bytes]) -> None: @classmethod def from_keys( cls, - keys: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> Self: @@ -50,7 +52,9 @@ def from_keys( :param search_paths: List of paths where to search for the file, defaults to None """ public_keys = ( - [cls.convert_key(x, password, search_paths=search_paths) for x in keys] if keys else [] + [cls.convert_key(x, password, search_paths=search_paths) for x in keys] + if keys + else [] ) if not all(isinstance(x, type(public_keys[0])) for x in public_keys): raise SPSDKError("RKHT must contains all keys of a same instances.") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py index 06365f49..80858300 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py @@ -29,7 +29,9 @@ class Rot: def __init__( self, family: str, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: @@ -69,7 +71,9 @@ class RotBase: def __init__( self, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: @@ -96,13 +100,17 @@ class RotCertBlockv1(RotBase): def __init__( self, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: """Rot cert block v1 initialization.""" super().__init__(keys_or_certs, password, search_paths) - self.rkht = RKHTv1.from_keys(self.keys_or_certs, self.password, self.search_paths) + self.rkht = RKHTv1.from_keys( + self.keys_or_certs, self.password, self.search_paths + ) def calculate_hash( self, @@ -122,13 +130,17 @@ class RotCertBlockv21(RotBase): def __init__( self, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: """Rot cert block v21 initialization.""" super().__init__(keys_or_certs, password, search_paths) - self.rkht = RKHTv21.from_keys(self.keys_or_certs, self.password, self.search_paths) + self.rkht = RKHTv21.from_keys( + self.keys_or_certs, self.password, self.search_paths + ) def calculate_hash( self, @@ -148,14 +160,19 @@ class RotSrkTableAhab(RotBase): def __init__( self, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: """AHAB SRK table initialization.""" super().__init__(keys_or_certs, password, search_paths) self.srk = AhabSrkTable( - [SRKRecord(RKHT.convert_key(key, password, search_paths)) for key in keys_or_certs] + [ + SRKRecord(RKHT.convert_key(key, password, search_paths)) + for key in keys_or_certs + ] ) self.srk.update_fields() @@ -175,7 +192,9 @@ class RotSrkTableHab(RotBase): def __init__( self, - keys_or_certs: Sequence[Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate]], + keys_or_certs: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], password: Optional[str] = None, search_paths: Optional[List[str]] = None, ) -> None: @@ -191,7 +210,9 @@ def __init__( "Unable to load certificate. Certificate must be provided for HAB RoT calculation." ) from exc if not isinstance(certificate, Certificate): - raise SPSDKError("Certificate must be provided for HAB RoT calculation.") + raise SPSDKError( + "Certificate must be provided for HAB RoT calculation." + ) item = SrkItem.from_certificate(certificate) self.srk.append(item) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py index c2ce9ac9..8e6c6978 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/database.py @@ -17,7 +17,7 @@ import platformdirs from typing_extensions import Self -from .. import SPSDK_DATA_FOLDER, SPSDK_CACHE_DISABLED +from .. import SPSDK_CACHE_DISABLED, SPSDK_DATA_FOLDER from ..crypto.hash import EnumHashAlgorithm, Hash, get_hash from ..exceptions import SPSDKError, SPSDKValueError from ..utils.misc import ( @@ -39,7 +39,11 @@ class Features: """Features dataclass represents a single device revision.""" def __init__( - self, name: str, is_latest: bool, device: "Device", features: Dict[str, Dict[str, Any]] + self, + name: str, + is_latest: bool, + device: "Device", + features: Dict[str, Dict[str, Any]], ) -> None: """Constructor of revision. @@ -76,7 +80,9 @@ def check_key(self, feature: str, key: Union[List[str], str]) -> bool: assert isinstance(key, str) return key in db_dict - def get_value(self, feature: str, key: Union[List[str], str], default: Any = None) -> Any: + def get_value( + self, feature: str, key: Union[List[str], str], default: Any = None + ) -> Any: """Get value. :param feature: Feature name @@ -270,7 +276,10 @@ def load(config: Dict[str, Any], defaults: Dict[str, Any]) -> "DeviceInfo": data = deepcopy(defaults) deep_update(data, config) return DeviceInfo( - purpose=data["purpose"], web=data["web"], memory_map=data["memory_map"], isp=data["isp"] + purpose=data["purpose"], + web=data["web"], + memory_map=data["memory_map"], + isp=data["isp"], ) def update(self, config: Dict[str, Any]) -> None: @@ -371,7 +380,9 @@ def _load_alias( return ret @staticmethod - def load(name: str, path: str, defaults: Dict[str, Any], other_devices: "Devices") -> "Device": + def load( + name: str, path: str, defaults: Dict[str, Any], other_devices: "Devices" + ) -> "Device": """Loads the device from folder. :param name: The name of device. @@ -405,7 +416,9 @@ def load(name: str, path: str, defaults: Dict[str, Any], other_devices: "Devices f"The latest revision defined in database for {name} is not in supported revisions" ) - ret = Device(name=name, path=path, info=dev_info, latest_rev=latest, device_alias=None) + ret = Device( + name=name, path=path, info=dev_info, latest_rev=latest, device_alias=None + ) for rev, rev_updates in dev_revisions.items(): features = deepcopy(dev_features) @@ -413,7 +426,12 @@ def load(name: str, path: str, defaults: Dict[str, Any], other_devices: "Devices if rev_specific_features: deep_update(features, rev_specific_features) revisions.append( - Features(name=rev, is_latest=bool(rev == latest), features=features, device=ret) + Features( + name=rev, + is_latest=bool(rev == latest), + features=features, + device=ret, + ) ) ret.revisions = revisions @@ -447,7 +465,9 @@ def get(self, name: str) -> Device: """ dev = find_first(self, lambda dev: dev.name == name) if not dev: - raise SPSDKErrorMissingDevice(f"The device with name {name} is not in the database.") + raise SPSDKErrorMissingDevice( + f"The device with name {name} is not in the database." + ) return dev @property @@ -466,7 +486,9 @@ def feature_items(self, feature: str, key: str) -> Iterator[Tuple[str, str, Any] for rev in device.revisions: value = rev.features[feature].get(key) if value is None: - raise SPSDKValueError(f"Missing item '{key}' in feature '{feature}'!") + raise SPSDKValueError( + f"Missing item '{key}' in feature '{feature}'!" + ) yield (device.name, rev.name, value) @staticmethod @@ -503,7 +525,10 @@ def load(devices_path: str, defaults: Dict[str, Any]) -> "Devices": try: devices.append( Device.load( - name=dev.name, path=dev.path, defaults=defaults, other_devices=devices + name=dev.name, + path=dev.path, + defaults=defaults, + other_devices=devices, ) ) uncompleted_aliases.remove(dev) @@ -529,7 +554,9 @@ def __init__(self, path: str) -> None: self._defaults = load_configuration( os.path.join(self.common_folder_path, "database_defaults.yaml") ) - self._devices = Devices.load(devices_path=self.devices_folder_path, defaults=self._defaults) + self._devices = Devices.load( + devices_path=self.devices_folder_path, defaults=self._defaults + ) # optional Database hash that could be used for identification of consistency self.db_hash = bytes() @@ -690,10 +717,14 @@ def _get_database(cls) -> Database: loaded_db = pickle.load(f) assert isinstance(loaded_db, Database) if db_hash == loaded_db.db_hash: - logger.debug(f"Loaded database from cache: {cls._db_cache_file_name}") + logger.debug( + f"Loaded database from cache: {cls._db_cache_file_name}" + ) return loaded_db # if the hash is not same clear cache and make a new one - logger.debug(f"Existing cached DB ({cls._db_cache_file_name}) has invalid hash") + logger.debug( + f"Existing cached DB ({cls._db_cache_file_name}) has invalid hash" + ) DatabaseManager.clear_cache() except Exception as exc: logger.debug(f"Cannot load database cache: {str(exc)}") @@ -717,7 +748,10 @@ def __new__(cls) -> Self: if cls._instance: return cls._instance cls._instance = super(DatabaseManager, cls).__new__(cls) - cls._db_cache_folder_name, cls._db_cache_file_name = DatabaseManager.get_cache_filename() + ( + cls._db_cache_folder_name, + cls._db_cache_file_name, + ) = DatabaseManager.get_cache_filename() cls._db = cls._instance._get_database() cls._db_hash = hash(cls._db) return cls._instance diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py index d1f6064f..280b656c 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py @@ -204,7 +204,9 @@ def validate(self) -> None: f"Image offset of {self.image_name} cannot be in negative numbers." ) if len(self) < 0: - raise SPSDKValueError(f"Image size of {self.image_name} cannot be in negative numbers.") + raise SPSDKValueError( + f"Image size of {self.image_name} cannot be in negative numbers." + ) for image in self.sub_images: image.validate() begin = image.offset @@ -254,7 +256,9 @@ def get_min_draw_width(self, include_sub_images: bool = True) -> int: ] if include_sub_images: for child in self.sub_images: - widths.append(child.get_min_draw_width() + 2) # +2 means add vertical borders + widths.append( + child.get_min_draw_width() + 2 + ) # +2 means add vertical borders return max(widths) def draw( @@ -333,7 +337,9 @@ def wrap_block(inner: str) -> str: block += _get_centered_line(self._get_size_line(len(self))) # - Description if self.description: - for line in textwrap.wrap(self.description, width=width - 2, fix_sentence_endings=True): + for line in textwrap.wrap( + self.description, width=width - 2, fix_sentence_endings=True + ): block += _get_centered_line(line) # - Pattern if self.pattern: @@ -442,7 +448,9 @@ def load_from_config( for i, region in enumerate(regions): binary_file: Dict = region.get("binary_file") if binary_file: - offset = binary_file.get("offset", ret.aligned_length(ret.alignment)) + offset = binary_file.get( + "offset", ret.aligned_length(ret.alignment) + ) name = binary_file.get("name", binary_file["path"]) ret.add_image( BinaryImage.load_binary_image( @@ -456,7 +464,9 @@ def load_from_config( binary_block: Dict = region.get("binary_block") if binary_block: size = binary_block["size"] - offset = binary_block.get("offset", ret.aligned_length(ret.alignment)) + offset = binary_block.get( + "offset", ret.aligned_length(ret.alignment) + ) name = binary_block.get("name", f"Binary block(#{i})") pattern = BinaryPattern(binary_block["pattern"]) ret.add_image(BinaryImage(name, size, offset, pattern=pattern)) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py index 0f3f850f..a5112abb 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py @@ -78,7 +78,9 @@ def open(self) -> None: raise SPSDKConnectionError("No device available") if not self.is_blocking: if not hasattr(os, "set_blocking"): - raise SPSDKError("Opening in non-blocking mode is available only on Linux") + raise SPSDKError( + "Opening in non-blocking mode is available only on Linux" + ) # pylint: disable=no-member # this is available only on Unix os.set_blocking(self.device.fileno(), False) self._opened = True diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py index 7a7ac517..0237cce2 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py @@ -196,7 +196,9 @@ def _check_port(cls, port: str, baudrate: int, timeout: int) -> Optional[Self]: :return: None if device doesn't respond to PING, instance of Interface if it does """ try: - logger.debug(f"Checking port: {port}, baudrate: {baudrate}, timeout: {timeout}") + logger.debug( + f"Checking port: {port}, baudrate: {baudrate}, timeout: {timeout}" + ) device = cls(port=port, baudrate=baudrate, timeout=timeout) device.open() device.close() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py index 058bb971..afbb86cc 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usb_device.py @@ -82,7 +82,9 @@ def open(self) -> None: self._device.Open(self.path) self._opened = True except Exception as error: - raise SPSDKConnectionError(f"Unable to open device '{str(self)}'") from error + raise SPSDKConnectionError( + f"Unable to open device '{str(self)}'" + ) from error def close(self) -> None: """Close the interface. @@ -96,7 +98,9 @@ def close(self) -> None: self._device.Close() self._opened = False except Exception as error: - raise SPSDKConnectionError(f"Unable to close device '{str(self)}'") from error + raise SPSDKConnectionError( + f"Unable to close device '{str(self)}'" + ) from error def read(self, length: int, timeout: Optional[int] = None) -> bytes: """Read data on the IN endpoint associated to the HID interface. @@ -173,7 +177,9 @@ def scan( :param timeout: Read/write timeout :return: list of matching RawHid devices """ - usb_filter = NXPUSBDeviceFilter(usb_id=device_id, nxp_device_names=usb_devices_filter) + usb_filter = NXPUSBDeviceFilter( + usb_id=device_id, nxp_device_names=usb_devices_filter + ) devices = cls.enumerate(usb_filter, timeout=timeout) return devices diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py index c4819acd..38b48c6f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py @@ -42,7 +42,9 @@ def parse(cls, params: str) -> Self: class UsbSioDevice(DeviceBase): """USBSIO device class.""" - def __init__(self, dev: int = 0, config: Optional[str] = None, timeout: int = 5000) -> None: + def __init__( + self, dev: int = 0, config: Optional[str] = None, timeout: int = 5000 + ) -> None: """Initialize the Interface object. :param dev: device index to be used, default is set to 0 @@ -164,7 +166,9 @@ def scan( if i2c_ports: if i2c is not None: devices.append( - UsbSioI2CDevice(dev=port, port=i2c, config=config, timeout=timeout) + UsbSioI2CDevice( + dev=port, port=i2c, config=config, timeout=timeout + ) ) elif not intf_specified: devices.extend( @@ -177,7 +181,9 @@ def scan( if spi_ports: if spi is not None: devices.append( - UsbSioSPIDevice(dev=port, port=spi, config=config, timeout=timeout) + UsbSioSPIDevice( + dev=port, port=spi, config=config, timeout=timeout + ) ) elif not intf_specified: devices.extend( @@ -216,7 +222,9 @@ def _filter_usb(sio: LIBUSBSIO, ports: List[int], flt: str) -> List[int]: for port in ports: info = sio.GetDeviceInfo(port) if not info: - raise SPSDKError(f"Cannot retrive information from LIBUSBSIO device {port}.") + raise SPSDKError( + f"Cannot retrive information from LIBUSBSIO device {port}." + ) dev_info = { "vendor_id": info.vendor_id, "product_id": info.product_id, @@ -350,7 +358,9 @@ def write(self, data: bytes, timeout: Optional[int] = None) -> None: logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") try: (dummy, result) = self.port.Transfer( - devSelectPort=self.spi_sselport, devSelectPin=self.spi_sselpin, txData=data + devSelectPort=self.spi_sselport, + devSelectPin=self.spi_sselpin, + txData=data, ) except Exception as e: raise SPSDKConnectionError(str(e)) from e @@ -423,7 +433,9 @@ def read(self, length: int, timeout: Optional[int] = None) -> bytes: :raises SPSDKTimeoutError: When no data received """ try: - (data, result) = self.port.DeviceRead(devAddr=self.i2c_address, rxSize=length) + (data, result) = self.port.DeviceRead( + devAddr=self.i2c_address, rxSize=length + ) except Exception as e: raise SPSDKConnectionError(str(e)) from e if result < 0 or not data: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py index 53e3741d..89102e6f 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py @@ -104,7 +104,12 @@ def _get_interfaces(cls) -> List[Type[Self]]: def get_interface(cls, identifier: str) -> Type[Self]: """Get list of all available interfaces.""" interface = next( - (iface for iface in cls._get_interfaces() if iface.identifier == identifier), None + ( + iface + for iface in cls._get_interfaces() + if iface.identifier == identifier + ), + None, ) if not interface: raise SPSDKError(f"Interface with identifier {identifier} does not exist.") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py index c280fd8c..400efc94 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/misc.py @@ -268,7 +268,9 @@ def get_abs_path(file_path: str, base_dir: Optional[str] = None) -> str: if os.path.isabs(file_path): return file_path.replace("\\", "/") - return os.path.abspath(os.path.join(base_dir or os.getcwd(), file_path)).replace("\\", "/") + return os.path.abspath(os.path.join(base_dir or os.getcwd(), file_path)).replace( + "\\", "/" + ) def _find_path( @@ -391,7 +393,9 @@ def use_working_directory(path: str) -> Iterator[None]: assert os.getcwd() == current_dir -def format_value(value: int, size: int, delimiter: str = "_", use_prefix: bool = True) -> str: +def format_value( + value: int, size: int, delimiter: str = "_", use_prefix: bool = True +) -> str: """Convert the 'value' into either BIN or HEX string, depending on 'size'. if 'size' is divisible by 8, function returns HEX, BIN otherwise @@ -438,7 +442,9 @@ def get_bytes_cnt_of_int( return cnt -def value_to_int(value: Union[bytes, bytearray, int, str], default: Optional[int] = None) -> int: +def value_to_int( + value: Union[bytes, bytearray, int, str], default: Optional[int] = None +) -> int: """Function loads value from lot of formats to integer. :param value: Input value. @@ -458,7 +464,9 @@ def value_to_int(value: Union[bytes, bytearray, int, str], default: Optional[int value.strip().lower(), ) if match: - base = {"0b": 2, "0o": 8, "0": 10, "0x": 16, None: 10}[match.group("prefix")] + base = {"0b": 2, "0o": 8, "0": 10, "0x": 16, None: 10}[ + match.group("prefix") + ] try: return int(match.group("number"), base=base) except ValueError: @@ -703,7 +711,11 @@ def get_rest_time_ms(self, raise_exc: bool = False) -> int: raise SPSDKTimeoutError("Timeout of operation.") # pylint: disable=superfluous-parens # because PEP20: Readability counts - return ((self.end_time - self._get_current_time_us()) // 1000) if self.enabled else 0 + return ( + ((self.end_time - self._get_current_time_us()) // 1000) + if self.enabled + else 0 + ) def overflow(self, raise_exc: bool = False) -> bool: """Check the the timer has been overflowed. @@ -730,7 +742,9 @@ def size_fmt(num: Union[float, int], use_kibibyte: bool = True) -> str: return f"{int(num)} {i}" if i == "B" else f"{num:3.1f} {i}" -def numberify_version(version: str, separator: str = ".", valid_numbers: int = 3) -> int: +def numberify_version( + version: str, separator: str = ".", valid_numbers: int = 3 +) -> int: """Turn version string into a number. Each group is weighted by a multiple of 1000 @@ -853,7 +867,9 @@ def load_configuration(path: str, search_paths: Optional[List[str]] = None) -> D raise SPSDKError(f"Unable to load '{path}'.") -def split_data(data: Union[bytearray, bytes], size: int) -> Generator[bytes, None, None]: +def split_data( + data: Union[bytearray, bytes], size: int +) -> Generator[bytes, None, None]: """Split data into chunks of size. :param bytearray data: array of bytes to be split diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py index 4525ec99..0db7b0a6 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/plugins.py @@ -71,7 +71,9 @@ def load_from_entrypoints(self, group_name: Optional[str] = None) -> int: count += 1 return count - def load_from_source_file(self, source_file: str, module_name: Optional[str] = None) -> None: + def load_from_source_file( + self, source_file: str, module_name: Optional[str] = None + ) -> None: """Import Python source file directly. :param source_file: Path to python source file: absolute or relative to cwd diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py index a6a8d353..907a23a2 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py @@ -40,7 +40,9 @@ class RegsEnum: """Storage for register enumerations.""" - def __init__(self, name: str, value: Any, description: str, max_width: int = 0) -> None: + def __init__( + self, name: str, value: Any, description: str, max_width: int = 0 + ) -> None: """Constructor of RegsEnum class. Used to store enumeration information of bitfield. :param name: Name of enumeration. @@ -76,7 +78,9 @@ def from_xml_element(cls, xml_element: ET.Element, maxwidth: int = 0) -> "RegsEn except (TypeError, ValueError, SPSDKError) as exc: raise SPSDKRegsError(f"Invalid Enum Value: {raw_val}") from exc - description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + description = xml_element.attrib.get("description", "N/A").replace( + " ", "\n" + ) return cls(name, value, description, maxwidth) @@ -202,7 +206,8 @@ def __init__(self, count: int, description: str = "") -> None: :param description: Extra description for config processor, defaults to "" """ super().__init__( - description=description or f"Actual binary value is shifted by {count} bits to right." + description=description + or f"Actual binary value is shifted by {count} bits to right." ) self.count = count @@ -271,7 +276,9 @@ def __init__( self.set_value(self.reset_value, raw=True) @classmethod - def from_xml_element(cls, xml_element: ET.Element, parent: "RegsRegister") -> "RegsBitField": + def from_xml_element( + cls, xml_element: ET.Element, parent: "RegsRegister" + ) -> "RegsBitField": """Initialization register by XML ET element. :param xml_element: Input XML subelement with register data. @@ -281,14 +288,24 @@ def from_xml_element(cls, xml_element: ET.Element, parent: "RegsRegister") -> "R name = xml_element.attrib.get("name", "N/A") offset = value_to_int(xml_element.attrib.get("offset", 0)) width = value_to_int(xml_element.attrib.get("width", 0)) - description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + description = xml_element.attrib.get("description", "N/A").replace( + " ", "\n" + ) access = xml_element.attrib.get("access", "R/W") reset_value = value_to_int(xml_element.attrib.get("reset_value", 0)) hidden = xml_element.tag != "bit_field" config_processor = ConfigProcessor.from_xml(xml_element) bitfield = cls( - parent, name, offset, width, description, reset_value, access, hidden, config_processor + parent, + name, + offset, + width, + description, + reset_value, + access, + hidden, + config_processor, ) for xml_enum in xml_element.findall("bit_field_value"): @@ -403,7 +420,9 @@ def get_enum_constant(self, enum_name: str) -> int: if enum.name == enum_name: return enum.get_value_int() - raise SPSDKRegsErrorEnumNotFound(f"The enum for {enum_name} has not been found.") + raise SPSDKRegsErrorEnumNotFound( + f"The enum for {enum_name} has not been found." + ) def get_enum_names(self) -> List[str]: """Returns list of the enum strings. @@ -417,7 +436,9 @@ def add_et_subelement(self, parent: ET.Element) -> None: :param parent: The parent object of ElementTree. """ - element = ET.SubElement(parent, "reserved_bit_field" if self.hidden else "bit_field") + element = ET.SubElement( + parent, "reserved_bit_field" if self.hidden else "bit_field" + ) element.set("offset", hex(self.offset)) element.set("width", str(self.width)) element.set("name", self.name) @@ -482,7 +503,9 @@ def __init__( :param alt_widths: List of alternative widths. """ if width % 8 != 0: - raise SPSDKValueError("SPSDK Register supports only widths in multiply 8 bits.") + raise SPSDKValueError( + "SPSDK Register supports only widths in multiply 8 bits." + ) self.name = name self.offset = offset self.width = width @@ -531,7 +554,9 @@ def from_xml_element(cls, xml_element: ET.Element) -> "RegsRegister": name = xml_element.attrib.get("name", "N/A") offset = value_to_int(xml_element.attrib.get("offset", 0)) width = value_to_int(xml_element.attrib.get("width", 0)) - description = xml_element.attrib.get("description", "N/A").replace(" ", "\n") + description = xml_element.attrib.get("description", "N/A").replace( + " ", "\n" + ) reverse = (xml_element.attrib.get("reversed", "False")) == "True" access = xml_element.attrib.get("access", "N/A") otp_index_raw = xml_element.attrib.get("otp_index") @@ -691,7 +716,9 @@ def set_value(self, val: Any, raw: bool = False) -> None: else: bit_pos = (index - 1) * subreg_width - sub_reg.set_value((value >> bit_pos) & ((1 << subreg_width) - 1), raw=raw) + sub_reg.set_value( + (value >> bit_pos) & ((1 << subreg_width) - 1), raw=raw + ) else: self._value = value @@ -841,7 +868,9 @@ def find_bitfield(self, name: str) -> RegsBitField: if name == bitfield.name: return bitfield - raise SPSDKRegsErrorBitfieldNotFound(f" The {name} is not found in register {self.name}.") + raise SPSDKRegsErrorBitfieldNotFound( + f" The {name} is not found in register {self.name}." + ) def add_setvalue_hook(self, hook: Callable, context: Optional[Any] = None) -> None: """Set the value hook for write operation. @@ -881,7 +910,9 @@ class Registers: "Instead of bitfields: ... field, the value: ... definition works as well." ) - def __init__(self, device_name: str, base_endianness: Endianness = Endianness.BIG) -> None: + def __init__( + self, device_name: str, base_endianness: Endianness = Endianness.BIG + ) -> None: """Initialization of Registers class.""" self._registers: List[RegsRegister] = [] self.dev_name = device_name @@ -1044,7 +1075,9 @@ def image_info( return image - def export(self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros")) -> bytes: + def export( + self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros") + ) -> bytes: """Export Registers into binary. :param size: Result size of Image, 0 means automatic minimal size. @@ -1083,7 +1116,9 @@ def _get_bitfield_yaml_description(self, bitfield: RegsBitField) -> str: for enum in bitfield.get_enums(): descr = enum.description if enum.description != "." else enum.name enum_description = descr.replace(" ", "\n") - description += f"\n- {enum.name}, ({enum.get_value_int()}): {enum_description}" + description += ( + f"\n- {enum.name}, ({enum.get_value_int()}): {enum_description}" + ) return description def get_validation_schema(self) -> Dict: @@ -1123,14 +1158,18 @@ def get_validation_schema(self) -> Dict: bitfields_schema[bitfield.name] = { "type": ["string", "number"], "title": f"{bitfield.name}", - "description": self._get_bitfield_yaml_description(bitfield), + "description": self._get_bitfield_yaml_description( + bitfield + ), "template_value": bitfield.get_value(), } else: bitfields_schema[bitfield.name] = { "type": ["string", "number"], "title": f"{bitfield.name}", - "description": self._get_bitfield_yaml_description(bitfield), + "description": self._get_bitfield_yaml_description( + bitfield + ), "enum_template": bitfield.get_enum_names(), "minimum": 0, "maximum": (1 << bitfield.width) - 1, @@ -1144,7 +1183,10 @@ def get_validation_schema(self) -> Dict: "skip_in_template": True, "additionalProperties": False, "properties": { - "bitfields": {"type": "object", "properties": bitfields_schema} + "bitfields": { + "type": "object", + "properties": bitfields_schema, + } }, } ) @@ -1168,14 +1210,18 @@ def get_validation_schema(self) -> Dict: return {"type": "object", "title": self.dev_name, "properties": properties} # pylint: disable=no-self-use #It's better to have this function visually close to callies - def _filter_by_names(self, items: List[ET.Element], names: List[str]) -> List[ET.Element]: + def _filter_by_names( + self, items: List[ET.Element], names: List[str] + ) -> List[ET.Element]: """Filter out all items in the "items" tree,whose name starts with one of the strings in "names" list. :param items: Items to be filtered out. :param names: Names to filter out. :return: Filtered item elements list. """ - return [item for item in items if not item.attrib["name"].startswith(tuple(names))] + return [ + item for item in items if not item.attrib["name"].startswith(tuple(names)) + ] # pylint: disable=dangerous-default-value def load_registers_from_xml( @@ -1252,7 +1298,9 @@ def load_yml_config(self, yml_data: Dict[str, Any]) -> None: register.set_value(val, False) else: bitfields = ( - reg_value["bitfields"] if "bitfields" in reg_value.keys() else reg_value + reg_value["bitfields"] + if "bitfields" in reg_value.keys() + else reg_value ) for bitfield_name in bitfields: bitfield_val = bitfields[bitfield_name] @@ -1273,11 +1321,13 @@ def load_yml_config(self, yml_data: Dict[str, Any]) -> None: except SPSDKError: # New versions of register data do not contain register and bitfield value in enum old_bitfield = bitfield_val - bitfield_val = bitfield_val.replace(bitfield.name + "_", "").replace( - register.name + "_", "" - ) + bitfield_val = bitfield_val.replace( + bitfield.name + "_", "" + ).replace(register.name + "_", "") # Some bitfield were renamed from ENABLE to ALLOW - bitfield_val = "ALLOW" if bitfield_val == "ENABLE" else bitfield_val + bitfield_val = ( + "ALLOW" if bitfield_val == "ENABLE" else bitfield_val + ) logger.warning( f"Bitfield {old_bitfield} not found, trying backward" " compatibility mode with {bitfield_val}" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py index bab8db8a..269627db 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/schema_validator.py @@ -157,7 +157,9 @@ def process_nested_rule( elif exc.rule == "format": if exc.rule_definition == "file": message += f"; Non-existing file: {exc.value}" - message += "; The file must exists even if the key is NOT used in configuration." + message += ( + "; The file must exists even if the key is NOT used in configuration." + ) elif exc.rule == "anyOf": message += process_nested_rule(exc, extra_formatters=extra_formatters) elif exc.rule == "oneOf": @@ -181,7 +183,9 @@ def check_config( """ custom_formatters: Dict[str, Callable[[str], bool]] = { "dir": lambda x: bool(find_dir(x, search_paths=search_paths, raise_exc=False)), - "file": lambda x: bool(find_file(x, search_paths=search_paths, raise_exc=False)), + "file": lambda x: bool( + find_file(x, search_paths=search_paths, raise_exc=False) + ), "file_name": lambda x: os.path.basename(x.replace("\\", "/")) not in ("", None), "optional_file": lambda x: not x or bool(find_file(x, search_paths=search_paths, raise_exc=False)), @@ -210,10 +214,12 @@ def check_config( else: validator = fastjsonschema.compile(schema, formats=formats) except (TypeError, fastjsonschema.JsonSchemaDefinitionException) as exc: - raise SPSDKError(f"Invalid validation schema to check config: {str(exc)}") from exc + raise SPSDKError( + f"Invalid validation schema to check config: {str(exc)}" + ) from exc try: if ENABLE_DEBUG: - import validator_file # type: ignore + import validator_file # type: ignore validator_file.validate(config_to_check, formats) else: @@ -273,7 +279,9 @@ def _get_title_block(self, title: str, description: Optional[str] = None) -> str return ret @staticmethod - def get_property_optional_required(key: str, block: Dict[str, Any]) -> PropertyRequired: + def get_property_optional_required( + key: str, block: Dict[str, Any] + ) -> PropertyRequired: """Function to determine if the config property is required or not. :param key: Name of config record @@ -293,7 +301,9 @@ def _find_required(d_in: Dict[str, Any]) -> Optional[List[str]]: return ret return None - def _find_required_in_schema_kws(schema_node: Union[List, Dict[str, Any]]) -> List[str]: + def _find_required_in_schema_kws( + schema_node: Union[List, Dict[str, Any]] + ) -> List[str]: """Find all required properties in structure composed of nested properties.""" all_props: List[str] = [] if isinstance(schema_node, dict): @@ -492,7 +502,9 @@ def get_help_name(schema: Dict) -> str: key = list(value.keys())[0] comment = "" if i == 0: - comment = self._get_title_block(title, f"Options [{option_types}]") + "\n" + comment = ( + self._get_title_block(title, f"Options [{option_types}]") + "\n" + ) comment += "\n " + ( f" [Example of possible configuration #{i}] ".center(self.max_line, "=") ) @@ -595,7 +607,9 @@ def _add_comment( # ) if template_title: - self._update_before_comment(cfg, key, "\n" + self._get_title_block(template_title)) + self._update_before_comment( + cfg, key, "\n" + self._get_title_block(template_title) + ) @staticmethod def _get_schema_block_keys(schema: Dict[str, Dict[str, Any]]) -> List[str]: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py index 7bec30ee..4d1870b8 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/spsdk_enum.py @@ -119,7 +119,9 @@ def from_tag(cls, tag: int) -> Self: for item in cls.__members__.values(): if item.tag == tag: return item - raise SPSDKKeyError(f"There is no {cls.__name__} item in with tag {tag} defined") + raise SPSDKKeyError( + f"There is no {cls.__name__} item in with tag {tag} defined" + ) @classmethod def from_label(cls, label: str) -> Self: @@ -132,7 +134,9 @@ def from_label(cls, label: str) -> Self: for item in cls.__members__.values(): if item.label.upper() == label.upper(): return item - raise SPSDKKeyError(f"There is no {cls.__name__} item with label {label} defined") + raise SPSDKKeyError( + f"There is no {cls.__name__} item with label {label} defined" + ) class SpsdkSoftEnum(SpsdkEnum): diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py index b5a90909..de89c972 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/usbfilter.py @@ -279,7 +279,9 @@ def _is_vid_or_pid(self, vid: Optional[int], pid: Optional[int]) -> bool: return False def _is_nxp_device_name(self, vid: int, pid: int) -> bool: - nxp_device_name_to_compare = {k.lower(): v for k, v in self.nxp_device_names.items()} + nxp_device_name_to_compare = { + k.lower(): v for k, v in self.nxp_device_names.items() + } assert isinstance(self.usb_id, str) if self.usb_id.lower() in nxp_device_name_to_compare: vendor_id, product_id = nxp_device_name_to_compare[self.usb_id.lower()] From a103c47130e55849f1cbddea6940fab87f1f2132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 21 Mar 2024 10:37:28 +0100 Subject: [PATCH 05/11] Ignore errors in lpc55_upload --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 34682126..7777fdd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,11 @@ ignore_errors = true module = "pynitrokey.trussed.bootloader.nrf52" disallow_untyped_calls = false +# pynitrokey.nk3.bootloader.lpc55_upload is only temporary in this package +[[tool.mypy.overrides]] +module = "pynitrokey.trussed.bootloader.lpc55_upload.*" +ignore_errors = true + # libraries without annotations [[tool.mypy.overrides]] module = [ From fb8cb750e3bca1db8429d20ee6fa2de9bcdce936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 21 Mar 2024 11:52:13 +0100 Subject: [PATCH 06/11] Fix circular import --- pynitrokey/trussed/bootloader/lpc55.py | 7 ++++++- pynitrokey/trussed/utils.py | 6 ------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pynitrokey/trussed/bootloader/lpc55.py b/pynitrokey/trussed/bootloader/lpc55.py index c08b170d..416fe249 100644 --- a/pynitrokey/trussed/bootloader/lpc55.py +++ b/pynitrokey/trussed/bootloader/lpc55.py @@ -20,6 +20,7 @@ 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.misc import BcdVersion3 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 @@ -135,9 +136,13 @@ def open(cls: type[T], path: str) -> Optional[T]: return None +def parse_bcd_version(version: BcdVersion3) -> Version: + return Version(major=version.major, minor=version.minor, patch=version.service) + + def parse_firmware_image(data: bytes) -> FirmwareMetadata: image = BootImageV21.parse(data, kek=KEK) - version = Version.from_bcd_version(image.header.product_version) + version = parse_bcd_version(image.header.product_version) metadata = FirmwareMetadata(version=version) if image.cert_block: if image.cert_block.rkth == RKTH: diff --git a/pynitrokey/trussed/utils.py b/pynitrokey/trussed/utils.py index 49fc1b40..d09818be 100644 --- a/pynitrokey/trussed/utils.py +++ b/pynitrokey/trussed/utils.py @@ -12,8 +12,6 @@ from functools import total_ordering from typing import Optional, Sequence -# from .bootloader.lpc55_upload.sbfile.misc import BcdVersion3 - @dataclass(order=True, frozen=True) class Uuid: @@ -228,10 +226,6 @@ def from_v_str(cls, s: str) -> "Version": raise ValueError(f"Missing v prefix for firmware version: {s}") return Version.from_str(s[1:]) - @classmethod - def from_bcd_version(cls, version: any) -> "Version": - return cls(major=version.major, minor=version.minor, patch=version.service) - @dataclass class Fido2Certs: From 0a9e053fc9a43a11b356b2957296e6d864b5bb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 21 Mar 2024 16:41:55 +0100 Subject: [PATCH 07/11] Remove unused spsdk code --- .../bootloader/lpc55_upload/__init__.py | 1 - .../bootloader/lpc55_upload/crypto/cmac.py | 42 - .../bootloader/lpc55_upload/crypto/cms.py | 169 - .../bootloader/lpc55_upload/crypto/keys.py | 250 -- .../bootloader/lpc55_upload/crypto/oscca.py | 149 - .../lpc55_upload/crypto/signature_provider.py | 8 - .../bootloader/lpc55_upload/ele/__init__.py | 8 - .../bootloader/lpc55_upload/ele/ele_comm.py | 291 -- .../lpc55_upload/ele/ele_constants.py | 428 -- .../lpc55_upload/ele/ele_message.py | 1585 ------- .../lpc55_upload/image/ahab/__init__.py | 8 - .../image/ahab/ahab_abstract_interfaces.py | 235 - .../lpc55_upload/image/ahab/ahab_container.py | 3885 ----------------- .../lpc55_upload/image/ahab/signed_msg.py | 1623 ------- .../lpc55_upload/image/ahab/utils.py | 86 - .../bootloader/lpc55_upload/image/header.py | 197 - .../bootloader/lpc55_upload/image/misc.py | 110 - .../bootloader/lpc55_upload/image/secret.py | 951 ---- .../bootloader/lpc55_upload/mboot/__init__.py | 20 +- .../lpc55_upload/mboot/interfaces/buspal.py | 566 --- .../lpc55_upload/mboot/interfaces/sdio.py | 172 - .../lpc55_upload/mboot/interfaces/uart.py | 110 - .../lpc55_upload/mboot/interfaces/usbsio.py | 110 - .../mboot/protocol/serial_protocol.py | 334 -- .../bootloader/lpc55_upload/mboot/scanner.py | 103 - .../lpc55_upload/sbfile/sb2/images.py | 376 +- .../bootloader/lpc55_upload/uboot/__init__.py | 7 - .../bootloader/lpc55_upload/uboot/uboot.py | 137 - .../lpc55_upload/utils/crypto/iee.py | 838 ---- .../lpc55_upload/utils/crypto/otfad.py | 658 --- .../lpc55_upload/utils/crypto/rot.py | 239 - .../bootloader/lpc55_upload/utils/images.py | 616 --- .../utils/interfaces/device/sdio_device.py | 271 -- .../utils/interfaces/device/serial_device.py | 208 - .../utils/interfaces/device/usbsio_device.py | 460 -- .../utils/interfaces/scanner_helper.py | 37 - .../lpc55_upload/utils/registers.py | 1373 ------ 37 files changed, 2 insertions(+), 16659 deletions(-) delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/header.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/sdio.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/uart.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py delete mode 100644 pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py index 9a900c6f..e2c02c55 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/__init__.py @@ -11,7 +11,6 @@ import os __author__ = "NXP" -__contact__ = "michal.starecek@nxp.com" __license__ = "BSD-3-Clause" __version__ = version __release__ = "beta" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py deleted file mode 100644 index 08157087..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cmac.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""OpenSSL implementation for CMAC packet authentication.""" - -# Used security modules -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives import cmac as cmac_cls -from cryptography.hazmat.primitives.ciphers import algorithms - - -def cmac(key: bytes, data: bytes) -> bytes: - """Return a CMAC from data with specified key and algorithm. - - :param key: The key in bytes format - :param data: Input data in bytes format - :return: CMAC bytes - """ - cmac_obj = cmac_cls.CMAC(algorithm=algorithms.AES(key)) - cmac_obj.update(data) - return cmac_obj.finalize() - - -def cmac_validate(key: bytes, data: bytes, signature: bytes) -> bool: - """Return a CMAC from data with specified key and algorithm. - - :param key: The key in bytes format - :param data: Input data in bytes format - :param signature: CMAC signature to validate - :return: CMAC bytes - """ - cmac_obj = cmac_cls.CMAC(algorithm=algorithms.AES(key)) - cmac_obj.update(data) - try: - cmac_obj.verify(signature=signature) - return True - except InvalidSignature: - return False diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py deleted file mode 100644 index 2d2db420..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/cms.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""ASN1Crypto implementation for CMS signature container.""" - - -# Used security modules -from datetime import datetime -from typing import Optional - -from ..crypto.certificate import Certificate -from ..crypto.hash import EnumHashAlgorithm, get_hash -from ..crypto.keys import ECDSASignature, PrivateKey, PrivateKeyEcc, PrivateKeyRsa -from ..crypto.signature_provider import SignatureProvider -from ..crypto.types import SPSDKEncoding -from ..exceptions import SPSDKError, SPSDKTypeError, SPSDKValueError - - -def cms_sign( - zulu: datetime, - data: bytes, - certificate: Certificate, - signing_key: Optional[PrivateKey], - signature_provider: Optional[SignatureProvider], -) -> bytes: - """Sign provided data and return CMS signature. - - :param zulu: current UTC time+date - :param data: to be signed - :param certificate: Certificate with issuer information - :param signing_key: Signing key, is mutually exclusive with signature_provider parameter - :param signature_provider: Signature provider, is mutually exclusive with signing_key parameter - :return: CMS signature (binary) - :raises SPSDKError: If certificate is not present - :raises SPSDKError: If private key is not present - :raises SPSDKError: If incorrect time-zone" - """ - # Lazy imports are used here to save some time during SPSDK startup - from asn1crypto import cms, util, x509 - - if certificate is None: - raise SPSDKValueError("Certificate is not present") - if not (signing_key or signature_provider): - raise SPSDKValueError("Private key or signature provider is not present") - if signing_key and signature_provider: - raise SPSDKValueError( - "Only one of private key and signature provider must be specified" - ) - if signing_key and not isinstance(signing_key, (PrivateKeyEcc, PrivateKeyRsa)): - raise SPSDKTypeError(f"Unsupported private key type {type(signing_key)}.") - - # signed data (main section) - signed_data = cms.SignedData() - signed_data["version"] = "v1" - signed_data["encap_content_info"] = util.OrderedDict([("content_type", "data")]) - signed_data["digest_algorithms"] = [ - util.OrderedDict([("algorithm", "sha256"), ("parameters", None)]) - ] - - # signer info sub-section - signer_info = cms.SignerInfo() - signer_info["version"] = "v1" - signer_info["digest_algorithm"] = util.OrderedDict( - [("algorithm", "sha256"), ("parameters", None)] - ) - signer_info["signature_algorithm"] = ( - util.OrderedDict([("algorithm", "rsassa_pkcs1v15"), ("parameters", b"")]) - if (signing_key and isinstance(signing_key, PrivateKeyRsa)) - or (signature_provider and signature_provider.signature_length >= 256) - else util.OrderedDict([("algorithm", "sha256_ecdsa")]) - ) - # signed identifier: issuer amd serial number - - asn1_cert = x509.Certificate.load(certificate.export(SPSDKEncoding.DER)) - signer_info["sid"] = cms.SignerIdentifier( - { - "issuer_and_serial_number": cms.IssuerAndSerialNumber( - { - "issuer": asn1_cert.issuer, - "serial_number": asn1_cert.serial_number, - } - ) - } - ) - # signed attributes - signed_attrs = cms.CMSAttributes() - signed_attrs.append( - cms.CMSAttribute( - { - "type": "content_type", - "values": [cms.ContentType("data")], - } - ) - ) - - # check time-zone is assigned (expected UTC+0) - if not zulu.tzinfo: - raise SPSDKError("Incorrect time-zone") - signed_attrs.append( - cms.CMSAttribute( - { - "type": "signing_time", - "values": [ - cms.Time(name="utc_time", value=zulu.strftime("%y%m%d%H%M%SZ")) - ], - } - ) - ) - signed_attrs.append( - cms.CMSAttribute( - { - "type": "message_digest", - "values": [cms.OctetString(get_hash(data))], # digest - } - ) - ) - signer_info["signed_attrs"] = signed_attrs - - # create signature - data_to_sign = signed_attrs.dump() - signature = sign_data(data_to_sign, signing_key, signature_provider) - - signer_info["signature"] = signature - # Adding SignerInfo object to SignedData object - signed_data["signer_infos"] = [signer_info] - - # content info - content_info = cms.ContentInfo() - content_info["content_type"] = "signed_data" - content_info["content"] = signed_data - - return content_info.dump() - - -def sign_data( - data_to_sign: bytes, - signing_key: Optional[PrivateKey], - signature_provider: Optional[SignatureProvider], -) -> bytes: - """Sign the data. - - :param data_to_sign: Data to be signed - :param signing_key: Signing key, is mutually exclusive with signature_provider parameter - :param signature_provider: Signature provider, is mutually exclusive with signing_key parameter - """ - assert signing_key or signature_provider - if signing_key and signature_provider: - raise SPSDKValueError( - "Only one of private key and signature provider must be specified" - ) - if signing_key: - return ( - signing_key.sign( - data_to_sign, algorithm=EnumHashAlgorithm.SHA256, der_format=True - ) - if isinstance(signing_key, PrivateKeyEcc) - else signing_key.sign(data_to_sign) - ) - assert signature_provider - signature = signature_provider.get_signature(data_to_sign) - # convert to DER format - if signature_provider.signature_length < 256: - ecdsa_signature = ECDSASignature.parse(signature) - signature = ecdsa_signature.export(encoding=SPSDKEncoding.DER) - return signature diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py index 3986e7b5..06ab3dee 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py @@ -38,16 +38,9 @@ from ..utils.abstract import BaseClass from ..utils.misc import Endianness, load_binary, write_file from .hash import EnumHashAlgorithm, get_hash, get_hash_algorithm -from .oscca import IS_OSCCA_SUPPORTED from .rng import rand_below, random_hex from .types import SPSDKEncoding -if IS_OSCCA_SUPPORTED: - from asn1tools import DecodeError # pylint: disable=import-error - from gmssl import sm2 # pylint: disable=import-error - - from .oscca import SM2Encoder, sanitize_pem - def _load_pem_private_key(data: bytes, password: Optional[bytes]) -> Any: """Load PEM Private key. @@ -62,13 +55,6 @@ def _load_pem_private_key(data: bytes, password: Optional[bytes]) -> Any: return _crypto_load_private_key(SPSDKEncoding.PEM, data, password) except (UnsupportedAlgorithm, ValueError) as exc: last_error = exc - if IS_OSCCA_SUPPORTED: - try: - key_data = sanitize_pem(data) - key_set = SM2Encoder().decode_private_key(data=key_data) - return sm2.CryptSM2(private_key=key_set.private, public_key=key_set.public) - except (SPSDKError, DecodeError) as exc: - last_error = exc raise SPSDKError(f"Cannot load PEM private key: {last_error}") @@ -85,12 +71,6 @@ def _load_der_private_key(data: bytes, password: Optional[bytes]) -> Any: return _crypto_load_private_key(SPSDKEncoding.DER, data, password) except (UnsupportedAlgorithm, ValueError) as exc: last_error = exc - if IS_OSCCA_SUPPORTED: - try: - key_set = SM2Encoder().decode_private_key(data=data) - return sm2.CryptSM2(private_key=key_set.private, public_key=key_set.public) - except (SPSDKError, DecodeError) as exc: - last_error = exc raise SPSDKError(f"Cannot load DER private key: {last_error}") @@ -139,13 +119,6 @@ def _load_pem_public_key(data: bytes) -> Any: return crypto_load_pem_public_key(data) except (UnsupportedAlgorithm, ValueError) as exc: last_error = exc - if IS_OSCCA_SUPPORTED: - try: - key_data = sanitize_pem(data) - public_key = SM2Encoder().decode_public_key(data=key_data) - return sm2.CryptSM2(private_key=None, public_key=public_key.public) - except (SPSDKError, DecodeError) as exc: - last_error = exc raise SPSDKError(f"Cannot load PEM public key: {last_error}") @@ -161,12 +134,6 @@ def _load_der_public_key(data: bytes) -> Any: return crypto_load_der_public_key(data) except (UnsupportedAlgorithm, ValueError) as exc: last_error = exc - if IS_OSCCA_SUPPORTED: - try: - public_key = SM2Encoder().decode_public_key(data=data) - return sm2.CryptSM2(private_key=None, public_key=public_key.public) - except (SPSDKError, DecodeError) as exc: - last_error = exc raise SPSDKError(f"Cannot load DER private key: {last_error}") @@ -294,8 +261,6 @@ def parse(cls, data: bytes, password: Optional[str] = None) -> Self: ) if isinstance(private_key, (ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey)): return cls.create(private_key) - if IS_OSCCA_SUPPORTED and isinstance(private_key, sm2.CryptSM2): - return cls.create(private_key) except (ValueError, SPSDKInvalidKeyType) as exc: raise SPSDKError(f"Cannot load private key: ({str(exc)})") from exc raise SPSDKError(f"Unsupported private key: ({str(private_key)})") @@ -312,9 +277,6 @@ def create(cls, key: Any) -> Self: PrivateKeyEcc: ec.EllipticCurvePrivateKey, PrivateKeyRsa: rsa.RSAPrivateKey, } - if IS_OSCCA_SUPPORTED: - SUPPORTED_KEYS[PrivateKeySM2] = sm2.CryptSM2 - for k, v in SUPPORTED_KEYS.items(): if isinstance(key, v): return k(key) @@ -391,8 +353,6 @@ def parse(cls, data: bytes) -> Self: }[SPSDKEncoding.get_file_encodings(data)](data) if isinstance(public_key, (ec.EllipticCurvePublicKey, rsa.RSAPublicKey)): return cls.create(public_key) - if IS_OSCCA_SUPPORTED and isinstance(public_key, sm2.CryptSM2): - return cls.create(public_key) except (ValueError, SPSDKInvalidKeyType) as exc: raise SPSDKError(f"Cannot load public key: ({str(exc)})") from exc raise SPSDKError(f"Unsupported public key: ({str(public_key)})") @@ -426,9 +386,6 @@ def create(cls, key: Any) -> Self: PublicKeyEcc: ec.EllipticCurvePublicKey, PublicKeyRsa: rsa.RSAPublicKey, } - if IS_OSCCA_SUPPORTED: - SUPPORTED_KEYS[PublicKeySM2] = sm2.CryptSM2 - for k, v in SUPPORTED_KEYS.items(): if isinstance(key, v): return k(key) @@ -1124,211 +1081,6 @@ def __str__(self) -> str: return f"ECC ({self.curve}) Public key: \nx({hex(self.x)}) \ny({hex(self.y)})" -# =================================================================================================== -# =================================================================================================== -# -# SM2 Key -# -# =================================================================================================== -# =================================================================================================== -if IS_OSCCA_SUPPORTED: - from .oscca import SM2Encoder, SM2KeySet, SM2PublicKey, sanitize_pem - - class PrivateKeySM2(PrivateKey): - """SPSDK SM2 Private Key.""" - - key: sm2.CryptSM2 - - def __init__(self, key: sm2.CryptSM2) -> None: - """Create SPSDK Key. - - :param key: Only SM2 key is accepted - """ - if not isinstance(key, sm2.CryptSM2): - raise SPSDKInvalidKeyType("The input key is not SM2 type") - self.key = key - - @classmethod - def generate_key(cls) -> Self: - """Generate SM2 Key (private key). - - :return: SM2 private key - """ - key = sm2.CryptSM2(None, "None") - n = int(key.ecc_table["n"], base=16) - prk = rand_below(n) - while True: - puk = key._kg(prk, key.ecc_table["g"]) - if puk[:2] != "04": # PUK cannot start with 04 - break - key.private_key = f"{prk:064x}" - key.public_key = puk - - return cls(key) - - def get_public_key(self) -> "PublicKeySM2": - """Generate public key. - - :return: Public key - """ - return PublicKeySM2( - sm2.CryptSM2(private_key=None, public_key=self.key.public_key) - ) - - def verify_public_key(self, public_key: PublicKey) -> bool: - """Verify public key. - - :param public_key: Public key to verify - :return: True if is in pair, False otherwise - """ - return self.get_public_key() == public_key - - def sign( - self, data: bytes, salt: Optional[str] = None, use_ber: bool = False - ) -> bytes: - """Sign data using SM2 algorithm with SM3 hash. - - :param data: Data to sign. - :param salt: Salt for signature generation, defaults to None. If not specified a random string will be used. - :param use_ber: Encode signature into BER format, defaults to True - :raises SPSDKError: Signature can't be created. - :return: SM2 signature. - """ - data_hash = bytes.fromhex(self.key._sm3_z(data)) - if salt is None: - salt = random_hex(self.key.para_len // 2) - signature_str = self.key.sign(data=data_hash, K=salt) - if not signature_str: - raise SPSDKError("Can't sign data") - signature = bytes.fromhex(signature_str) - if use_ber: - ber_signature = SM2Encoder().encode_signature(signature) - return ber_signature - return signature - - def export( - self, - password: Optional[str] = None, - encoding: SPSDKEncoding = SPSDKEncoding.DER, - ) -> bytes: - """Convert key into bytes supported by NXP.""" - if encoding != SPSDKEncoding.DER: - raise SPSDKNotImplementedError( - "Only DER enocding is supported for SM2 keys export" - ) - keys = SM2KeySet(self.key.private_key, self.key.public_key) - return SM2Encoder().encode_private_key(keys) - - def __repr__(self) -> str: - return "SM2 Private Key" - - def __str__(self) -> str: - """Object description in string format.""" - return f"SM2Key(private_key={self.key.private_key}, public_key='{self.key.public_key}')" - - @property - def key_size(self) -> int: - """Size of the key in bits.""" - return self.key.para_len - - @property - def signature_size(self) -> int: - """Signature size.""" - return 64 - - class PublicKeySM2(PublicKey): - """SM2 Public Key.""" - - key: sm2.CryptSM2 - - def __init__(self, key: sm2.CryptSM2) -> None: - """Create SPSDK Public Key. - - :param key: SPSDK Public Key data or file path - """ - if not isinstance(key, sm2.CryptSM2): - raise SPSDKInvalidKeyType("The input key is not SM2 type") - self.key = key - - def verify_signature( - self, - signature: bytes, - data: bytes, - algorithm: Optional[EnumHashAlgorithm] = None, - ) -> bool: - """Verify signature. - - :param signature: SM2 signature to verify - :param data: Signed data - :param algorithm: Just to keep compatibility with abstract class - :raises SPSDKError: Invalid signature - """ - # Check if the signature is BER formatted - if len(signature) > 64 and signature[0] == 0x30: - signature = SM2Encoder().decode_signature(signature) - # Otherwise the signature is in raw format r || s - data_hash = bytes.fromhex(self.key._sm3_z(data)) - return self.key.verify(Sign=signature.hex(), data=data_hash) - - def export(self, encoding: SPSDKEncoding = SPSDKEncoding.DER) -> bytes: - """Convert key into bytes supported by NXP. - - :return: Byte representation of key - """ - if encoding != SPSDKEncoding.DER: - raise SPSDKNotImplementedError( - "Only DER enocding is supported for SM2 keys export" - ) - keys = SM2PublicKey(self.key.public_key) - return SM2Encoder().encode_public_key(keys) - - @property - def signature_size(self) -> int: - """Signature size.""" - return 64 - - @property - def public_numbers(self) -> str: - """Public numbers of key. - - :return: Public numbers - """ - return self.key.public_key - - @classmethod - def recreate(cls, data: bytes) -> Self: - """Recreate SM2 public key from data. - - :param data: public key data - :return: SPSDK public key. - """ - return cls(sm2.CryptSM2(private_key=None, public_key=data.hex())) - - @classmethod - def recreate_from_data(cls, data: bytes) -> Self: - """Recreate SM2 public key from data. - - :param data: PEM or DER encoded key. - :return: SM2 public key. - """ - key_data = sanitize_pem(data) - public_key = SM2Encoder().decode_public_key(data=key_data) - return cls(sm2.CryptSM2(private_key=None, public_key=public_key.public)) - - def __repr__(self) -> str: - return "SM2 Public Key" - - def __str__(self) -> str: - """Object description in string format.""" - ret = f"SM2 Public Key <{self.public_numbers}>" - return ret - -else: - # In case the OSCCA is not installed, do this to avoid import errors - PrivateKeySM2 = PrivateKey # type: ignore - PublicKeySM2 = PublicKey # type: ignore - - class ECDSASignature: """ECDSA Signature.""" @@ -1448,8 +1200,6 @@ def get_supported_keys_generators() -> KeyGeneratorInfo: "secp384r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp384r1"}), "secp521r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp521r1"}), } - if IS_OSCCA_SUPPORTED: - ret["sm2"] = (PrivateKeySM2.generate_key, {}) return ret diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py deleted file mode 100644 index d315c043..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/oscca.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2022-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Support for OSCCA SM2/SM3.""" - - -from .. import SPSDK_DATA_FOLDER_COMMON -from ..utils.misc import Endianness - -try: - # this import is to find out whether OSCCA support is installed or not - # pylint: disable=unused-import - import gmssl - - IS_OSCCA_SUPPORTED = True -except ImportError: - IS_OSCCA_SUPPORTED = False - - -if IS_OSCCA_SUPPORTED: - import base64 - import os - from typing import Any, NamedTuple, Optional, Type, TypeVar - - from ..exceptions import SPSDKError - - OSCCA_ASN_DEFINITION_FILE = os.path.join( - SPSDK_DATA_FOLDER_COMMON, "crypto", "oscca.asn" - ) - SM2_OID = "1.2.156.10197.1.301" - - class SM2KeySet(NamedTuple): - """Bare-bone representation of a SM2 Key.""" - - private: str - public: Optional[str] - - class SM2PublicKey(NamedTuple): - """Bare-bone representation of a SM2 Public Key.""" - - public: str - - _T = TypeVar("_T") - - def singleton(class_: Type[_T]) -> Type[_T]: - """Decorator providing Singleton functionality for classes.""" - instances = {} - - def getinstance(*args: Any, **kwargs: Any) -> _T: - # args/kwargs should be part of cache key - if class_ not in instances: - instances[class_] = class_(*args, **kwargs) - return instances[class_] - - return getinstance # type: ignore # why are we even using Mypy?! - - @singleton - class SM2Encoder: - """ASN1 Encoder/Decoder for SM2 keys and signature.""" - - def __init__(self, asn_file: str = OSCCA_ASN_DEFINITION_FILE) -> None: - """Create ASN encoder/decoder based on provided ASN file.""" - try: - import asn1tools - except ImportError as import_error: - raise SPSDKError( - "asn1tools package is missing, " - "please install it with pip install 'spsdk[oscca]' in order to use OSCCA" - ) from import_error - - self.parser = asn1tools.compile_files(asn_file) - - def decode_private_key(self, data: bytes) -> SM2KeySet: - """Parse private SM2 key set from binary data.""" - result = self.parser.decode("Private", data) - key_set = self.parser.decode("KeySet", result["keyset"]) - return SM2KeySet( - private=key_set["prk"].hex(), public=key_set["puk"][0][1:].hex() - ) - - def decode_public_key(self, data: bytes) -> SM2PublicKey: - """Parse public SM2 key set from binary data.""" - result = self.parser.decode("Public", data) - return SM2PublicKey(public=result["puk"][0][1:].hex()) - - def encode_private_key(self, keys: SM2KeySet) -> bytes: - """Encode private SM2 key set from keyset.""" - assert isinstance(keys.public, str) - puk_array = bytearray(bytes.fromhex(keys.public)) - puk_array[0:0] = b"\x04" # 0x4 must be prepended - puk = (puk_array, 520) # tuple contains 520 - keyset = self.parser.encode( - "KeySet", - data={ - "number": 1, - "prk": bytes.fromhex(keys.private), - "puk": puk, - }, - ) - private_key = {"number": 0, "ids": [SM2_OID, SM2_OID], "keyset": keyset} - return self.parser.encode("Private", data=private_key) - - def encode_public_key(self, key: SM2PublicKey) -> bytes: - """Encode public SM2 key from SM2PublicKey.""" - puk_array = bytearray(bytes.fromhex(key.public)) - puk_array[0:0] = b"\x04" # 0x4 must be prepended - puk = (puk_array, 520) # tuple contains 520 - data = {"ids": [SM2_OID, SM2_OID], "puk": puk} - return self.parser.encode("Public", data=data) - - def decode_signature(self, data: bytes) -> bytes: - """Decode BER signature into r||s coordinates.""" - result = self.parser.decode("Signature", data) - r = int.to_bytes(result["r"], length=32, byteorder=Endianness.BIG.value) - s = int.to_bytes(result["s"], length=32, byteorder=Endianness.BIG.value) - return r + s - - def encode_signature(self, data: bytes) -> bytes: - """Encode raw r||s signature into BER format.""" - if len(data) != 64: - raise SPSDKError("SM2 signature must be 64B long.") - r = int.from_bytes(data[:32], byteorder=Endianness.BIG.value) - s = int.from_bytes(data[32:], byteorder=Endianness.BIG.value) - ber_signature = self.parser.encode("Signature", data={"r": r, "s": s}) - return ber_signature - - def sanitize_pem(data: bytes) -> bytes: - """Covert PEM data into DER.""" - if b"---" not in data: - return data - - capture_data = False - base64_data = b"" - for line in data.splitlines(keepends=False): - if capture_data: - base64_data += line - # PEM data may contain EC PARAMS, thus capture trigger should be the word KEY - if b"KEY" in line: - capture_data = not capture_data - # in the end the `capture_data` flag should be false singaling propper END * KEY - # and we should have some data - if capture_data is False and len(base64_data) > 0: - der_data = base64.b64decode(base64_data) - return der_data - raise SPSDKError("PEM data are corrupted") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py index eb9b5171..77fd17b8 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/signature_provider.py @@ -29,10 +29,8 @@ PrivateKey, PrivateKeyEcc, PrivateKeyRsa, - PrivateKeySM2, PublicKeyEcc, PublicKeyRsa, - PublicKeySM2, SPSDKKeyPassphraseMissing, prompt_for_passphrase, ) @@ -218,8 +216,6 @@ def _get_hash_algorithm( hash_size = 512 hash_alg_name = EnumHashAlgorithm.from_label(f"sha{hash_size}") - elif isinstance(self.private_key, PrivateKeySM2): - hash_alg_name = EnumHashAlgorithm.SM3 else: raise SPSDKError( f"Unsupported private key by signature provider: {str(self.private_key)}" @@ -241,10 +237,6 @@ def verify_public_key(self, public_key: bytes) -> bool: return self.private_key.verify_public_key(PublicKeyRsa.parse(public_key)) except SPSDKError: pass - try: - return self.private_key.verify_public_key(PublicKeySM2.parse(public_key)) - except SPSDKError: - pass raise SPSDKError("Unsupported public key") def info(self) -> str: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py deleted file mode 100644 index 2dfaaee1..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""This module contains support for EdgeLock Enclave Tool.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py deleted file mode 100644 index 09914ec0..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_comm.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""EdgeLock Enclave Message handler.""" - -import logging -import re -from abc import abstractmethod -from types import TracebackType -from typing import List, Optional, Tuple, Type, Union - -from ..ele.ele_constants import ResponseStatus -from ..ele.ele_message import EleMessage -from ..exceptions import SPSDKError, SPSDKLengthError -from ..mboot.mcuboot import McuBoot -from ..uboot.uboot import Uboot -from ..utils.database import DatabaseManager, get_db, get_families -from ..utils.misc import value_to_bytes - -logger = logging.getLogger(__name__) - - -class EleMessageHandler: - """Base class for ELE message handling.""" - - def __init__( - self, device: Union[McuBoot, Uboot], family: str, revision: str = "latest" - ) -> None: - """Class object initialized. - - :param device: Communication interface. - :param family: Target family name. - :param revision: Target revision, default is use 'latest' revision. - """ - self.device = device - self.database = get_db(device=family, revision=revision) - self.family = family - self.revision = revision - self.comm_buff_addr = self.database.get_int( - DatabaseManager.COMM_BUFFER, "address" - ) - self.comm_buff_size = self.database.get_int(DatabaseManager.COMM_BUFFER, "size") - logger.info( - f"ELE communicator is using {self.comm_buff_size} B size buffer at " - f"{self.comm_buff_addr:08X} address in {family} target." - ) - - @staticmethod - def get_supported_families() -> List[str]: - """Get list of supported target families. - - :return: List of supported families. - """ - return get_families(DatabaseManager.ELE) - - @staticmethod - def get_ele_device(device: str) -> str: - """Get default ELE device from DB.""" - return get_db(device, "latest").get_str(DatabaseManager.ELE, "ele_device") - - @abstractmethod - def send_message(self, msg: EleMessage) -> None: - """Send message and receive response. - - :param msg: EdgeLock Enclave message - """ - - def __enter__(self) -> None: - """Enter function of ELE handler.""" - if not self.device.is_opened: - self.device.open() - - def __exit__( - self, - exception_type: Optional[Type[BaseException]] = None, - exception_value: Optional[BaseException] = None, - traceback: Optional[TracebackType] = None, - ) -> None: - """Close function of ELE handler.""" - if self.device.is_opened: - self.device.close() - - -class EleMessageHandlerMBoot(EleMessageHandler): - """EdgeLock Enclave Message Handler over MCUBoot. - - This class can send the ELE message into target over mBoot and decode the response. - """ - - def __init__(self, device: McuBoot, family: str, revision: str = "latest") -> None: - """Class object initialized. - - :param device: mBoot device. - :param family: Target family name. - :param revision: Target revision, default is use 'latest' revision. - """ - if not isinstance(device, McuBoot): - raise SPSDKError("Wrong instance of device, must be MCUBoot") - super().__init__(device, family, revision) - - def send_message(self, msg: EleMessage) -> None: - """Send message and receive response. - - :param msg: EdgeLock Enclave message - :raises SPSDKError: Invalid response status detected. - :raises SPSDKLengthError: Invalid read back length detected. - """ - if not isinstance(self.device, McuBoot): - raise SPSDKError("Wrong instance of device, must be MCUBoot") - msg.set_buffer_params(self.comm_buff_addr, self.comm_buff_size) - try: - # 1. Prepare command in target memory - self.device.write_memory(msg.command_address, msg.export()) - - # 1.1. Prepare command data in target memory if required - if msg.has_command_data: - self.device.write_memory(msg.command_data_address, msg.command_data) - - # 2. Execute ELE message on target - self.device.ele_message( - msg.command_address, - msg.command_words_count, - msg.response_address, - msg.response_words_count, - ) - if msg.response_words_count == 0: - return - # 3. Read back the response - response = self.device.read_memory( - msg.response_address, 4 * msg.response_words_count - ) - except SPSDKError as exc: - raise SPSDKError( - f"ELE Communication failed with mBoot: {str(exc)}" - ) from exc - - if not response or len(response) != 4 * msg.response_words_count: - raise SPSDKLengthError( - "ELE Message - Invalid response read-back operation." - ) - # 4. Decode the response - msg.decode_response(response) - - # 4.1 Check the response status - if msg.status != ResponseStatus.ELE_SUCCESS_IND: - raise SPSDKError(f"ELE Message failed. \n{msg.info()}") - - # 4.2 Read back the response data from target memory if required - if msg.has_response_data: - try: - response_data = self.device.read_memory( - msg.response_data_address, msg.response_data_size - ) - except SPSDKError as exc: - raise SPSDKError( - f"ELE Communication failed with mBoot: {str(exc)}" - ) from exc - - if not response_data or len(response_data) != msg.response_data_size: - raise SPSDKLengthError( - "ELE Message - Invalid response data read-back operation." - ) - - msg.decode_response_data(response_data) - - logger.info(f"Sent message information:\n{msg.info()}") - - -class EleMessageHandlerUBoot(EleMessageHandler): - """EdgeLock Enclave Message Handler over UBoot. - - This class can send the ELE message into target over UBoot and decode the response. - """ - - def __init__(self, device: Uboot, family: str, revision: str = "latest") -> None: - """Class object initialized. - - :param device: UBoot device. - :param family: Target family name. - :param revision: Target revision, default is use 'latest' revision. - """ - if not isinstance(device, Uboot): - raise SPSDKError("Wrong instance of device, must be UBoot") - super().__init__(device, family, revision) - - def extract_error_values(self, error_message: str) -> Tuple[int, int, int]: - """Extract error values from error_mesage. - - :param error_message: Error message containing ret and response - :return: abort_code, status and indication - """ - # Define regular expressions to extract values - ret_pattern = re.compile(r"ret (0x[0-9a-fA-F]+),") - response_pattern = re.compile(r"response (0x[0-9a-fA-F]+)") - - # Find matches in the error message - ret_match = ret_pattern.search(error_message) - response_match = response_pattern.search(error_message) - - if not ret_match or not response_match: - logger.error(f"Cannot decode error message from ELE!\n{error_message}") - abort_code = 0 - status = 0 - indication = 0 - else: - abort_code = int(ret_match.group(1), 16) - status_all = int(response_match.group(1), 16) - indication = status_all >> 8 - status = status_all & 0xFF - return abort_code, status, indication - - def send_message(self, msg: EleMessage) -> None: - """Send message and receive response. - - :param msg: EdgeLock Enclave message - :raises SPSDKError: Invalid response status detected. - :raises SPSDKLengthError: Invalid read back length detected. - """ - if not isinstance(self.device, Uboot): - raise SPSDKError("Wrong instance of device, must be UBoot") - msg.set_buffer_params(self.comm_buff_addr, self.comm_buff_size) - - try: - logger.debug( - f"ELE msg {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}" - ) - - # 0. Prepare command data in target memory if required - if msg.has_command_data: - self.device.write_memory(msg.command_data_address, msg.command_data) - - # 1. Execute ELE message on target - self.device.write( - f"ele_message {hex(msg.buff_addr)} {hex(msg.buff_size)} {msg.export().hex()}" - ) - output = self.device.read_output() - logger.debug(f"Raw ELE message output:\n{output}") - - if msg.response_words_count == 0: - return - - if "Error" in output: - msg.abort_code, msg.status, msg.indication = self.extract_error_values( - output - ) - else: - # 2. Read back the response - stripped_output = output.splitlines()[-1].replace("u-boot=> ", "") - logger.debug(f"Stripped output {stripped_output}") - response = value_to_bytes("0x" + stripped_output) - except (SPSDKError, IndexError) as exc: - raise SPSDKError( - f"ELE Communication failed with UBoot: {str(exc)}" - ) from exc - - if not "Error" in output: - if not response or len(response) != 4 * msg.response_words_count: - raise SPSDKLengthError( - "ELE Message - Invalid response read-back operation." - ) - # 3. Decode the response - msg.decode_response(response) - - # 3.1 Check the response status - if msg.status != ResponseStatus.ELE_SUCCESS_IND: - raise SPSDKError(f"ELE Message failed. \n{msg.info()}") - - # 3.2 Read back the response data from target memory if required - if msg.has_response_data: - try: - response_data = self.device.read_memory( - msg.response_data_address, msg.response_data_size - ) - self.device.read_output() - except SPSDKError as exc: - raise SPSDKError( - f"ELE Communication failed with mBoot: {str(exc)}" - ) from exc - - if not response_data or len(response_data) != msg.response_data_size: - raise SPSDKLengthError( - "ELE Message - Invalid response data read-back operation." - ) - - msg.decode_response_data(response_data) - - logger.info(f"Sent message information:\n{msg.info()}") diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py deleted file mode 100644 index 23427186..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_constants.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""EdgeLock Enclave Message constants.""" - -from ..utils.spsdk_enum import SpsdkEnum, SpsdkSoftEnum - - -class MessageIDs(SpsdkSoftEnum): - """ELE Messages ID.""" - - PING_REQ = (0x01, "PING_REQ", "Ping request.") - ELE_FW_AUTH_REQ = (0x02, "ELE_FW_AUTH_REQ", "ELE firmware authenticate request.") - ELE_DUMP_DEBUG_BUFFER_REQ = (0x21, "ELE_DUMP_DEBUG_BUFFER_REQ", "Dump the ELE logs") - ELE_OEM_CNTN_AUTH_REQ = ( - 0x87, - "ELE_OEM_CNTN_AUTH_REQ", - "OEM Container authenticate", - ) - ELE_VERIFY_IMAGE_REQ = (0x88, "ELE_VERIFY_IMAGE_REQ", "Verify Image") - ELE_RELEASE_CONTAINER_REQ = ( - 0x89, - "ELE_RELEASE_CONTAINER_REQ", - "Release Container.", - ) - WRITE_SEC_FUSE_REQ = (0x91, "WRITE_SEC_FUSE_REQ", "Write secure fuse request.") - ELE_FWD_LIFECYCLE_UP_REQ = ( - 0x95, - "ELE_FWD_LIFECYCLE_UP_REQ", - "Forward Lifecycle update", - ) - READ_COMMON_FUSE = (0x97, "READ_COMMON_FUSE", "Read common fuse request.") - GET_FW_VERSION_REQ = (0x9D, "GET_FW_VERSION_REQ", "Get firmware version request.") - RETURN_LIFECYCLE_UPDATE_REQ = ( - 0xA0, - "RETURN_LIFECYCLE_UPDATE_REQ", - "Return lifecycle update request.", - ) - ELE_GET_EVENTS_REQ = (0xA2, "ELE_GET_EVENTS_REQ", "Get Events") - LOAD_KEY_BLOB_REQ = (0xA7, "LOAD_KEY_BLOB_REQ", "Load KeyBlob request.") - ELE_COMMIT_REQ = (0xA8, "ELE_COMMIT_REQ", "EdgeLock Enclave commit request.") - ELE_DERIVE_KEY_REQ = (0xA9, "ELE_DERIVE_KEY_REQ", "Derive key") - GENERATE_KEY_BLOB_REQ = (0xAF, "GENERATE_KEY_BLOB_REQ", "Generate KeyBlob request.") - GET_FW_STATUS_REQ = (0xC5, "GET_FW_STATUS_REQ", "Get ELE FW status request.") - ELE_ENABLE_APC_REQ = ( - 0xD2, - "ELE_ENABLE_APC_REQ", - "Enable APC (Application processor)", - ) - ELE_ENABLE_RTC_REQ = (0xD3, "ELE_ENABLE_RTC_REQ", "Enable RTC (Runtime processor)") - GET_INFO_REQ = (0xDA, "GET_INFO_REQ", "Get ELE Information request.") - ELE_RESET_APC_CTX_REQ = (0xD8, "ELE_RESET_APC_CTX_REQ", "Reset APC Context") - START_RNG_REQ = (0xA3, "START_RNG_REQ", "Start True Random Generator request.") - GET_TRNG_STATE_REQ = ( - 0xA3, - "GET_TRNG_STATE_REQ", - "Get True Random Generator state request.", - ) - RESET_REQ = (0xC7, "RESET_REQ", "System reset request.") - WRITE_FUSE = (0xD6, "WRITE_FUSE", "Write fuse") - WRITE_SHADOW_FUSE = (0xF2, "WRITE_SHADOW_FUSE", "Write shadow fuse") - READ_SHADOW_FUSE = (0xF3, "READ_SHADOW_FUSE", "Read shadow fuse request.") - - -class LifeCycle(SpsdkSoftEnum): - """ELE life cycles.""" - - LC_BLANK = (0x002, "BLANK", "Blank device") - LC_FAB = (0x004, "FAB", "Fab mode") - LC_NXP_PROV = (0x008, "NXP_PROV", "NXP Provisioned") - LC_OEM_OPEN = (0x010, "OEM_OPEN", "OEM Open") - LC_OEM_SWC = (0x020, "OEM_SWC", "OEM Secure World Closed") - LC_OEM_CLSD = (0x040, "OEM_CLSD", "OEM Closed") - LC_OEM_FR = (0x080, "OEM_FR", "Field Return OEM") - LC_NXP_FR = (0x100, "NXP_FR", "Field Return NXP") - LC_OEM_LCKD = (0x200, "OEM_LCKD", "OEM Locked") - LC_BRICKED = (0x400, "BRICKED", "BRICKED") - - -class LifeCycleToSwitch(SpsdkSoftEnum): - """ELE life cycles to switch request.""" - - OEM_CLOSED = (0x08, "OEM_CLOSED", "OEM Closed") - OEM_LOCKED = (0x80, "OEM_LOCKED", "OEM Locked") - - -class MessageUnitId(SpsdkSoftEnum): - """Message Unit ID.""" - - RTD_MU = (0x01, "RTD_MU", "Real Time Device message unit") - APD_MU = (0x02, "APD_MU", "Application Processor message unit") - - -class ResponseStatus(SpsdkEnum): - """ELE Message Response status.""" - - ELE_SUCCESS_IND = (0xD6, "Success", "The request was successful") - ELE_FAILURE_IND = (0x29, "Failure", "The request failed") - - -class ResponseIndication(SpsdkSoftEnum): - """ELE Message Response indication.""" - - ELE_ROM_PING_FAILURE_IND = (0x0A, "ELE_ROM_PING_FAILURE_IND", "ROM ping failure") - ELE_FW_PING_FAILURE_IND = (0x1A, "ELE_FW_PING_FAILURE_IND", "Firmware ping failure") - ELE_UNALIGNED_PAYLOAD_FAILURE_IND = ( - 0xA6, - "ELE_UNALIGNED_PAYLOAD_FAILURE_IND", - "Un-aligned payload failure", - ) - ELE_WRONG_SIZE_FAILURE_IND = ( - 0xA7, - "ELE_WRONG_SIZE_FAILURE_IND", - "Wrong size failure", - ) - ELE_ENCRYPTION_FAILURE_IND = ( - 0xA8, - "ELE_ENCRYPTION_FAILURE_IND", - "Encryption failure", - ) - ELE_DECRYPTION_FAILURE_IND = ( - 0xA9, - "ELE_DECRYPTION_FAILURE_IND", - "Decryption failure", - ) - ELE_OTP_PROGFAIL_FAILURE_IND = ( - 0xAA, - "ELE_OTP_PROGFAIL_FAILURE_IND", - "OTP program fail failure", - ) - ELE_OTP_LOCKED_FAILURE_IND = ( - 0xAB, - "ELE_OTP_LOCKED_FAILURE_IND", - "OTP locked failure", - ) - ELE_OTP_INVALID_IDX_FAILURE_IND = ( - 0xAD, - "ELE_OTP_INVALID_IDX_FAILURE_IND", - "OTP Invalid IDX failure", - ) - ELE_TIME_OUT_FAILURE_IND = (0xB0, "ELE_TIME_OUT_FAILURE_IND", "Timeout failure") - ELE_BAD_PAYLOAD_FAILURE_IND = ( - 0xB1, - "ELE_BAD_PAYLOAD_FAILURE_IND", - "Bad payload failure", - ) - ELE_WRONG_ADDRESS_FAILURE_IND = ( - 0xB4, - "ELE_WRONG_ADDRESS_FAILURE_IND", - "Wrong address failure", - ) - ELE_DMA_FAILURE_IND = (0xB5, "ELE_DMA_FAILURE_IND", "DMA failure") - ELE_DISABLED_FEATURE_FAILURE_IND = ( - 0xB6, - "ELE_DISABLED_FEATURE_FAILURE_IND", - "Disabled feature failure", - ) - ELE_MUST_ATTEST_FAILURE_IND = ( - 0xB7, - "ELE_MUST_ATTEST_FAILURE_IND", - "Must attest failure", - ) - ELE_RNG_NOT_STARTED_FAILURE_IND = ( - 0xB8, - "ELE_RNG_NOT_STARTED_FAILURE_IND", - "Random number generator not started failure", - ) - ELE_CRC_ERROR_IND = (0xB9, "ELE_CRC_ERROR_IND", "CRC error") - ELE_AUTH_SKIPPED_OR_FAILED_FAILURE_IND = ( - 0xBB, - "ELE_AUTH_SKIPPED_OR_FAILED_FAILURE_IND", - "Authentication skipped or failed failure", - ) - ELE_INCONSISTENT_PAR_FAILURE_IND = ( - 0xBC, - "ELE_INCONSISTENT_PAR_FAILURE_IND", - "Inconsistent parameter failure", - ) - ELE_RNG_INST_FAILURE_IND = ( - 0xBD, - "ELE_RNG_INST_FAILURE_IND", - "Random number generator instantiation failure", - ) - ELE_LOCKED_REG_FAILURE_IND = ( - 0xBE, - "ELE_LOCKED_REG_FAILURE_IND", - "Locked register failure", - ) - ELE_BAD_ID_FAILURE_IND = (0xBF, "ELE_BAD_ID_FAILURE_IND", "Bad ID failure") - ELE_INVALID_OPERATION_FAILURE_IND = ( - 0xC0, - "ELE_INVALID_OPERATION_FAILURE_IND", - "Invalid operation failure", - ) - ELE_NON_SECURE_STATE_FAILURE_IND = ( - 0xC1, - "ELE_NON_SECURE_STATE_FAILURE_IND", - "Non secure state failure", - ) - ELE_MSG_TRUNCATED_IND = (0xC2, "ELE_MSG_TRUNCATED_IND", "Message truncated failure") - ELE_BAD_IMAGE_NUM_FAILURE_IND = ( - 0xC3, - "ELE_BAD_IMAGE_NUM_FAILURE_IND", - "Bad image number failure", - ) - ELE_BAD_IMAGE_ADDR_FAILURE_IND = ( - 0xC4, - "ELE_BAD_IMAGE_ADDR_FAILURE_IND", - "Bad image address failure", - ) - ELE_BAD_IMAGE_PARAM_FAILURE_IND = ( - 0xC5, - "ELE_BAD_IMAGE_PARAM_FAILURE_IND", - "Bad image parameters failure", - ) - ELE_BAD_IMAGE_TYPE_FAILURE_IND = ( - 0xC6, - "ELE_BAD_IMAGE_TYPE_FAILURE_IND", - "Bad image type failure", - ) - ELE_APC_ALREADY_ENABLED_FAILURE_IND = ( - 0xCB, - "ELE_APC_ALREADY_ENABLED_FAILURE_IND", - "APC already enabled failure", - ) - ELE_RTC_ALREADY_ENABLED_FAILURE_IND = ( - 0xCC, - "ELE_RTC_ALREADY_ENABLED_FAILURE_IND", - "RTC already enabled failure", - ) - ELE_WRONG_BOOT_MODE_FAILURE_IND = ( - 0xCD, - "ELE_WRONG_BOOT_MODE_FAILURE_IND", - "Wrong boot mode failure", - ) - ELE_OLD_VERSION_FAILURE_IND = ( - 0xCE, - "ELE_OLD_VERSION_FAILURE_IND", - "Old version failure", - ) - ELE_CSTM_FAILURE_IND = (0xCF, "ELE_CSTM_FAILURE_IND", "CSTM failure") - ELE_CORRUPTED_SRK_FAILURE_IND = ( - 0xD0, - "ELE_CORRUPTED_SRK_FAILURE_IND", - "Corrupted SRK failure", - ) - ELE_OUT_OF_MEMORY_IND = (0xD1, "ELE_OUT_OF_MEMORY_IND", "Out of memory failure") - - ELE_MUST_SIGNED_FAILURE_IND = ( - 0xE0, - "ELE_MUST_SIGNED_FAILURE_IND", - "Must be signed failure", - ) - ELE_NO_AUTHENTICATION_FAILURE_IND = ( - 0xEE, - "ELE_NO_AUTHENTICATION_FAILURE_IND", - "No authentication failure", - ) - ELE_BAD_SRK_SET_FAILURE_IND = ( - 0xEF, - "ELE_BAD_SRK_SET_FAILURE_IND", - "Bad SRK set failure", - ) - ELE_BAD_SIGNATURE_FAILURE_IND = ( - 0xF0, - "ELE_BAD_SIGNATURE_FAILURE_IND", - "Bad signature failure", - ) - ELE_BAD_HASH_FAILURE_IND = (0xF1, "ELE_BAD_HASH_FAILURE_IND", "Bad hash failure") - ELE_INVALID_LIFECYCLE_IND = (0xF2, "ELE_INVALID_LIFECYCLE_IND", "Invalid lifecycle") - ELE_PERMISSION_DENIED_FAILURE_IND = ( - 0xF3, - "ELE_PERMISSION_DENIED_FAILURE_IND", - "Permission denied failure", - ) - ELE_INVALID_MESSAGE_FAILURE_IND = ( - 0xF4, - "ELE_INVALID_MESSAGE_FAILURE_IND", - "Invalid message failure", - ) - ELE_BAD_VALUE_FAILURE_IND = (0xF5, "ELE_BAD_VALUE_FAILURE_IND", "Bad value failure") - ELE_BAD_FUSE_ID_FAILURE_IND = ( - 0xF6, - "ELE_BAD_FUSE_ID_FAILURE_IND", - "Bad fuse ID failure", - ) - ELE_BAD_CONTAINER_FAILURE_IND = ( - 0xF7, - "ELE_BAD_CONTAINER_FAILURE_IND", - "Bad container failure", - ) - ELE_BAD_VERSION_FAILURE_IND = ( - 0xF8, - "ELE_BAD_VERSION_FAILURE_IND", - "Bad version failure", - ) - ELE_INVALID_KEY_FAILURE_IND = ( - 0xF9, - "ELE_INVALID_KEY_FAILURE_IND", - "The key in the container is invalid", - ) - ELE_BAD_KEY_HASH_FAILURE_IND = ( - 0xFA, - "ELE_BAD_KEY_HASH_FAILURE_IND", - "The key hash verification does not match OTP", - ) - ELE_NO_VALID_CONTAINER_FAILURE_IND = ( - 0xFB, - "ELE_NO_VALID_CONTAINER_FAILURE_IND", - "No valid container failure", - ) - ELE_BAD_CERTIFICATE_FAILURE_IND = ( - 0xFC, - "ELE_BAD_CERTIFICATE_FAILURE_IND", - "Bad certificate failure", - ) - ELE_BAD_UID_FAILURE_IND = (0xFD, "ELE_BAD_UID_FAILURE_IND", "Bad UID failure") - ELE_BAD_MONOTONIC_COUNTER_FAILURE_IND = ( - 0xFE, - "ELE_BAD_MONOTONIC_COUNTER_FAILURE_IND", - "Bad monotonic counter failure", - ) - ELE_ABORT_IND = (0xFF, "ELE_ABORT_IND", "Abort") - - -class EleFwStatus(SpsdkSoftEnum): - """ELE Firmware status.""" - - ELE_FW_STATUS_NOT_IN_PLACE = (0, "ELE_FW_STATUS_NOT_IN_PLACE", "Not in place") - ELE_FW_STATUS_IN_PLACE = ( - 1, - "ELE_FW_STATUS_IN_PLACE", - "Authenticated and operational", - ) - - -class EleInfo2Commit(SpsdkSoftEnum): - """ELE Information type to be committed.""" - - NXP_SRK_REVOCATION = ( - 0x1 << 0, - "NXP_SRK_REVOCATION", - "SRK revocation of the NXP container", - ) - NXP_FW_FUSE = (0x1 << 1, "NXP_FW_FUSE", "FW fuse version of the NXP container") - OEM_SRK_REVOCATION = ( - 0x1 << 4, - "OEM_SRK_REVOCATION", - "SRK revocation of the OEM container", - ) - OEM_FW_FUSE = (0x1 << 5, "OEM_FW_FUSE", "FW fuse version of the OEM container") - - -class KeyBlobEncryptionAlgorithm(SpsdkSoftEnum): - """ELE KeyBlob encryption algorithms.""" - - AES_CBC = (0x03, "AES_CBC", "KeyBlob encryption algorithm AES CBC") - AES_CTR = (0x04, "AES_CTR", "KeyBlob encryption algorithm AES CTR") - AES_XTS = (0x37, "AES_XTS", "KeyBlob encryption algorithm AES XTS") - SM4_CBC = (0x2B, "SM4_CBC", "KeyBlob encryption algorithm SM4 CBC") - - -class KeyBlobEncryptionIeeCtrModes(SpsdkSoftEnum): - """IEE Keyblob mode attributes.""" - - AesCTRWAddress = (0x02, "CTR_WITH_ADDRESS", " AES CTR w address binding mode") - AesCTRWOAddress = (0x03, "CTR_WITHOUT_ADDRESS", " AES CTR w/o address binding mode") - AesCTRkeystream = (0x04, "CTR_KEY_STREAM", "AES CTR keystream only") - - -class EleTrngState(SpsdkSoftEnum): - """ELE TRNG state.""" - - ELE_TRNG_NOT_READY = ( - 0x0, - "ELE_TRNG_NOT_READY", - "True random generator not started yet. Use 'start-trng' command", - ) - ELE_TRNG_PROGRAM = (0x1, "ELE_TRNG_PROGRAM", "TRNG is in program mode") - ELE_TRNG_GENERATING_ENTROPY = ( - 0x2, - "ELE_TRNG_GENERATING_ENTROPY", - "TRNG is still generating entropy", - ) - ELE_TRNG_READY = ( - 0x3, - "ELE_TRNG_READY", - "TRNG entropy is valid and ready to be read", - ) - ELE_TRNG_ERROR = ( - 0x4, - "ELE_TRNG_ERROR", - "TRNG encounter an error while generating entropy", - ) - - -class EleCsalState(SpsdkSoftEnum): - """ELE CSAL state.""" - - ELE_CSAL_NOT_READY = ( - 0x0, - "ELE_CSAL_NOT_READY", - "Crypto Lib random context initialization is not done yet", - ) - ELE_CSAL_ON_GOING = ( - 0x1, - "ELE_CSAL_ON_GOING", - "Crypto Lib random context initialization is on-going", - ) - ELE_CSAL_READY = ( - 0x2, - "ELE_CSAL_READY", - "Crypto Lib random context initialization succeed", - ) - ELE_CSAL_ERROR = ( - 0x3, - "ELE_CSAL_ERROR", - "Crypto Lib random context initialization failed", - ) - ELE_CSAL_PAUSE = ( - 0x4, - "ELE_CSAL_PAUSE", - "Crypto Lib random context initialization is in 'pause' mode", - ) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py b/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py deleted file mode 100644 index 09cfd6aa..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/ele/ele_message.py +++ /dev/null @@ -1,1585 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""EdgeLock Enclave Message.""" - - -import logging -from struct import pack, unpack -from typing import Dict, List, Optional - -from crcmod.predefined import mkPredefinedCrcFun - -from ..ele.ele_constants import ( - EleCsalState, - EleFwStatus, - EleInfo2Commit, - EleTrngState, - KeyBlobEncryptionAlgorithm, - KeyBlobEncryptionIeeCtrModes, - LifeCycle, - LifeCycleToSwitch, - MessageIDs, - MessageUnitId, - ResponseIndication, - ResponseStatus, -) -from ..exceptions import SPSDKParsingError, SPSDKValueError -from ..image.ahab.signed_msg import SignedMessage -from ..utils.misc import Endianness, align, align_block -from ..utils.spsdk_enum import SpsdkEnum - -logger = logging.getLogger(__name__) - -LITTLE_ENDIAN = "<" -UINT8 = "B" -UINT16 = "H" -UINT32 = "L" -UINT64 = "Q" -RESERVED = 0 - - -class EleMessage: - """Base class for any EdgeLock Enclave Message. - - Message contains a header - tag, command id, size and version. - """ - - CMD = 0x00 - TAG = 0x17 - RSP_TAG = 0xE1 - VERSION = 0x06 - HEADER_FORMAT = LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8 - COMMAND_HEADER_WORDS_COUNT = 1 - COMMAND_PAYLOAD_WORDS_COUNT = 0 - RESPONSE_HEADER_WORDS_COUNT = 2 - RESPONSE_PAYLOAD_WORDS_COUNT = 0 - ELE_MSG_ALIGN = 8 - MAX_RESPONSE_DATA_SIZE = 0 - MAX_COMMAND_DATA_SIZE = 0 - - def __init__(self) -> None: - """Class object initialized.""" - self.abort_code = 0 - self.indication = 0 - self.status = 0 - self.buff_addr = 0 - self.buff_size = 0 - self.command = self.CMD - self._response_data_size = self.MAX_RESPONSE_DATA_SIZE - - @property - def command_address(self) -> int: - """Command address in target memory space.""" - return align(self.buff_addr, self.ELE_MSG_ALIGN) - - @property - def command_words_count(self) -> int: - """Command Words count.""" - return self.COMMAND_HEADER_WORDS_COUNT + self.COMMAND_PAYLOAD_WORDS_COUNT - - @property - def has_command_data(self) -> bool: - """Check if command has additional data.""" - return bool(self.command_data_size > 0) - - @property - def command_data_address(self) -> int: - """Command data address in target memory space.""" - return align( - self.command_address + self.command_words_count * 4, self.ELE_MSG_ALIGN - ) - - @property - def command_data_size(self) -> int: - """Command data address in target memory space.""" - return align( - len(self.command_data) or self.MAX_COMMAND_DATA_SIZE, self.ELE_MSG_ALIGN - ) - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - return b"" - - @property - def response_address(self) -> int: - """Response address in target memory space.""" - if self.has_command_data: - address = self.command_data_address + self.command_data_size - else: - address = self.buff_addr + self.command_words_count * 4 - return align(address, self.ELE_MSG_ALIGN) - - @property - def response_words_count(self) -> int: - """Response Words count.""" - return self.RESPONSE_HEADER_WORDS_COUNT + self.RESPONSE_PAYLOAD_WORDS_COUNT - - @property - def has_response_data(self) -> bool: - """Check if response has additional data.""" - return bool(self.response_data_size > 0) - - @property - def response_data_address(self) -> int: - """Response data address in target memory space.""" - return align( - self.response_address + self.response_words_count * 4, self.ELE_MSG_ALIGN - ) - - @property - def response_data_size(self) -> int: - """Response data address in target memory space.""" - return align(self._response_data_size, self.ELE_MSG_ALIGN) - - @property - def free_space_address(self) -> int: - """First free address after ele message in target memory space.""" - return align( - self.response_data_address + self._response_data_size, self.ELE_MSG_ALIGN - ) - - @property - def free_space_size(self) -> int: - """Free space size after ele message in target memory space.""" - return align( - self.buff_size - (self.free_space_address - self.buff_addr), - self.ELE_MSG_ALIGN, - ) - - @property - def status_string(self) -> str: - """Get status in readable string format.""" - if self.status not in ResponseStatus: - return "Invalid status!" - if self.status == ResponseStatus.ELE_SUCCESS_IND: - return "Succeeded" - indication = ( - ResponseIndication.get_label(self.indication) - if ResponseIndication.contains(self.indication) - else f"Invalid indication code: {self.indication:02X}" - ) - return f"Failed: {indication}" - - def set_buffer_params(self, buff_addr: int, buff_size: int) -> None: - """Set the communication buffer parameters to allow command update addresses inside command payload. - - :param buff_addr: Real address of communication buffer in target memory space - :param buff_size: Size of communication buffer in target memory space - """ - self.buff_addr = buff_addr - self.buff_size = buff_size - - self.validate_buffer_params() - - def validate_buffer_params(self) -> None: - """Validate communication buffer parameters. - - raises SPSDKValueError: Invalid buffer parameters. - """ - if self.has_response_data: - needed_space = self.response_data_address + self.response_data_size - else: - needed_space = self.response_address + self.response_words_count * 4 - - if self.buff_size < needed_space - self.buff_addr: - raise SPSDKValueError( - "ELE Message: Communication buffer is to small to fit message. " - f"({needed_space-self.buff_addr} > {self.buff_size})" - ) - - def validate(self) -> None: - """Validate message.""" - - def header_export( - self, - ) -> bytes: - """Exports message header to bytes. - - :return: Bytes representation of message header. - """ - return pack( - self.HEADER_FORMAT, - self.VERSION, - self.command_words_count, - self.command, - self.TAG, - ) - - def export( - self, - ) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - return self.header_export() - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - # Decode and validate header - (version, size, command, tag) = unpack(self.HEADER_FORMAT, response[:4]) - if tag != self.RSP_TAG: - raise SPSDKParsingError(f"Message TAG in response is invalid: {hex(tag)}") - if command != self.command: - raise SPSDKParsingError( - f"Message COMMAND in response is invalid: {hex(command)}" - ) - if size not in [self.response_words_count, self.RESPONSE_HEADER_WORDS_COUNT]: - raise SPSDKParsingError(f"Message SIZE in response is invalid: {hex(size)}") - if version != self.VERSION: - raise SPSDKParsingError( - f"Message VERSION in response is invalid: {hex(version)}" - ) - - # Decode status word - ( - self.status, - self.indication, - self.abort_code, - ) = unpack(LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, response[4:8]) - - def decode_response_data(self, response_data: bytes) -> None: - """Decode response data from target. - - :note: The response data are specific per command. - :param response_data: Data of response. - """ - - def __eq__(self, other: object) -> bool: - if isinstance(other, EleMessage): - if ( - self.TAG == other.TAG - and self.command == other.command - and self.VERSION == other.VERSION - and self.command_words_count == other.command_words_count - ): - return True - - return False - - @staticmethod - def get_msg_crc(payload: bytes) -> bytes: - """Compute message CRC. - - :param payload: The input data to compute CRC on them. Must be 4 bytes aligned. - :return: 4 bytes of CRC in little endian format. - """ - assert len(payload) % 4 == 0 - res = 0 - for i in range(0, len(payload), 4): - res ^= int.from_bytes(payload[i : i + 4], Endianness.LITTLE.value) - return res.to_bytes(4, Endianness.LITTLE.value) - - def response_status(self) -> str: - """Print the response status information. - - :return: String with response status. - """ - ret = f"Response status: {ResponseStatus.get_label(self.status)}\n" - if self.status == ResponseStatus.ELE_FAILURE_IND: - ret += ( - f" Response indication: {ResponseIndication.get_label(self.indication)}" - f" - ({hex(self.indication)})\n" - ) - ret += f" Response abort code: {hex(self.abort_code)}\n" - return ret - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - ret = f"Command: {MessageIDs.get_label(self.command)} - ({hex(self.command)})\n" - ret += f"Command words: {self.command_words_count}\n" - ret += f"Command data: {self.has_command_data}\n" - ret += f"Response words: {self.response_words_count}\n" - ret += f"Response data: {self.has_response_data}\n" - # if self.status in ResponseStatus: - ret += self.response_status() - - return ret - - -class EleMessagePing(EleMessage): - """ELE Message Ping.""" - - CMD = MessageIDs.PING_REQ.tag - - -class EleMessageDumpDebugBuffer(EleMessage): - """ELE Message Dump Debug buffer.""" - - CMD = MessageIDs.ELE_DUMP_DEBUG_BUFFER_REQ.tag - RESPONSE_PAYLOAD_WORDS_COUNT = 21 - - def __init__(self) -> None: - """Class object initialized.""" - super().__init__() - self.debug_words: List[int] = [0] * 20 - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - *self.debug_words, crc = unpack(LITTLE_ENDIAN + "20L4s", response[8:92]) - crc_computed = self.get_msg_crc(response[0:88]) - if crc != crc_computed: - raise SPSDKParsingError("Invalid message CRC for dump debug buffer") - - def response_info(self) -> str: - """Print Dumped data of debug buffer.""" - ret = "" - for i, dump_data in enumerate(self.debug_words): - ret += f"Dump debug word[{i}]: {dump_data:08X}\n" - - return ret - - -class EleMessageReset(EleMessage): - """ELE Message Reset.""" - - CMD = MessageIDs.RESET_REQ.tag - RESPONSE_HEADER_WORDS_COUNT = 0 - - -class EleMessageEleFwAuthenticate(EleMessage): - """Ele firmware authenticate request.""" - - CMD = MessageIDs.ELE_FW_AUTH_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 3 - - def __init__(self, ele_fw_address: int) -> None: - """Constructor. - - Be aware to have ELE FW in accessible memory for ROM, and - do not use the RAM memory used to communicate with ELE. - - :param ele_fw_address: Address in target memory with ele firmware. - """ - super().__init__() - self.ele_fw_address = ele_fw_address - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, - self.ele_fw_address, - 0, - self.ele_fw_address, - ) - return ret - - -class EleMessageOemContainerAuthenticate(EleMessage): - """OEM container authenticate request.""" - - CMD = MessageIDs.ELE_OEM_CNTN_AUTH_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 2 - - def __init__(self, oem_cntn_addr: int) -> None: - """Constructor. - - Be aware to have OEM Container in accessible memory for ROM. - - :param oem_cntn_addr: Address in target memory with oem container. - """ - super().__init__() - self.oem_cntn_addr = oem_cntn_addr - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT32 + UINT32, 0, self.oem_cntn_addr) - return ret - - -class EleMessageVerifyImage(EleMessage): - """Verify image request.""" - - CMD = MessageIDs.ELE_VERIFY_IMAGE_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 1 - RESPONSE_PAYLOAD_WORDS_COUNT = 2 - - def __init__(self, image_mask: int = 0x0000_0001) -> None: - """Constructor. - - The Verify Image message is sent to the ELE after a container has been - loaded into memory and processed with an Authenticate Container message. - This commands the ELE to check the hash on one or more images. - - :param image_mask: Used to indicate which images are to be checked. There must be at least - one image. Each bit corresponds to a particular image index in the header, for example, - bit 0 is for image 0, and bit 1 is for image 1, and so on. - """ - super().__init__() - self.image_mask = image_mask - self.valid_image_mask = 0 - self.invalid_image_mask = 0xFFFF_FFFF - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT32, self.image_mask) - return ret - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.valid_image_mask, self.invalid_image_mask = unpack( - LITTLE_ENDIAN + "LL", response[8:16] - ) - checked_mask = self.valid_image_mask | self.invalid_image_mask - if self.image_mask != checked_mask: - logger.error( - "The invalid&valid mask doesn't cover requested mask to check! " - f"valid: 0x{self.valid_image_mask:08X} | invalid: 0x{self.invalid_image_mask:08X}" - f" != requested: 0x{self.image_mask:08X}" - ) - - def response_info(self) -> str: - """Print Dumped data of debug buffer.""" - ret = f"Valid image mask : 0x{self.valid_image_mask:08X}\n" - ret += f"Invalid image mask : 0x{self.invalid_image_mask:08X}" - return ret - - -class EleMessageReleaseContainer(EleMessage): - """ELE Message Release container.""" - - CMD = MessageIDs.ELE_RELEASE_CONTAINER_REQ.tag - - -class EleMessageForwardLifeCycleUpdate(EleMessage): - """Forward Life cycle update request.""" - - CMD = MessageIDs.ELE_FWD_LIFECYCLE_UP_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 1 - - def __init__(self, lifecycle_update: LifeCycleToSwitch) -> None: - """Constructor. - - Be aware that this is non-revertible operation. - - :param lifecycle_update: New life cycle value. - """ - super().__init__() - self.lifecycle_update = lifecycle_update - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack( - LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, self.lifecycle_update.tag, 0, 0 - ) - return ret - - -class EleMessageGetEvents(EleMessage): - """Get events request. - - \b - Event layout: - ------------------------- - - TAG - CMD - IND - STS - - ------------------------- - \b - """ - - CMD = MessageIDs.ELE_GET_EVENTS_REQ.tag - RESPONSE_PAYLOAD_WORDS_COUNT = 10 - - MAX_EVENT_CNT = 8 - - def __init__(self) -> None: - """Constructor. - - This message is used to retrieve any singular event that has occurred since the FW has - started. A singular event occurs when the second word of a response to any request is - different from ELE_SUCCESS_IND. That includes commands with failure response as well as - commands with successful response containing an indication (i.e. warning response). - The events are stored by the ELE in a fixed sized buffer. When the capacity of the buffer - is exceeded, new occurring events are lost. - The event buffer is systematically returned in full to the requester independently of - the actual numbers of events stored. - """ - super().__init__() - self.event_cnt = 0 - self.events: List[int] = [0] * self.MAX_EVENT_CNT - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.event_cnt, max_events, *self.events, crc = unpack( - LITTLE_ENDIAN + UINT16 + UINT16 + "8L4s", response[8:48] - ) - if max_events != self.MAX_EVENT_CNT: - logger.error( - f"Invalid maximal events count: {max_events}!={self.MAX_EVENT_CNT}" - ) - - crc_computed = self.get_msg_crc(response[0:44]) - if crc != crc_computed: - logger.error("Invalid message CRC for get events message") - - @staticmethod - def get_ipc_id(event: int) -> str: - """Get IPC ID in string from event.""" - ipc_id = (event >> 24) & 0xFF - return MessageUnitId.get_description(ipc_id, f"Unknown MU: ({ipc_id})") or "" - - @staticmethod - def get_cmd(event: int) -> str: - """Get Command in string from event.""" - cmd = (event >> 16) & 0xFF - return MessageIDs.get_description(cmd, f"Unknown Command: (0x{cmd:02})") or "" - - @staticmethod - def get_ind(event: int) -> str: - """Get Indication in string from event.""" - ind = (event >> 8) & 0xFF - return ( - ResponseIndication.get_description(ind, f"Unknown Indication: (0x{ind:02})") - or "" - ) - - @staticmethod - def get_sts(event: int) -> str: - """Get Status in string from event.""" - sts = event & 0xFF - return ( - ResponseStatus.get_description(sts, f"Unknown Status: (0x{sts:02})") or "" - ) - - def response_info(self) -> str: - """Print events info.""" - ret = f"Event count: {self.event_cnt}" - for i, event in enumerate( - self.events[: min(self.event_cnt, self.MAX_EVENT_CNT)] - ): - ret += f"\nEvent[{i}]: 0x{event:08X}" - ret += f"\n IPC ID: {self.get_ipc_id(event)}" - ret += f"\n Command: {self.get_cmd(event)}" - ret += f"\n Indication: {self.get_ind(event)}" - ret += f"\n Status: {self.get_sts(event)}" - if self.event_cnt > self.MAX_EVENT_CNT: - ret += "\nEvent count is bigger than maximal supported, " - ret += f"only first {self.MAX_EVENT_CNT} events are listed." - return ret - - -class EleMessageStartTrng(EleMessage): - """ELE Message Start True Random Generator.""" - - CMD = MessageIDs.START_RNG_REQ.tag - - -class EleMessageGetTrngState(EleMessage): - """ELE Message Get True Random Generator State.""" - - CMD = MessageIDs.GET_TRNG_STATE_REQ.tag - RESPONSE_PAYLOAD_WORDS_COUNT = 1 - - def __init__(self) -> None: - """Class object initialized.""" - super().__init__() - self.ele_trng_state = EleTrngState.ELE_TRNG_PROGRAM.tag - self.ele_csal_state = EleCsalState.ELE_CSAL_NOT_READY.tag - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.ele_trng_state, self.ele_csal_state, _ = unpack( - LITTLE_ENDIAN + UINT8 + UINT8 + "2s", response[8:12] - ) - - def response_info(self) -> str: - """Print specific information of ELE. - - :return: Information about the TRNG. - """ - return ( - f"EdgeLock Enclave TRNG state: {EleTrngState.get_description(self.ele_trng_state)}" - + f"\nEdgeLock Enclave CSAL state: {EleCsalState.get_description(self.ele_csal_state)}" - ) - - -class EleMessageCommit(EleMessage): - """ELE Message Get FW status.""" - - CMD = MessageIDs.ELE_COMMIT_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 1 - RESPONSE_PAYLOAD_WORDS_COUNT = 1 - - def __init__(self, info_to_commit: List[EleInfo2Commit]) -> None: - """Class object initialized.""" - super().__init__() - self.info_to_commit = info_to_commit - - @property - def info2commit_mask(self) -> int: - """Get info to commit mask used in command.""" - ret = 0 - for rule in self.info_to_commit: - ret |= rule.tag - return ret - - def mask_to_info2commit(self, mask: int) -> List[EleInfo2Commit]: - """Get list of info to commit from mask.""" - ret = [] - for bit in range(32): - bit_mask = 1 << bit - if mask and bit_mask: - ret.append(EleInfo2Commit.from_tag(bit)) - return ret - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT32, self.info2commit_mask) - return ret - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - mask = int.from_bytes(response[8:12], Endianness.LITTLE.value) - if mask != self.info2commit_mask: - logger.error( - f"Only those information has been committed: {[x.label for x in self.mask_to_info2commit(mask)]}," - f" from those:{[x.label for x in self.info_to_commit]}" - ) - - -class EleMessageGetFwStatus(EleMessage): - """ELE Message Get FW status.""" - - CMD = MessageIDs.GET_FW_STATUS_REQ.tag - RESPONSE_PAYLOAD_WORDS_COUNT = 1 - - def __init__(self) -> None: - """Class object initialized.""" - super().__init__() - self.ele_fw_status = EleFwStatus.ELE_FW_STATUS_NOT_IN_PLACE.tag - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.ele_fw_status, _ = unpack(LITTLE_ENDIAN + UINT8 + "3s", response[8:12]) - - def response_info(self) -> str: - """Print specific information of ELE. - - :return: Information about the ELE. - """ - return f"EdgeLock Enclave firmware state: {EleFwStatus.get_label(self.ele_fw_status)}" - - -class EleMessageGetFwVersion(EleMessage): - """ELE Message Get FW version.""" - - CMD = MessageIDs.GET_FW_VERSION_REQ.tag - RESPONSE_PAYLOAD_WORDS_COUNT = 2 - - def __init__(self) -> None: - """Class object initialized.""" - super().__init__() - self.ele_fw_version_raw = 0 - self.ele_fw_version_sha1 = 0 - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.ele_fw_version_raw = int.from_bytes( - response[8:12], Endianness.LITTLE.value - ) - self.ele_fw_version_sha1 = int.from_bytes( - response[12:16], Endianness.LITTLE.value - ) - - def response_info(self) -> str: - """Print specific information of ELE. - - :return: Information about the ELE. - """ - ret = ( - f"EdgeLock Enclave firmware version: {self.ele_fw_version_raw:08X}\n" - f"Readable form: {(self.ele_fw_version_raw>>16) & 0xff}." - f"{(self.ele_fw_version_raw>>4) & 0xfff}.{self.ele_fw_version_raw & 0xf}\n" - f"Commit SHA1 (First 4 bytes): {self.ele_fw_version_sha1:08X}" - ) - if self.ele_fw_version_raw & 1 << 31: - ret += "\nDirty build" - return ret - - -class EleMessageReadCommonFuse(EleMessage): - """ELE Message Read common fuse.""" - - CMD = MessageIDs.READ_COMMON_FUSE.tag - COMMAND_PAYLOAD_WORDS_COUNT = 1 - RESPONSE_PAYLOAD_WORDS_COUNT = 1 - - def __init__(self, index: int) -> None: - """Constructor. - - Read common fuse. - - :param index: Fuse ID. - """ - super().__init__() - self.index = index - self.fuse_value = 0 - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT16 + UINT16, self.index, 0) - return ret - - def decode_response(self, response: bytes) -> None: - """Decode response from target. - - :param response: Data of response. - :raises SPSDKParsingError: Response parse detect some error. - """ - super().decode_response(response) - self.fuse_value = int.from_bytes(response[8:12], Endianness.LITTLE.value) - - def response_info(self) -> str: - """Print fuse value. - - :return: Read fuse value. - """ - return f"Fuse ID_{self.index}: 0x{self.fuse_value:08X}\n" - - -class EleMessageReadShadowFuse(EleMessageReadCommonFuse): - """ELE Message Read shadow fuse.""" - - CMD = MessageIDs.READ_SHADOW_FUSE.tag - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - ret += pack(LITTLE_ENDIAN + UINT32, self.index) - return ret - - -class EleMessageGetInfo(EleMessage): - """ELE Message Get Info.""" - - CMD = MessageIDs.GET_INFO_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 3 - MAX_RESPONSE_DATA_SIZE = 256 - - def __init__(self) -> None: - """Class object initialized.""" - super().__init__() - self.info_length = 0 - self.info_version = 0 - self.info_cmd = 0 - self.info_soc_rev = 0 - self.info_soc_id = 0 - self.info_life_cycle = 0 - self.info_sssm_state = 0 - self.info_uuid = bytes() - self.info_sha256_rom_patch = bytes() - self.info_sha256_fw = bytes() - self.info_oem_srkh = bytes() - self.info_imem_state = 0 - self.info_csal_state = 0 - self.info_trng_state = 0 - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT16 + UINT16, - 0, - self.response_data_address, - self.response_data_size, - 0, - ) - return self.header_export() + payload - - def decode_response_data(self, response_data: bytes) -> None: - """Decode response data from target. - - :note: The response data are specific per command. - :param response_data: Data of response. - """ - (self.info_cmd, self.info_version, self.info_length) = unpack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, response_data[:4] - ) - - (self.info_soc_id, self.info_soc_rev) = unpack( - LITTLE_ENDIAN + UINT16 + UINT16, response_data[4:8] - ) - (self.info_life_cycle, self.info_sssm_state, _) = unpack( - LITTLE_ENDIAN + UINT16 + UINT8 + UINT8, response_data[8:12] - ) - self.info_uuid = response_data[12:28] - self.info_sha256_rom_patch = response_data[28:60] - self.info_sha256_fw = response_data[60:92] - if self.info_version == 0x02: - self.info_oem_srkh = response_data[92:156] - self.info_oem_srkh = response_data[92:156] - ( - self.info_trng_state, - self.info_csal_state, - self.info_imem_state, - _, - ) = unpack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, response_data[156:160] - ) - - def response_info(self) -> str: - """Print specific information of ELE. - - :return: Information about the ELE. - """ - ret = f"Command: {hex(self.info_cmd)}\n" - ret += f"Version: {self.info_version}\n" - ret += f"Length: {self.info_length}\n" - ret += f"SoC ID: {self.info_soc_id:04X}\n" - ret += f"SoC version: {self.info_soc_rev:04X}\n" - ret += f"Life Cycle: {LifeCycle.get_label(self.info_life_cycle)} - 0x{self.info_life_cycle:04X}\n" - ret += f"SSSM state: {self.info_sssm_state}\n" - ret += f"UUID: {self.info_uuid.hex()}\n" - ret += f"SHA256 ROM PATCH: {self.info_sha256_rom_patch.hex()}\n" - ret += f"SHA256 FW: {self.info_sha256_fw.hex()}\n" - if self.info_version == 0x02: - ret += "Advanced information:\n" - ret += f" OEM SRKH: {self.info_oem_srkh.hex()}\n" - ret += f" IMEM state: {self.info_imem_state}\n" - ret += ( - f" CSAL state: " - f"{EleCsalState.get_description(self.info_csal_state, str(self.info_csal_state))}\n" - ) - ret += ( - f" TRNG state: " - f"{EleTrngState.get_description(self.info_trng_state, str(self.info_trng_state))}\n" - ) - - return ret - - -class EleMessageDeriveKey(EleMessage): - """ELE Message Derive Key.""" - - CMD = MessageIDs.ELE_DERIVE_KEY_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 6 - MAX_RESPONSE_DATA_SIZE = 32 - _MAX_COMMAND_DATA_SIZE = 65536 - SUPPORTED_KEY_SIZES = [16, 32] - - def __init__(self, key_size: int, context: Optional[bytes]) -> None: - """Class object initialized. - - :param key_size: Output key size [16,32] is valid - :param context: User's context to be used for key diversification - """ - if key_size not in self.SUPPORTED_KEY_SIZES: - raise SPSDKValueError( - f"Output Key size ({key_size}) must be in {self.SUPPORTED_KEY_SIZES}" - ) - if context and len(context) > self._MAX_COMMAND_DATA_SIZE: - raise SPSDKValueError( - f"User context length ({len(context)}) <= {self._MAX_COMMAND_DATA_SIZE}" - ) - super().__init__() - self.key_size = key_size - self._response_data_size = key_size - self.context = context - self.derived_key = b"" - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32 + UINT32 + UINT16 + UINT16, - 0, - self.response_data_address, - 0, - self.command_data_address if self.context else 0, - self.key_size, - self.command_data_size, - ) - header = self.header_export() - return header + payload + self.get_msg_crc(header + payload) - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - return self.context if self.context else b"" - - def decode_response_data(self, response_data: bytes) -> None: - """Decode response data from target. - - :note: The response data are specific per command. - :param response_data: Data of response. - """ - self.derived_key = response_data[: self.key_size] - - def get_key(self) -> bytes: - """Get derived key.""" - return self.derived_key - - -class EleMessageSigned(EleMessage): - """ELE Message Signed.""" - - COMMAND_PAYLOAD_WORDS_COUNT = 2 - - def __init__(self, signed_msg: bytes) -> None: - """Class object initialized. - - :param signed_msg: Signed message container. - """ - super().__init__() - self.signed_msg_binary = signed_msg - # Get the command inside the signed message - self.signed_msg = SignedMessage.parse(signed_msg) - self.signed_msg.update_fields() - assert self.signed_msg.message - self.command = self.signed_msg.message.cmd - self._command_data_size = len(self.signed_msg_binary) - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32, - 0, - self.command_data_address, - ) - return self.header_export() + payload - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - return self.signed_msg_binary - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - ret = super().info() - ret += "\n" + self.signed_msg.image_info().draw() - - return ret - - -class EleMessageGenerateKeyBlob(EleMessage): - """ELE Message Generate KeyBlob.""" - - KEYBLOB_NAME = "Unknown" - # List of supported algorithms and theirs key sizes - SUPPORTED_ALGORITHMS: Dict[SpsdkEnum, List[int]] = {} - - KEYBLOB_TAG = 0x81 - KEYBLOB_VERSION = 0x00 - CMD = MessageIDs.GENERATE_KEY_BLOB_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 7 - MAX_RESPONSE_DATA_SIZE = 512 - - def __init__( - self, key_identifier: int, algorithm: KeyBlobEncryptionAlgorithm, key: bytes - ) -> None: - """Constructor of Generate Key Blob class. - - :param key_identifier: ID of key - :param algorithm: Select supported algorithm - :param key: Key to be wrapped - """ - super().__init__() - self.key_id = key_identifier - self.algorithm = algorithm - - self.key = key - self.key_blob = bytes() - self.validate() - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - payload = pack( - LITTLE_ENDIAN - + UINT32 - + UINT32 - + UINT32 - + UINT32 - + UINT32 - + UINT16 - + UINT16, - self.key_id, - 0, - self.command_data_address, - 0, - self.response_data_address, - self.MAX_RESPONSE_DATA_SIZE, - 0, - ) - payload = self.header_export() + payload - return payload + EleMessage.get_msg_crc(payload) - - def validate(self) -> None: - """Validate generate keyblob message data. - - :raises SPSDKValueError: Invalid used key size or encryption algorithm - """ - if self.algorithm not in self.SUPPORTED_ALGORITHMS: - raise SPSDKValueError( - f"{self.algorithm} is not supported by {self.KEYBLOB_NAME} keyblob in ELE." - ) - - if len(self.key) * 8 not in self.SUPPORTED_ALGORITHMS[self.algorithm]: - raise SPSDKValueError( - f"Unsupported size of input key by {self.KEYBLOB_NAME} keyblob" - f" for {self.algorithm.label} algorithm." - f"The list of supported keys in bit count: {self.SUPPORTED_ALGORITHMS[self.algorithm]}" - ) - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - ret = super().info() - ret += "\n" - ret += f"KeyBlob type: {self.KEYBLOB_NAME}\n" - ret += f"Key ID: {self.key_id}\n" - ret += f"Algorithm: {self.algorithm.label}\n" - ret += f"Key size: {len(self.key)*8} bits\n" - return ret - - @classmethod - def get_supported_algorithms(cls) -> List[str]: - """Get the list of supported algorithms. - - :return: List of supported algorithm names. - """ - return list(x.label for x in cls.SUPPORTED_ALGORITHMS) - - @classmethod - def get_supported_key_sizes(cls) -> str: - """Get table with supported key sizes per algorithm. - - :return: Table with supported key size in text. - """ - ret = "" - for key, value in cls.SUPPORTED_ALGORITHMS.items(): - ret += key.label + ": " + str(value) + ",\n" - return ret - - def decode_response_data(self, response_data: bytes) -> None: - """Decode response data from target. - - :note: The response data are specific per command. - :param response_data: Data of response. - :raises SPSDKParsingError: Invalid response detected. - """ - ver, length, tag = unpack( - LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, response_data[:4] - ) - if tag != self.KEYBLOB_TAG: - raise SPSDKParsingError("Invalid TAG in generated KeyBlob") - if ver != self.KEYBLOB_VERSION: - raise SPSDKParsingError("Invalid Version in generated KeyBlob") - if length > self.MAX_RESPONSE_DATA_SIZE: - raise SPSDKParsingError("Invalid Length in generated KeyBlob") - - self.key_blob = response_data[:length] - - -class EleMessageGenerateKeyBlobDek(EleMessageGenerateKeyBlob): - """ELE Message Generate DEK KeyBlob.""" - - KEYBLOB_NAME = "DEK" - # List of supported algorithms and theirs key sizes - SUPPORTED_ALGORITHMS = { - KeyBlobEncryptionAlgorithm.AES_CBC: [128, 192, 256], - KeyBlobEncryptionAlgorithm.SM4_CBC: [128], - } - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - header = pack( - LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, - self.KEYBLOB_VERSION, - 8 + len(self.key), - self.KEYBLOB_TAG, - ) - options = pack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, - 0x01, # Flags - DEK - len(self.key), - self.algorithm.tag, - 0, - ) - return header + options + self.key - - -class EleMessageGenerateKeyBLobOtfad(EleMessageGenerateKeyBlob): - """ELE Message Generate OTFAD KeyBlob.""" - - KEYBLOB_NAME = "OTFAD" - # List of supported algorithms and theirs key sizes - SUPPORTED_ALGORITHMS = {KeyBlobEncryptionAlgorithm.AES_CTR: [128]} - - def __init__( - self, - key_identifier: int, - key: bytes, - aes_counter: bytes, - start_address: int, - end_address: int, - read_only: bool = True, - decryption_enabled: bool = True, - configuration_valid: bool = True, - ) -> None: - """Constructor of generate OTFAD keyblob class. - - :param key_identifier: ID of Key - :param key: OTFAD key - :param aes_counter: AES counter value - :param start_address: Start address in memory to be encrypted - :param end_address: End address in memory to be encrypted - :param read_only: Read only flag, defaults to True - :param decryption_enabled: Decryption enable flag, defaults to True - :param configuration_valid: Configuration valid flag, defaults to True - """ - self.aes_counter = aes_counter - self.start_address = start_address - self.end_address = end_address - self.read_only = read_only - self.decryption_enabled = decryption_enabled - self.configuration_valid = configuration_valid - super().__init__(key_identifier, KeyBlobEncryptionAlgorithm.AES_CTR, key) - - def validate(self) -> None: - """Validate generate OTFAD keyblob.""" - # Validate general members - super().validate() - # 1 Validate OTFAD Key identifier - struct_index = self.key_id & 0xFF - peripheral_index = (self.key_id >> 8) & 0xFF - reserved = self.key_id & 0xFFFF0000 - - if struct_index > 3: - raise SPSDKValueError( - "Invalid OTFAD Key Identifier. Byte 0 must be in range [0-3]," - " to select used key struct, for proper scrambling." - ) - - if peripheral_index not in [1, 2]: - raise SPSDKValueError( - "Invalid OTFAD Key Identifier. Byte 1 must be in range [1-2]," - " to select used peripheral [FlexSPIx]." - ) - - if reserved != 0: - raise SPSDKValueError( - "Invalid OTFAD Key Identifier. Byte 2-3 must be set to 0." - ) - - # 2. validate AES counter - if len(self.aes_counter) != 8: - raise SPSDKValueError("Invalid AES counter length. It must be 64 bits.") - - # 3. start address - if self.start_address != 0 and self.start_address != align( - self.start_address, 1024 - ): - raise SPSDKValueError( - "Invalid OTFAD start address. Start address has to be aligned to 1024 bytes." - ) - - # 4. end address - if self.end_address != 0 and self.end_address != align(self.end_address, 1024): - raise SPSDKValueError( - "Invalid OTFAD end address. End address has to be aligned to 1024 bytes." - ) - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - header = pack( - LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, - self.KEYBLOB_VERSION, - 0x30, - self.KEYBLOB_TAG, - ) - options = pack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, - 0x02, # Flags - OTFAD - 0x28, - self.algorithm.tag, - 0, - ) - end_address = self.end_address - if self.read_only: - end_address |= 0x04 - if self.decryption_enabled: - end_address |= 0x02 - if self.configuration_valid: - end_address |= 0x01 - - otfad_config = pack( - LITTLE_ENDIAN + "16s" + "8s" + UINT32 + UINT32 + UINT32, - self.key, - self.aes_counter, - self.start_address, - end_address, - 0, - ) - crc32_function = mkPredefinedCrcFun("crc-32-mpeg") - crc: int = crc32_function(otfad_config) - return ( - header + options + otfad_config + crc.to_bytes(4, Endianness.LITTLE.value) - ) - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - ret = super().info() - ret += f"AES Counter: {self.aes_counter.hex()}\n" - ret += f"Start address: {self.start_address:08x}\n" - ret += f"End address: {self.end_address:08x}\n" - ret += f"Read_only: {self.read_only}\n" - ret += f"Enabled: {self.decryption_enabled}\n" - ret += f"Valid: {self.configuration_valid}\n" - return ret - - -class EleMessageGenerateKeyBlobIee(EleMessageGenerateKeyBlob): - """ELE Message Generate IEE KeyBlob.""" - - KEYBLOB_NAME = "IEE" - # List of supported algorithms and theirs key sizes - SUPPORTED_ALGORITHMS = { - KeyBlobEncryptionAlgorithm.AES_XTS: [256, 512], - KeyBlobEncryptionAlgorithm.AES_CTR: [128, 256], - } - - def __init__( - self, - key_identifier: int, - algorithm: KeyBlobEncryptionAlgorithm, - key: bytes, - ctr_mode: KeyBlobEncryptionIeeCtrModes, - aes_counter: bytes, - page_offset: int, - region_number: int, - bypass: bool = False, - locked: bool = False, - ) -> None: - """Constructor of generate IEE keyblob class. - - :param key_identifier: ID of key - :param algorithm: Used algorithm - :param key: IEE key - :param ctr_mode: In case of AES CTR algorithm, the CTR mode must be selected - :param aes_counter: AES counter in case of AES CTR algorithm - :param page_offset: IEE page offset - :param region_number: Region number - :param bypass: Encryption bypass flag, defaults to False - :param locked: Locked flag, defaults to False - """ - self.ctr_mode = ctr_mode - self.aes_counter = aes_counter - self.page_offset = page_offset - self.region_number = region_number - self.bypass = bypass - self.locked = locked - super().__init__(key_identifier, algorithm, key) - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - header = pack( - LITTLE_ENDIAN + UINT8 + UINT16 + UINT8, - self.KEYBLOB_VERSION, - 88, - self.KEYBLOB_TAG, - ) - options = pack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT8 + UINT8, - 0x03, # Flags - IEE - len(self.key), - self.algorithm.tag, - 0, - ) - region_attribute = 0 - if self.bypass: - region_attribute |= 1 << 7 - if self.algorithm == KeyBlobEncryptionAlgorithm.AES_XTS: - region_attribute |= 0b01 << 4 - if len(self.key) == 64: - region_attribute |= 0x01 - else: - region_attribute |= self.ctr_mode.tag << 4 - if len(self.key) == 32: - region_attribute |= 0x01 - - if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: - key1 = align_block(self.key, 32, 0) - key2 = align_block(self.aes_counter, 32, 0) - else: - key_len = len(self.key) - key1 = align_block(self.key[: key_len // 2], 32, 0) - key2 = align_block(self.key[key_len // 2 :], 32, 0) - - lock_options = pack( - LITTLE_ENDIAN + UINT8 + UINT8 + UINT16, - self.region_number, - 0x01 if self.locked else 0x00, - 0, - ) - - iee_config = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + "32s" + "32s" + "4s", - region_attribute, - self.page_offset, - key1, - key2, - lock_options, - ) - crc32_function = mkPredefinedCrcFun("crc-32-mpeg") - crc: int = crc32_function(iee_config) - return header + options + iee_config + crc.to_bytes(4, Endianness.LITTLE.value) - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: - key1 = align_block(self.key, 32, 0) - key2 = align_block(self.aes_counter, 32, 0) - else: - key_len = len(self.key) - key1 = align_block(self.key[: key_len // 2], 32, 0) - key2 = align_block(self.key[key_len // 2 :], 32, 0) - ret = super().info() - if self.algorithm == KeyBlobEncryptionAlgorithm.AES_CTR: - ret += f"AES Counter mode:{KeyBlobEncryptionIeeCtrModes.get_description(self.ctr_mode.tag)}\n" - ret += f"AES Counter: {self.aes_counter.hex()}\n" - ret += f"Key1: {key1.hex()}\n" - ret += f"Key2: {key2.hex()}\n" - ret += f"Page offset: {self.page_offset:08x}\n" - ret += f"Region number: {self.region_number:02x}\n" - ret += f"Bypass: {self.bypass}\n" - ret += f"Locked: {self.locked}\n" - return ret - - -class EleMessageLoadKeyBLob(EleMessage): - """ELE Message Load KeyBlob.""" - - CMD = MessageIDs.LOAD_KEY_BLOB_REQ.tag - COMMAND_PAYLOAD_WORDS_COUNT = 3 - - def __init__(self, key_identifier: int, keyblob: bytes) -> None: - """Constructor of Load Key Blob class. - - :param key_identifier: ID of key - :param keyblob: Keyblob to be wrapped - """ - super().__init__() - self.key_id = key_identifier - - self.keyblob = keyblob - self.validate() - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - payload = pack( - LITTLE_ENDIAN + UINT32 + UINT32 + UINT32, - self.key_id, - 0, - self.command_data_address, - ) - payload = self.header_export() + payload - return payload - - @property - def command_data(self) -> bytes: - """Command data to be loaded into target memory space.""" - return self.keyblob - - def info(self) -> str: - """Print information including live data. - - :return: Information about the message. - """ - ret = super().info() - ret += "\n" - ret += f"Key ID: {self.key_id}\n" - ret += f"KeyBlob size: {len(self.keyblob)}\n" - return ret - - -class EleMessageWriteFuse(EleMessage): - """Write Fuse request.""" - - CMD = MessageIDs.WRITE_FUSE.tag - COMMAND_PAYLOAD_WORDS_COUNT = 2 - - def __init__( - self, bit_position: int, bit_length: int, lock: bool, payload: int - ) -> None: - """Constructor. - - This command allows to write to the fuses. - OEM Fuses are accessible depending on the chip lifecycle. - - :param bit_position: Fuse identifier expressed as its position in bit in the fuse map. - :param bit_length: Number of bits to be written. - :param lock: Write lock requirement. When set to 1, fuse words are locked. When unset, no write lock is done. - :param payload: Data to be written - """ - super().__init__() - self.bit_position = bit_position - self.bit_length = bit_length - self.lock = lock - self.payload = payload - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - - ret += pack( - LITTLE_ENDIAN + UINT16 + UINT16 + UINT32, - self.bit_position, - self.bit_length | 0x8000 if self.lock else 0, - self.payload, - ) - return ret - - -class EleMessageWriteShadowFuse(EleMessage): - """Write shadow fuse request.""" - - CMD = MessageIDs.WRITE_SHADOW_FUSE.tag - COMMAND_PAYLOAD_WORDS_COUNT = 2 - - def __init__(self, index: int, value: int) -> None: - """Constructor. - - This command allows to write to the shadow fuses. - - :param index: Fuse identifier expressed as its position in bit in the fuse map. - :param value: Data to be written. - """ - super().__init__() - self.index = index - self.value = value - - def export(self) -> bytes: - """Exports message to final bytes array. - - :return: Bytes representation of message object. - """ - ret = self.header_export() - - ret += pack( - LITTLE_ENDIAN + UINT32 + UINT32, - self.index, - self.value, - ) - return ret - - -class EleMessageEnableApc(EleMessage): - """Enable APC (Application core) ELE Message.""" - - CMD = MessageIDs.ELE_ENABLE_APC_REQ.tag - - -class EleMessageEnableRtc(EleMessage): - """Enable RTC (Real time core) ELE Message.""" - - CMD = MessageIDs.ELE_ENABLE_RTC_REQ.tag - - -class EleMessageResetApcContext(EleMessage): - """Send request to reset APC context ELE Message.""" - - CMD = MessageIDs.ELE_RESET_APC_CTX_REQ.tag diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py deleted file mode 100644 index ae89a397..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""This module contains AHAB related code.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py deleted file mode 100644 index 0e7356a2..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_abstract_interfaces.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2022-2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""AHAB abstract classes.""" - -from struct import calcsize, unpack -from typing import Tuple - -from typing_extensions import Self - -from ...exceptions import SPSDKLengthError, SPSDKParsingError, SPSDKValueError -from ...utils.abstract import BaseClass -from ...utils.misc import check_range - -LITTLE_ENDIAN = "<" -UINT8 = "B" -UINT16 = "H" -UINT32 = "L" -UINT64 = "Q" -RESERVED = 0 - - -class Container(BaseClass): - """Base class for any container.""" - - @classmethod - def fixed_length(cls) -> int: - """Returns the length of a container which is fixed. - - i.e. part of a container holds fixed values, whereas some entries have - variable length. - """ - return calcsize(cls.format()) - - def __len__(self) -> int: - """Returns the total length of a container. - - The length includes the fixed as well as the variable length part. - """ - return self.fixed_length() - - def __repr__(self) -> str: - return "Base AHAB Container class: " + self.__class__.__name__ - - def __str__(self) -> str: - raise NotImplementedError( - "__str__() is not implemented in base AHAB container class" - ) - - def export(self) -> bytes: - """Serialize object into bytes array.""" - raise NotImplementedError( - "export() is not implemented in base AHAB container class" - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Deserialize object from bytes array.""" - raise NotImplementedError( - "parse() is not implemented in base AHAB container class" - ) - - @classmethod - def format(cls) -> str: - """Returns the container data format as defined by struct package. - - The base returns only endianness (LITTLE_ENDIAN). - """ - return LITTLE_ENDIAN - - @classmethod - def _check_fixed_input_length(cls, binary: bytes) -> None: - """Checks the data length and container fixed length. - - This is just a helper function used throughout the code. - - :param Binary: Binary input data. - :raises SPSDKLengthError: If containers length is larger than data length. - """ - data_len = len(binary) - fixed_input_len = cls.fixed_length() - if data_len < fixed_input_len: - raise SPSDKLengthError( - f"Parsing error in fixed part of {cls.__name__} data!\n" - f"Input data must be at least {fixed_input_len} bytes!" - ) - - -class HeaderContainer(Container): - """A container with first byte defined as header - tag, length and version. - - Every "container" in AHAB consists of a header - tag, length and version. - - The only exception is the 'image array' or 'image array entry' respectively - which has no header at all and SRK record, which has 'signing algorithm' - instead of version. But this can be considered as a sort of SRK record - 'version'. - """ - - TAG = 0x00 - VERSION = 0x00 - - def __init__(self, tag: int, length: int, version: int): - """Class object initialized. - - :param tag: container tag. - :param length: container length. - :param version: container version. - """ - self.length = length - self.tag = tag - self.version = version - - def __eq__(self, other: object) -> bool: - if isinstance(other, (HeaderContainer, HeaderContainerInversed)): - if ( - self.tag == other.tag - and self.length == other.length - and self.version == other.version - ): - return True - - return False - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return super().format() + UINT8 + UINT16 + UINT8 - - def validate_header(self) -> None: - """Validates the header of container properties... - - i.e. tag e <0; 255>, otherwise an exception is raised. - :raises SPSDKValueError: Any MAndatory field has invalid value. - """ - if self.tag is None or not check_range(self.tag, end=0xFF): - raise SPSDKValueError( - f"AHAB: Head of Container: Invalid TAG Value: {self.tag}" - ) - if self.length is None or not check_range(self.length, end=0xFFFF): - raise SPSDKValueError( - f"AHAB: Head of Container: Invalid Length Value: {self.length}" - ) - if self.version is None or not check_range(self.version, end=0xFF): - raise SPSDKValueError( - f"AHAB: Head of Container: Invalid Version Value: {self.version}" - ) - - @classmethod - def parse_head(cls, binary: bytes) -> Tuple[int, int, int]: - """Parse binary data to get head members. - - :param binary: Binary data. - :raises SPSDKLengthError: Binary data length is not enough. - :return: Tuple with TAG, LENGTH, VERSION - """ - if len(binary) < 4: - raise SPSDKLengthError( - f"Parsing error in {cls.__name__} container head data!\n" - "Input data must be at least 4 bytes!" - ) - (version, length, tag) = unpack(HeaderContainer.format(), binary) - return tag, length, version - - @classmethod - def check_container_head(cls, binary: bytes) -> None: - """Compares the data length and container length. - - This is just a helper function used throughout the code. - - :param binary: Binary input data. - :raises SPSDKLengthError: If containers length is larger than data length. - :raises SPSDKParsingError: If containers header value doesn't match. - """ - cls._check_fixed_input_length(binary) - data_len = len(binary) - (tag, length, version) = cls.parse_head( - binary[: HeaderContainer.fixed_length()] - ) - - if ( - isinstance(cls.TAG, int) - and tag != cls.TAG - or isinstance(cls.TAG, list) - and not tag in cls.TAG - ): - raise SPSDKParsingError( - f"Parsing error of {cls.__name__} data!\n" - f"Invalid TAG {hex(tag)} loaded, expected {hex(cls.TAG)}!" - ) - - if data_len < length: - raise SPSDKLengthError( - f"Parsing error of {cls.__name__} data!\n" - f"At least {length} bytes expected, got {data_len} bytes!" - ) - - if ( - isinstance(cls.VERSION, int) - and version != cls.VERSION - or isinstance(cls.VERSION, list) - and not version in cls.VERSION - ): - raise SPSDKParsingError( - f"Parsing error of {cls.__name__} data!\n" - f"Invalid VERSION {version} loaded, expected {cls.VERSION}!" - ) - - -class HeaderContainerInversed(HeaderContainer): - """A container with first byte defined as header - tag, length and version. - - It same as "HeaderContainer" only the tag/length/version are in reverse order in binary form. - """ - - @classmethod - def parse_head(cls, binary: bytes) -> Tuple[int, int, int]: - """Parse binary data to get head members. - - :param binary: Binary data. - :raises SPSDKLengthError: Binary data length is not enough. - :return: Tuple with TAG, LENGTH, VERSION - """ - if len(binary) < 4: - raise SPSDKLengthError( - f"Parsing error in {cls.__name__} container head data!\n" - "Input data must be at least 4 bytes!" - ) - # Only SRK Table has splitted tag and version in binary format - (tag, length, version) = unpack(HeaderContainer.format(), binary) - return tag, length, version diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py deleted file mode 100644 index 487addf4..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/ahab_container.py +++ /dev/null @@ -1,3885 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2021-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Implementation of raw AHAB container support. - -This module represents a generic AHAB container implementation. You can set the -containers values at will. From this perspective, consult with your reference -manual of your device for allowed values. -""" -# pylint: disable=too-many-lines -import logging -import math -import os -from struct import calcsize, pack, unpack -from typing import Any, Dict, List, Optional, Tuple, Union - -from typing_extensions import Self - -from ... import version as spsdk_version -from ...crypto.hash import EnumHashAlgorithm, get_hash -from ...crypto.keys import ( - IS_OSCCA_SUPPORTED, - EccCurve, - PublicKey, - PublicKeyEcc, - PublicKeyRsa, - PublicKeySM2, -) -from ...crypto.signature_provider import SignatureProvider, get_signature_provider -from ...crypto.symmetric import ( - aes_cbc_decrypt, - aes_cbc_encrypt, - sm4_cbc_decrypt, - sm4_cbc_encrypt, -) -from ...crypto.types import SPSDKEncoding -from ...crypto.utils import extract_public_key, get_matching_key_id -from ...ele.ele_constants import KeyBlobEncryptionAlgorithm -from ...exceptions import ( - SPSDKError, - SPSDKLengthError, - SPSDKParsingError, - SPSDKValueError, -) -from ...image.ahab.ahab_abstract_interfaces import ( - Container, - HeaderContainer, - HeaderContainerInversed, -) -from ...utils.database import DatabaseManager, get_db, get_families -from ...utils.images import BinaryImage -from ...utils.misc import ( - BinaryPattern, - Endianness, - align, - align_block, - check_range, - extend_block, - find_file, - load_binary, - load_configuration, - load_hex_string, - reverse_bytes_in_longs, - value_to_bytes, - value_to_int, - write_file, -) -from ...utils.schema_validator import CommentedConfig, check_config -from ...utils.spsdk_enum import SpsdkEnum - -logger = logging.getLogger(__name__) - -LITTLE_ENDIAN = "<" -UINT8 = "B" -UINT16 = "H" -UINT32 = "L" -UINT64 = "Q" -RESERVED = 0 -CONTAINER_ALIGNMENT = 8 -START_IMAGE_ADDRESS = 0x2000 -START_IMAGE_ADDRESS_NAND = 0x1C00 - - -TARGET_MEMORY_SERIAL_DOWNLOADER = "serial_downloader" -TARGET_MEMORY_NOR = "nor" -TARGET_MEMORY_NAND_4K = "nand_4k" -TARGET_MEMORY_NAND_2K = "nand_2k" - -TARGET_MEMORY_BOOT_OFFSETS = { - TARGET_MEMORY_SERIAL_DOWNLOADER: 0x400, - TARGET_MEMORY_NOR: 0x1000, - TARGET_MEMORY_NAND_4K: 0x400, - TARGET_MEMORY_NAND_2K: 0x400, -} - - -class AHABTags(SpsdkEnum): - """AHAB container related tags.""" - - BLOB = (0x81, "Blob (Wrapped Data Encryption Key).") - CONTAINER_HEADER = (0x87, "Container header.") - SIGNATURE_BLOCK = (0x90, "Signature block.") - CERTIFICATE_UUID = (0xA0, "Certificate with UUID.") - CERTIFICATE_NON_UUID = (0xAF, "Certificate without UUID.") - SRK_TABLE = (0xD7, "SRK table.") - SIGNATURE = (0xD8, "Signature part of signature block.") - SRK_RECORD = (0xE1, "SRK record.") - - -class AHABCoreId(SpsdkEnum): - """AHAB cored IDs.""" - - UNDEFINED = (0, "undefined", "Undefined core") - CORTEX_M33 = (1, "cortex-m33", "Cortex M33") - CORTEX_M4 = (2, "cortex-m4", "Cortex M4") - CORTEX_M7 = (2, "cortex-m7", "Cortex M7") - CORTEX_A55 = (2, "cortex-a55", "Cortex A55") - CORTEX_M4_1 = (3, "cortex-m4_1", "Cortex M4 alternative") - CORTEX_A53 = (4, "cortex-a53", "Cortex A53") - CORTEX_A35 = (4, "cortex-a35", "Cortex A35") - CORTEX_A72 = (5, "cortex-a72", "Cortex A72") - SECO = (6, "seco", "EL enclave") - HDMI_TX = (7, "hdmi-tx", "HDMI Tx") - HDMI_RX = (8, "hdmi-rx", "HDMI Rx") - V2X_1 = (9, "v2x-1", "V2X 1") - V2X_2 = (10, "v2x-2", "V2X 2") - - -def get_key_by_val(dictionary: Dict, val: Any) -> Any: - """Get Dictionary key by its value or default. - - :param dictionary: Dictionary to search in. - :param val: Value to search - :raises SPSDKValueError: In case that dictionary doesn't contains the value. - :return: Key. - """ - for key, value in dictionary.items(): - if value == val: - return key - raise SPSDKValueError( - f"The requested value [{val}] in dictionary [{dictionary}] is not available." - ) - - -class ImageArrayEntry(Container): - """Class representing image array entry as part of image array in the AHAB container. - - Image Array Entry content:: - - +-----+---------------------------------------------------------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+---------------------------------------------------------------+ - |0x00 | Image Offset | - +-----+---------------------------------------------------------------+ - |0x04 | Image Size | - +-----+---------------------------------------------------------------+ - |0x08 | | - |-----+ Load Address (64 bits) | - |0x0C | | - +-----+---------------------------------------------------------------+ - |0x10 | | - |-----+ Entry Point (64 bits) | - |0x14 | | - +-----+---------------------------------------------------------------+ - |0x18 | Flags | - +-----+---------------------------------------------------------------+ - |0x1C | Image meta data | - +-----+---------------------------------------------------------------+ - |0x20 | | - |-----+ Hash (512 bits) | - |.... | | - +-----+---------------------------------------------------------------+ - |0x60 | IV (256 bits) | - +-----+---------------------------------------------------------------+ - - """ - - IMAGE_OFFSET_LEN = 4 - IMAGE_SIZE_LEN = 4 - LOAD_ADDRESS_LEN = 8 - ENTRY_POINT_ADDRESS_LEN = 8 - FLAGS_LEN = 4 - IMAGE_META_DATA_LEN = 4 - HASH_LEN = 64 - IV_LEN = 32 - FLAGS_TYPE_OFFSET = 0 - FLAGS_TYPE_SIZE = 4 - FLAGS_TYPES = { - "csf": 0x01, - "scd": 0x02, - "executable": 0x03, - "data": 0x04, - "dcd_image": 0x05, - "seco": 0x06, - "provisioning_image": 0x07, - "dek_validation_fcb_chk": 0x08, - "provisioning_data": 0x09, - "executable_fast_boot_image": 0x0A, - "v2x_primary": 0x0B, - "v2x_secondary": 0x0C, - "v2x_rom_patch": 0x0D, - "v2x_dummy": 0x0E, - } - FLAGS_CORE_ID_OFFSET = 4 - FLAGS_CORE_ID_SIZE = 4 - FLAGS_HASH_OFFSET = 8 - FLAGS_HASH_SIZE = 3 - FLAGS_IS_ENCRYPTED_OFFSET = 11 - FLAGS_IS_ENCRYPTED_SIZE = 1 - FLAGS_BOOT_FLAGS_OFFSET = 16 - FLAGS_BOOT_FLAGS_SIZE = 15 - METADATA_START_CPU_ID_OFFSET = 0 - METADATA_START_CPU_ID_SIZE = 10 - METADATA_MU_CPU_ID_OFFSET = 10 - METADATA_MU_CPU_ID_SIZE = 10 - METADATA_START_PARTITION_ID_OFFSET = 20 - METADATA_START_PARTITION_ID_SIZE = 8 - - IMAGE_ALIGNMENTS = { - TARGET_MEMORY_SERIAL_DOWNLOADER: 512, - TARGET_MEMORY_NOR: 1024, - TARGET_MEMORY_NAND_2K: 2048, - TARGET_MEMORY_NAND_4K: 4096, - } - - def __init__( - self, - parent: "AHABContainer", - image: Optional[bytes] = None, - image_offset: int = 0, - load_address: int = 0, - entry_point: int = 0, - flags: int = 0, - image_meta_data: int = 0, - image_hash: Optional[bytes] = None, - image_iv: Optional[bytes] = None, - already_encrypted_image: bool = False, - ) -> None: - """Class object initializer. - - :param parent: Parent AHAB Container object. - :param image: Image in bytes. - :param image_offset: Offset in bytes from start of container to beginning of image. - :param load_address: Address the image is written to in memory (absolute address in system memory map). - :param entry_point: Entry point of image (absolute address). Only valid for executable image types. - For other image types the value is irrelevant. - :param flags: flags. - :param image_meta_data: image meta-data. - :param image_hash: SHA of image (512 bits) in big endian. Left - aligned and padded with zeroes for hash sizes below 512 bits. - :param image_iv: SHA256 of plain text image (256 bits) in big endian. - :param already_encrypted_image: The input image is already encrypted. - Used only for encrypted images. - """ - self._image_offset = 0 - self.parent = parent - self.flags = flags - self.already_encrypted_image = already_encrypted_image - self.image = image if image else b"" - self.image_offset = image_offset - self.image_size = self._get_valid_size(self.image) - self.load_address = load_address - self.entry_point = entry_point - self.image_meta_data = image_meta_data - self.image_hash = image_hash - self.image_iv = ( - image_iv or get_hash(self.plain_image, algorithm=EnumHashAlgorithm.SHA256) - if self.flags_is_encrypted - else bytes(self.IV_LEN) - ) - - @property - def _ahab_container(self) -> "AHABContainer": - """AHAB Container object.""" - return self.parent - - @property - def _ahab_image(self) -> "AHABImage": - """AHAB Image object.""" - return self._ahab_container.parent - - @property - def image_offset(self) -> int: - """Image offset.""" - return self._image_offset + self._ahab_container.container_offset - - @image_offset.setter - def image_offset(self, offset: int) -> None: - """Image offset. - - :param offset: Image offset. - """ - self._image_offset = offset - self._ahab_container.container_offset - - @property - def image_offset_real(self) -> int: - """Real offset in Bootable image.""" - target_memory = self._ahab_image.target_memory - return self.image_offset + TARGET_MEMORY_BOOT_OFFSETS[target_memory] - - def __eq__(self, other: object) -> bool: - if isinstance(other, ImageArrayEntry): - if ( - self.image_offset # pylint: disable=too-many-boolean-expressions - == other.image_offset - and self.image_size == other.image_size - and self.load_address == other.load_address - and self.entry_point == other.entry_point - and self.flags == other.flags - and self.image_meta_data == other.image_meta_data - and self.image_hash == other.image_hash - and self.image_iv == other.image_iv - ): - return True - - return False - - def __repr__(self) -> str: - return f"AHAB Image Array Entry, load address({hex(self.load_address)})" - - def __str__(self) -> str: - return ( - "AHAB Image Array Entry:\n" - f" Image size: {self.image_size}B\n" - f" Image offset in table: {hex(self.image_offset)}\n" - f" Image offset real: {hex(self.image_offset_real)}\n" - f" Entry point: {hex(self.entry_point)}\n" - f" Load address: {hex(self.load_address)}\n" - f" Flags: {hex(self.flags)})\n" - f" Meta data: {hex(self.image_meta_data)})\n" - f" Image hash: {self.image_hash.hex() if self.image_hash else 'Not available'})\n" - f" Image IV: {self.image_iv.hex()})\n" - ) - - @property - def image(self) -> bytes: - """Image data for this Image array entry. - - The class decide by flags if encrypted of plain data has been returned. - - :raises SPSDKError: Invalid Image - Image is not encrypted yet. - :return: Image bytes. - """ - # if self.flags_is_encrypted and not self.already_encrypted_image: - # raise SPSDKError("Image is NOT encrypted, yet.") - - if self.flags_is_encrypted and self.already_encrypted_image: - return self.encrypted_image - return self.plain_image - - @image.setter - def image(self, data: bytes) -> None: - """Image data for this Image array entry. - - The class decide by flags if encrypted of plain data has been stored. - """ - input_image = align_block( - data, 16 if self.flags_is_encrypted else 4, padding=RESERVED - ) # align to encryptable block - self.plain_image = input_image if not self.already_encrypted_image else b"" - self.encrypted_image = input_image if self.already_encrypted_image else b"" - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() # endianness from base class - + UINT32 # Image Offset - + UINT32 # Image Size - + UINT64 # Load Address - + UINT64 # Entry Point - + UINT32 # Flags - + UINT32 # Image Meta Data - + "64s" # HASH - + "32s" # Input Vector - ) - - def update_fields(self) -> None: - """Updates the image fields in container based on provided image.""" - # self.image = align_block(self.image, self.get_valid_alignment(), 0) - self.image_size = self._get_valid_size(self.image) - algorithm = self.get_hash_from_flags(self.flags) - self.image_hash = extend_block( - get_hash(self.image, algorithm=algorithm), - self.HASH_LEN, - padding=0, - ) - if not self.image_iv and self.flags_is_encrypted: - self.image_iv = get_hash( - self.plain_image, algorithm=EnumHashAlgorithm.SHA256 - ) - - @staticmethod - def create_meta( - start_cpu_id: int = 0, mu_cpu_id: int = 0, start_partition_id: int = 0 - ) -> int: - """Create meta data field. - - :param start_cpu_id: ID of CPU to start, defaults to 0 - :param mu_cpu_id: ID of MU for selected CPU to start, defaults to 0 - :param start_partition_id: ID of partition to start, defaults to 0 - :return: Image meta data field. - """ - meta_data = start_cpu_id - meta_data |= mu_cpu_id << 10 - meta_data |= start_partition_id << 20 - return meta_data - - @staticmethod - def create_flags( - image_type: str = "executable", - core_id: AHABCoreId = AHABCoreId.CORTEX_M33, - hash_type: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, - is_encrypted: bool = False, - boot_flags: int = 0, - ) -> int: - """Create flags field. - - :param image_type: Type of image, defaults to "executable" - :param core_id: Core ID, defaults to "cortex-m33" - :param hash_type: Hash type, defaults to sha256 - :param is_encrypted: Is image encrypted, defaults to False - :param boot_flags: Boot flags controlling the SCFW boot, defaults to 0 - :return: Image flags data field. - """ - flags_data = ImageArrayEntry.FLAGS_TYPES[image_type] - flags_data |= core_id.tag << ImageArrayEntry.FLAGS_CORE_ID_OFFSET - flags_data |= { - EnumHashAlgorithm.SHA256: 0x0, - EnumHashAlgorithm.SHA384: 0x1, - EnumHashAlgorithm.SHA512: 0x2, - EnumHashAlgorithm.SM3: 0x3, - }[hash_type] << ImageArrayEntry.FLAGS_HASH_OFFSET - flags_data |= ( - 1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET if is_encrypted else 0 - ) - flags_data |= boot_flags << ImageArrayEntry.FLAGS_BOOT_FLAGS_OFFSET - - return flags_data - - @staticmethod - def get_hash_from_flags(flags: int) -> EnumHashAlgorithm: - """Get Hash algorithm name from flags. - - :param flags: Value of flags. - :return: Hash name. - """ - hash_val = (flags >> ImageArrayEntry.FLAGS_HASH_OFFSET) & ( - (1 << ImageArrayEntry.FLAGS_HASH_SIZE) - 1 - ) - return { - 0x00: EnumHashAlgorithm.SHA256, - 0x01: EnumHashAlgorithm.SHA384, - 0x02: EnumHashAlgorithm.SHA512, - 0x03: EnumHashAlgorithm.SM3, - }[hash_val] - - @property - def flags_image_type(self) -> str: - """Get Image type name from flags. - - :return: Image type name - """ - image_type_val = (self.flags >> ImageArrayEntry.FLAGS_TYPE_OFFSET) & ( - (1 << ImageArrayEntry.FLAGS_TYPE_SIZE) - 1 - ) - try: - return get_key_by_val(ImageArrayEntry.FLAGS_TYPES, image_type_val) - except SPSDKValueError: - return f"Unknown Image Type {image_type_val}" - - @property - def flags_core_id(self) -> int: - """Get Core ID from flags. - - :return: Core ID - """ - return (self.flags >> ImageArrayEntry.FLAGS_CORE_ID_OFFSET) & ( - (1 << ImageArrayEntry.FLAGS_CORE_ID_SIZE) - 1 - ) - - @property - def flags_is_encrypted(self) -> bool: - """Get Is encrypted property from flags. - - :return: True if is encrypted, false otherwise - """ - return bool( - (self.flags >> ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET) - & ((1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_SIZE) - 1) - ) - - @property - def flags_boot_flags(self) -> int: - """Get boot flags property from flags. - - :return: Boot flags - """ - return (self.flags >> ImageArrayEntry.FLAGS_BOOT_FLAGS_OFFSET) & ( - (1 << ImageArrayEntry.FLAGS_BOOT_FLAGS_SIZE) - 1 - ) - - @property - def metadata_start_cpu_id(self) -> int: - """Get CPU ID property from Meta data. - - :return: Start CPU ID - """ - return ( - self.image_meta_data >> ImageArrayEntry.METADATA_START_CPU_ID_OFFSET - ) & ((1 << ImageArrayEntry.METADATA_START_CPU_ID_SIZE) - 1) - - @property - def metadata_mu_cpu_id(self) -> int: - """Get Start CPU Memory Unit ID property from Meta data. - - :return: Start CPU MU ID - """ - return (self.image_meta_data >> ImageArrayEntry.METADATA_MU_CPU_ID_OFFSET) & ( - (1 << ImageArrayEntry.METADATA_MU_CPU_ID_SIZE) - 1 - ) - - @property - def metadata_start_partition_id(self) -> int: - """Get Start Partition ID property from Meta data. - - :return: Start Partition ID - """ - return ( - self.image_meta_data >> ImageArrayEntry.METADATA_START_PARTITION_ID_OFFSET - ) & ((1 << ImageArrayEntry.METADATA_START_PARTITION_ID_SIZE) - 1) - - def export(self) -> bytes: - """Serializes container object into bytes in little endian. - - The hash and IV are kept in big endian form. - - :return: bytes representing container content. - """ - # hash: fixed at 512 bits, left aligned and padded with zeros for hash below 512 bits. - # In case the hash is shorter, the pack() (in little endian mode) should grant, that the - # hash is left aligned and padded with zeros due to the '64s' formatter. - # iv: fixed at 256 bits. - data = pack( - self.format(), - self._image_offset, - self.image_size, - self.load_address, - self.entry_point, - self.flags, - self.image_meta_data, - self.image_hash, - self.image_iv, - ) - - return data - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - if self.image is None or self._get_valid_size(self.image) != self.image_size: - raise SPSDKValueError("Image Entry: Invalid Image binary.") - if self.image_offset is None or not check_range( - self.image_offset, end=(1 << 32) - 1 - ): - raise SPSDKValueError( - f"Image Entry: Invalid Image Offset: {self.image_offset}" - ) - if self.image_size is None or not check_range( - self.image_size, end=(1 << 32) - 1 - ): - raise SPSDKValueError(f"Image Entry: Invalid Image Size: {self.image_size}") - if self.load_address is None or not check_range( - self.load_address, end=(1 << 64) - 1 - ): - raise SPSDKValueError( - f"Image Entry: Invalid Image Load address: {self.load_address}" - ) - if self.entry_point is None or not check_range( - self.entry_point, end=(1 << 64) - 1 - ): - raise SPSDKValueError( - f"Image Entry: Invalid Image Entry point: {self.entry_point}" - ) - if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): - raise SPSDKValueError(f"Image Entry: Invalid Image Flags: {self.flags}") - if self.image_meta_data is None or not check_range( - self.image_meta_data, end=(1 << 32) - 1 - ): - raise SPSDKValueError( - f"Image Entry: Invalid Image Meta data: {self.image_meta_data}" - ) - if ( - self.image_hash is None - or not any(self.image_hash) - or len(self.image_hash) != self.HASH_LEN - ): - raise SPSDKValueError("Image Entry: Invalid Image Hash.") - - @classmethod - def parse(cls, data: bytes, parent: "AHABContainer") -> Self: # type: ignore # pylint: disable=arguments-differ - """Parse input binary chunk to the container object. - - :param parent: Parent AHABContainer object. - :param data: Binary data with Image Array Entry block to parse. - :raises SPSDKLengthError: If invalid length of image is detected. - :raises SPSDKValueError: Invalid hash for image. - :return: Object recreated from the binary data. - """ - binary_size = len(data) - # Just updates offsets from AHAB Image start As is feature of none xip containers - ImageArrayEntry._check_fixed_input_length(data) - ( - image_offset, - image_size, - load_address, - entry_point, - flags, - image_meta_data, - image_hash, - image_iv, - ) = unpack(ImageArrayEntry.format(), data[: ImageArrayEntry.fixed_length()]) - - iae = cls( - parent=parent, - image_offset=0, - image=None, - load_address=load_address, - entry_point=entry_point, - flags=flags, - image_meta_data=image_meta_data, - image_hash=image_hash, - image_iv=image_iv, - already_encrypted_image=bool( - (flags >> ImageArrayEntry.FLAGS_IS_ENCRYPTED_OFFSET) - & ((1 << ImageArrayEntry.FLAGS_IS_ENCRYPTED_SIZE) - 1) - ), - ) - iae._image_offset = image_offset - - iae_offset = ( - AHABContainer.fixed_length() - + parent.image_array_len * ImageArrayEntry.fixed_length() - + parent.container_offset - ) - - logger.debug( - ( - "Parsing Image array Entry:\n" - f"Image offset: {hex(iae.image_offset)}\n" - f"Image offset raw: {hex(iae._image_offset)}\n" - f"Image offset real: {hex(iae.image_offset_real)}" - ) - ) - if iae.image_offset + image_size - iae_offset > binary_size: - raise SPSDKLengthError( - "Container data image is out of loaded binary:" - f"Image entry record has end of image at {hex(iae.image_offset + image_size - iae_offset)}," - f" but the loaded image length has only {hex(binary_size)}B size." - ) - image = data[ - iae.image_offset - iae_offset : iae.image_offset - iae_offset + image_size - ] - image_hash_cmp = extend_block( - get_hash(image, algorithm=ImageArrayEntry.get_hash_from_flags(flags)), - ImageArrayEntry.HASH_LEN, - padding=0, - ) - if image_hash != image_hash_cmp: - raise SPSDKValueError("Parsed Container data image has invalid HASH!") - iae.image = image - return iae - - @staticmethod - def load_from_config( - parent: "AHABContainer", config: Dict[str, Any] - ) -> "ImageArrayEntry": - """Converts the configuration option into an AHAB image array entry object. - - "config" content of container configurations. - - :param parent: Parent AHABContainer object. - :param config: Configuration of ImageArray. - :return: Container Header Image Array Entry object. - """ - image_path = config.get("image_path") - search_paths = parent.search_paths - assert isinstance(image_path, str) - is_encrypted = config.get("is_encrypted", False) - meta_data = ImageArrayEntry.create_meta( - value_to_int(config.get("meta_data_start_cpu_id", 0)), - value_to_int(config.get("meta_data_mu_cpu_id", 0)), - value_to_int(config.get("meta_data_start_partition_id", 0)), - ) - image_data = load_binary(image_path, search_paths=search_paths) - flags = ImageArrayEntry.create_flags( - image_type=config.get("image_type", "executable"), - core_id=AHABCoreId.from_label(config.get("core_id", "cortex-m33")), - hash_type=EnumHashAlgorithm.from_label(config.get("hash_type", "sha256")), - is_encrypted=is_encrypted, - boot_flags=value_to_int(config.get("boot_flags", 0)), - ) - return ImageArrayEntry( - parent=parent, - image=image_data, - image_offset=value_to_int(config.get("image_offset", 0)), - load_address=value_to_int(config.get("load_address", 0)), - entry_point=value_to_int(config.get("entry_point", 0)), - flags=flags, - image_meta_data=meta_data, - image_iv=None, # IV data are updated by UpdateFields function - ) - - def create_config( - self, index: int, image_index: int, data_path: str - ) -> Dict[str, Any]: - """Create configuration of the AHAB Image data blob. - - :param index: Container index. - :param image_index: Data Image index. - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - ret_cfg: Dict[str, Union[str, int, bool]] = {} - image_name = "N/A" - if self.plain_image: - image_name = ( - f"container{index}_image{image_index}_{self.flags_image_type}.bin" - ) - write_file(self.plain_image, os.path.join(data_path, image_name), "wb") - if self.encrypted_image: - image_name_encrypted = f"container{index}_image{image_index}_{self.flags_image_type}_encrypted.bin" - write_file( - self.encrypted_image, - os.path.join(data_path, image_name_encrypted), - "wb", - ) - if image_name == "N/A": - image_name = image_name_encrypted - - ret_cfg["image_path"] = image_name - ret_cfg["image_offset"] = hex(self.image_offset) - ret_cfg["load_address"] = hex(self.load_address) - ret_cfg["entry_point"] = hex(self.entry_point) - ret_cfg["image_type"] = self.flags_image_type - core_ids = self.parent.parent._database.get_dict( - DatabaseManager.AHAB, "core_ids" - ) - ret_cfg["core_id"] = core_ids.get( - self.flags_core_id, f"Unknown ID: {self.flags_core_id}" - ) - ret_cfg["is_encrypted"] = bool(self.flags_is_encrypted) - ret_cfg["boot_flags"] = self.flags_boot_flags - ret_cfg["meta_data_start_cpu_id"] = self.metadata_start_cpu_id - ret_cfg["meta_data_mu_cpu_id"] = self.metadata_mu_cpu_id - ret_cfg["meta_data_start_partition_id"] = self.metadata_start_partition_id - ret_cfg["hash_type"] = self.get_hash_from_flags(self.flags).label - - return ret_cfg - - def get_valid_alignment(self) -> int: - """Get valid alignment for AHAB container and memory target. - - :return: AHAB valid alignment - """ - if ( - self.flags_image_type == "seco" - and self.parent.parent.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER - ): - return 4 - - return max([self.IMAGE_ALIGNMENTS[self._ahab_image.target_memory], 1024]) - - def _get_valid_size(self, image: Optional[bytes]) -> int: - """Get valid image size that will be stored. - - :return: AHAB valid image size - """ - if not image: - return 0 - return align(len(image), 4 if self.flags_image_type == "seco" else 1) - - def get_valid_offset(self, original_offset: int) -> int: - """Get valid offset for AHAB container. - - :param original_offset: Offset that should be updated to valid one - :return: AHAB valid offset - """ - alignment = self.get_valid_alignment() - alignment = max( - alignment, - self.parent.parent._database.get_int( - DatabaseManager.AHAB, "valid_offset_minimal_alignment", 4 - ), - ) - return align(original_offset, alignment) - - -class SRKRecord(HeaderContainerInversed): - """Class representing SRK (Super Root Key) record as part of SRK table in the AHAB container. - - The class holds information about RSA/ECDSA signing algorithms. - - SRK Record:: - - +-----+---------------------------------------------------------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+---------------------------------------------------------------+ - |0x00 | Tag | Length of SRK | Signing Algo | - +-----+---------------------------------------------------------------+ - |0x04 | Hash Algo | Key Size/Curve | Not Used | SRK Flags | - +-----+---------------------------------------------------------------+ - |0x08 | RSA modulus len / ECDSA X len | RSA exponent len / ECDSA Y len| - +-----+---------------------------------------------------------------+ - |0x0C | RSA modulus (big endian) / ECDSA X (big endian) | - +-----+---------------------------------------------------------------+ - |... | RSA exponent (big endian) / ECDSA Y (big endian) | - +-----+---------------------------------------------------------------+ - - """ - - TAG = AHABTags.SRK_RECORD.tag - VERSION = [0x21, 0x27, 0x28] # type: ignore - VERSION_ALGORITHMS = {"rsa": 0x21, "ecdsa": 0x27, "sm2": 0x28} - HASH_ALGORITHM = { - EnumHashAlgorithm.SHA256: 0x0, - EnumHashAlgorithm.SHA384: 0x1, - EnumHashAlgorithm.SHA512: 0x2, - EnumHashAlgorithm.SM3: 0x3, - } - ECC_KEY_TYPE = { - EccCurve.SECP521R1: 0x3, - EccCurve.SECP384R1: 0x2, - EccCurve.SECP256R1: 0x1, - } - RSA_KEY_TYPE = {2048: 0x5, 4096: 0x7} - SM2_KEY_TYPE = 0x8 - KEY_SIZES = { - 0x1: (32, 32), - 0x2: (48, 48), - 0x3: (66, 66), - 0x5: (128, 128), - 0x7: (256, 256), - 0x8: (32, 32), - } - - FLAGS_CA_MASK = 0x80 - - def __init__( - self, - src_key: Optional[PublicKey] = None, - signing_algorithm: str = "rsa", - hash_type: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, - key_size: int = 0, - srk_flags: int = 0, - crypto_param1: bytes = b"", - crypto_param2: bytes = b"", - ): - """Class object initializer. - - :param src_key: Optional source public key used to create the SRKRecord - :param signing_algorithm: signing algorithm type. - :param hash_type: hash algorithm type. - :param key_size: key (curve) size. - :param srk_flags: flags. - :param crypto_param1: RSA modulus (big endian) or ECDSA X (big endian) - :param crypto_param2: RSA exponent (big endian) or ECDSA Y (big endian) - """ - super().__init__( - tag=self.TAG, length=-1, version=self.VERSION_ALGORITHMS[signing_algorithm] - ) - self.signing_algorithm = signing_algorithm - self.src_key = src_key - self.hash_algorithm = self.HASH_ALGORITHM[hash_type] - self.key_size = key_size - self.srk_flags = srk_flags - self.crypto_param1 = crypto_param1 - self.crypto_param2 = crypto_param2 - - def __eq__(self, other: object) -> bool: - if isinstance(other, SRKRecord): - if ( - super().__eq__(other) # pylint: disable=too-many-boolean-expressions - and self.hash_algorithm == other.hash_algorithm - and self.key_size == other.key_size - and self.srk_flags == other.srk_flags - and self.crypto_param1 == other.crypto_param1 - and self.crypto_param2 == other.crypto_param2 - ): - return True - - return False - - def __len__(self) -> int: - return super().__len__() + len(self.crypto_param1) + len(self.crypto_param2) - - def __repr__(self) -> str: - return f"AHAB SRK record, key: {self.get_key_name()}" - - def __str__(self) -> str: - return ( - "AHAB SRK Record:\n" - f" Key: {self.get_key_name()}\n" - f" SRK flags: {hex(self.srk_flags)}\n" - f" Param 1 value: {self.crypto_param1.hex()})\n" - f" Param 2 value: {self.crypto_param2.hex()})\n" - ) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() - + UINT8 # Hash Algorithm - + UINT8 # Key Size / Curve - + UINT8 # Not Used - + UINT8 # SRK Flags - + UINT16 # crypto_param2_len - + UINT16 # crypto_param1_len - ) - - def update_fields(self) -> None: - """Update all fields depended on input values.""" - self.length = len(self) - - def export(self) -> bytes: - """Export one SRK record, little big endian format. - - The crypto parameters (X/Y for ECDSA or modulus/exponent) are kept in - big endian form. - - :return: bytes representing container content. - """ - return ( - pack( - self.format(), - self.tag, - self.length, - self.version, - self.hash_algorithm, - self.key_size, - RESERVED, - self.srk_flags, - len(self.crypto_param1), - len(self.crypto_param2), - ) - + self.crypto_param1 - + self.crypto_param2 - ) - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - self.validate_header() - if self.hash_algorithm is None or not check_range(self.hash_algorithm, end=2): - raise SPSDKValueError( - f"SRK record: Invalid Hash algorithm: {self.hash_algorithm}" - ) - - if self.srk_flags is None or not check_range(self.srk_flags, end=0xFF): - raise SPSDKValueError(f"SRK record: Invalid Flags: {self.srk_flags}") - - if self.version == 0x21: # Signing algorithm RSA - if self.key_size not in self.RSA_KEY_TYPE.values(): - raise SPSDKValueError( - f"SRK record: Invalid Key size in match to RSA signing algorithm: {self.key_size}" - ) - elif self.version == 0x27: # Signing algorithm ECDSA - if self.key_size not in self.ECC_KEY_TYPE.values(): - raise SPSDKValueError( - f"SRK record: Invalid Key size in match to ECDSA signing algorithm: {self.key_size}" - ) - elif self.version == 0x28: # Signing algorithm SM2 - if self.key_size != self.SM2_KEY_TYPE: - raise SPSDKValueError( - f"SRK record: Invalid Key size in match to SM2 signing algorithm: {self.key_size}" - ) - else: - raise SPSDKValueError( - f"SRK record: Invalid Signing algorithm: {self.version}" - ) - - # Check lengths - - if ( - self.crypto_param1 is None - or len(self.crypto_param1) != self.KEY_SIZES[self.key_size][0] - ): - raise SPSDKValueError( - f"SRK record: Invalid Crypto parameter 1: 0x{self.crypto_param1.hex()}" - ) - - if ( - self.crypto_param2 is None - or len(self.crypto_param2) != self.KEY_SIZES[self.key_size][1] - ): - raise SPSDKValueError( - f"SRK record: Invalid Crypto parameter 2: 0x{self.crypto_param2.hex()}" - ) - - computed_length = ( - self.fixed_length() - + self.KEY_SIZES[self.key_size][0] - + self.KEY_SIZES[self.key_size][1] - ) - if self.length != len(self) or self.length != computed_length: - raise SPSDKValueError( - f"SRK record: Invalid Length: Length of SRK:{self.length}" - f", Computed Length of SRK:{computed_length}" - ) - - @staticmethod - def create_from_key(public_key: PublicKey, srk_flags: int = 0) -> "SRKRecord": - """Create instance from key data. - - :param public_key: Loaded public key. - :param srk_flags: SRK flags for key. - :raises SPSDKValueError: Unsupported keys size is detected. - """ - if isinstance(public_key, PublicKeyRsa): - par_n: int = public_key.public_numbers.n - par_e: int = public_key.public_numbers.e - key_size = SRKRecord.RSA_KEY_TYPE[public_key.key_size] - return SRKRecord( - src_key=public_key, - signing_algorithm="rsa", - hash_type=EnumHashAlgorithm.SHA256, - key_size=key_size, - srk_flags=srk_flags, - crypto_param1=par_n.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][0], - byteorder=Endianness.BIG.value, - ), - crypto_param2=par_e.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][1], - byteorder=Endianness.BIG.value, - ), - ) - - elif isinstance(public_key, PublicKeyEcc): - par_x: int = public_key.x - par_y: int = public_key.y - key_size = SRKRecord.ECC_KEY_TYPE[public_key.curve] - - if not public_key.key_size in [256, 384, 521]: - raise SPSDKValueError( - f"Unsupported ECC key for AHAB container: {public_key.key_size}" - ) - hash_type = { - 256: EnumHashAlgorithm.SHA256, - 384: EnumHashAlgorithm.SHA384, - 521: EnumHashAlgorithm.SHA512, - }[public_key.key_size] - - return SRKRecord( - signing_algorithm="ecdsa", - hash_type=hash_type, - key_size=key_size, - srk_flags=srk_flags, - crypto_param1=par_x.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][0], - byteorder=Endianness.BIG.value, - ), - crypto_param2=par_y.to_bytes( - length=SRKRecord.KEY_SIZES[key_size][1], - byteorder=Endianness.BIG.value, - ), - ) - - assert isinstance( - public_key, PublicKeySM2 - ), "Unsupported public key for SRK record" - param1: bytes = value_to_bytes( - "0x" + public_key.public_numbers[:64], byte_cnt=32 - ) - param2: bytes = value_to_bytes( - "0x" + public_key.public_numbers[64:], byte_cnt=32 - ) - assert len(param1 + param2) == 64, "Invalid length of the SM2 key" - key_size = SRKRecord.SM2_KEY_TYPE - return SRKRecord( - src_key=public_key, - signing_algorithm="sm2", - hash_type=EnumHashAlgorithm.SM3, - key_size=key_size, - srk_flags=srk_flags, - crypto_param1=param1, - crypto_param2=param2, - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with SRK record block to parse. - :raises SPSDKLengthError: Invalid length of SRK record data block. - :return: SRK record recreated from the binary data. - """ - SRKRecord.check_container_head(data) - ( - _, # tag - container_length, - signing_algo, - hash_algo, - key_size_curve, - _, # reserved - srk_flags, - crypto_param1_len, - crypto_param2_len, - ) = unpack(SRKRecord.format(), data[: SRKRecord.fixed_length()]) - - # Although we know from the total length, that we have enough bytes, - # the crypto param lengths may be set improperly and we may get into trouble - # while parsing. So we need to check the lengths as well. - param_length = SRKRecord.fixed_length() + crypto_param1_len + crypto_param2_len - if container_length < param_length: - raise SPSDKLengthError( - "Parsing error of SRK Record data." - "SRK record lengths mismatch. Sum of lengths declared in container " - f"({param_length} (= {SRKRecord.fixed_length()} + {crypto_param1_len} + " - f"{crypto_param2_len})) doesn't match total length declared in container ({container_length})!" - ) - crypto_param1 = data[ - SRKRecord.fixed_length() : SRKRecord.fixed_length() + crypto_param1_len - ] - crypto_param2 = data[ - SRKRecord.fixed_length() - + crypto_param1_len : SRKRecord.fixed_length() - + crypto_param1_len - + crypto_param2_len - ] - - return cls( - signing_algorithm=get_key_by_val( - SRKRecord.VERSION_ALGORITHMS, signing_algo - ), - hash_type=get_key_by_val(SRKRecord.HASH_ALGORITHM, hash_algo), - key_size=key_size_curve, - srk_flags=srk_flags, - crypto_param1=crypto_param1, - crypto_param2=crypto_param2, - ) - - def get_key_name(self) -> str: - """Get text key name in SRK record. - - :return: Key name. - """ - if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "rsa": - return f"rsa{get_key_by_val(self.RSA_KEY_TYPE, self.key_size)}" - if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "ecdsa": - return get_key_by_val(self.ECC_KEY_TYPE, self.key_size) - if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2": - return "sm2" - return "Unknown Key name!" - - def get_public_key(self, encoding: SPSDKEncoding = SPSDKEncoding.PEM) -> bytes: - """Store the SRK public key as a file. - - :param encoding: Public key encoding style, default is PEM. - :raises SPSDKError: Unsupported public key - """ - par1 = int.from_bytes(self.crypto_param1, Endianness.BIG.value) - par2 = int.from_bytes(self.crypto_param2, Endianness.BIG.value) - key: Union[PublicKey, PublicKeyEcc, PublicKeyRsa, PublicKeySM2] - if get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "rsa": - # RSA Key to store - key = PublicKeyRsa.recreate(par1, par2) - elif get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "ecdsa": - # ECDSA Key to store - curve = get_key_by_val(self.ECC_KEY_TYPE, self.key_size) - key = PublicKeyEcc.recreate(par1, par2, curve=curve) - elif ( - get_key_by_val(self.VERSION_ALGORITHMS, self.version) == "sm2" - and IS_OSCCA_SUPPORTED - ): - encoding = SPSDKEncoding.DER - key = PublicKeySM2.recreate(self.crypto_param1 + self.crypto_param2) - - return key.export(encoding=encoding) - - -class SRKTable(HeaderContainerInversed): - """Class representing SRK (Super Root Key) table in the AHAB container as part of signature block. - - SRK Table:: - - +-----+---------------------------------------------------------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+---------------------------------------------------------------+ - |0x00 | Tag | Length of SRK Table | Version | - +-----+---------------------------------------------------------------+ - |0x04 | SRK Record 1 | - +-----+---------------------------------------------------------------+ - |... | SRK Record 2 | - +-----+---------------------------------------------------------------+ - |... | SRK Record 3 | - +-----+---------------------------------------------------------------+ - |... | SRK Record 4 | - +-----+---------------------------------------------------------------+ - - """ - - TAG = AHABTags.SRK_TABLE.tag - VERSION = 0x42 - SRK_RECORDS_CNT = 4 - - def __init__(self, srk_records: Optional[List[SRKRecord]] = None) -> None: - """Class object initializer. - - :param srk_records: list of SRKRecord objects. - """ - super().__init__(tag=self.TAG, length=-1, version=self.VERSION) - self._srk_records: List[SRKRecord] = srk_records or [] - self.length = len(self) - - def __repr__(self) -> str: - return f"AHAB SRK TABLE, keys count: {len(self._srk_records)}" - - def __str__(self) -> str: - return ( - "AHAB SRK table:\n" - f" Keys count: {len(self._srk_records)}\n" - f" Length: {self.length}\n" - f"SRK table HASH: {self.compute_srk_hash().hex()}" - ) - - def clear(self) -> None: - """Clear the SRK Table Object.""" - self._srk_records.clear() - self.length = -1 - - def add_record(self, public_key: PublicKey, srk_flags: int = 0) -> None: - """Add SRK table record. - - :param public_key: Loaded public key. - :param srk_flags: SRK flags for key. - """ - self._srk_records.append( - SRKRecord.create_from_key(public_key=public_key, srk_flags=srk_flags) - ) - self.length = len(self) - - def __eq__(self, other: object) -> bool: - """Compares for equality with other SRK Table objects. - - :param other: object to compare with. - :return: True on match, False otherwise. - """ - if isinstance(other, SRKTable): - if super().__eq__(other) and self._srk_records == other._srk_records: - return True - - return False - - def __len__(self) -> int: - records_len = 0 - for record in self._srk_records: - records_len += len(record) - return super().__len__() + records_len - - def update_fields(self) -> None: - """Update all fields depended on input values.""" - for rec in self._srk_records: - rec.update_fields() - self.length = len(self) - - def compute_srk_hash(self) -> bytes: - """Computes a SHA256 out of all SRK records. - - :return: SHA256 computed over SRK records. - """ - return get_hash(data=self.export(), algorithm=EnumHashAlgorithm.SHA256) - - def get_source_keys(self) -> List[PublicKey]: - """Return list of source public keys. - - Either from the src_key field or recreate them. - :return: List of public keys. - """ - ret = [] - for srk in self._srk_records: - if srk.src_key: - # return src key if available - ret.append(srk.src_key) - else: - # recreate the key - ret.append(PublicKey.parse(srk.get_public_key())) - return ret - - def export(self) -> bytes: - """Serializes container object into bytes in little endian. - - :return: bytes representing container content. - """ - data = pack(self.format(), self.tag, self.length, self.version) - - for srk_record in self._srk_records: - data += srk_record.export() - - return data - - def validate(self, data: Dict[str, Any]) -> None: - """Validate object data. - - :param data: Additional validation data. - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - self.validate_header() - if self._srk_records is None or len(self._srk_records) != self.SRK_RECORDS_CNT: - raise SPSDKValueError( - f"SRK table: Invalid SRK records: {self._srk_records}" - ) - - # Validate individual SRK records - for srk_rec in self._srk_records: - srk_rec.validate() - - # Check if all SRK records has same type - srk_records_info = [ - (x.version, x.hash_algorithm, x.key_size, x.length, x.srk_flags) - for x in self._srk_records - ] - - messages = [ - "Signing algorithm", - "Hash algorithm", - "Key Size", - "Length", - "Flags", - ] - for i in range(4): - if not all(srk_records_info[0][i] == x[i] for x in srk_records_info): - raise SPSDKValueError( - f"SRK table: SRK records haven't same {messages[i]}: {[x[i] for x in srk_records_info]}" - ) - - if "srkh_sha_supports" in data.keys(): - if ( - get_key_by_val( - SRKRecord.HASH_ALGORITHM, self._srk_records[0].hash_algorithm - ).label - not in data["srkh_sha_supports"] - ): - raise SPSDKValueError( - "SRK table: SRK records haven't supported hash algorithm:" - f" Used:{self._srk_records[0].hash_algorithm} is not member of" - f" {data['srkh_sha_supports']}" - ) - # Check container length - if self.length != len(self): - raise SPSDKValueError( - f"SRK table: Invalid Length of SRK table: {self.length} != {len(self)}" - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with SRK table block to parse. - :raises SPSDKLengthError: Invalid length of SRK table data block. - :return: Object recreated from the binary data. - """ - SRKTable.check_container_head(data) - srk_rec_offset = SRKTable.fixed_length() - _, container_length, _ = unpack(SRKTable.format(), data[:srk_rec_offset]) - if ((container_length - srk_rec_offset) % SRKTable.SRK_RECORDS_CNT) != 0: - raise SPSDKLengthError("SRK table: Invalid length of SRK records data.") - srk_rec_size = math.ceil( - (container_length - srk_rec_offset) / SRKTable.SRK_RECORDS_CNT - ) - - # try to parse records - srk_records: List[SRKRecord] = [] - for _ in range(SRKTable.SRK_RECORDS_CNT): - srk_record = SRKRecord.parse(data[srk_rec_offset:]) - srk_rec_offset += srk_rec_size - srk_records.append(srk_record) - - return cls(srk_records=srk_records) - - def create_config(self, index: int, data_path: str) -> Dict[str, Any]: - """Create configuration of the AHAB Image SRK Table. - - :param index: Container Index. - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - ret_cfg: Dict[str, Union[List, bool]] = {} - cfg_srks = [] - - ret_cfg["flag_ca"] = bool( - self._srk_records[0].srk_flags & SRKRecord.FLAGS_CA_MASK - ) - - for ix_srk, srk in enumerate(self._srk_records): - filename = ( - f"container{index}_srk_public_key{ix_srk}_{srk.get_key_name()}.PEM" - ) - write_file( - data=srk.get_public_key(), - path=os.path.join(data_path, filename), - mode="wb", - ) - cfg_srks.append(filename) - - ret_cfg["srk_array"] = cfg_srks - return ret_cfg - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "SRKTable": - """Converts the configuration option into an AHAB image object. - - "config" content of container configurations. - - :param config: array of AHAB containers configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: SRK Table object. - """ - srk_table = SRKTable() - flags = 0 - flag_ca = config.get("flag_ca", False) - if flag_ca: - flags |= SRKRecord.FLAGS_CA_MASK - srk_list = config.get("srk_array") - assert isinstance(srk_list, list) - for srk_key in srk_list: - assert isinstance(srk_key, str) - srk_key_path = find_file(srk_key, search_paths=search_paths) - srk_table.add_record(extract_public_key(srk_key_path), srk_flags=flags) - return srk_table - - -class ContainerSignature(HeaderContainer): - """Class representing the signature in AHAB container as part of the signature block. - - Signature:: - - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Tag | Length (MSB) | Length (LSB) | Version | - +-----+--------------+--------------+----------------+----------------+ - |0x04 | Reserved | - +-----+---------------------------------------------------------------+ - |0x08 | Signature Data | - +-----+---------------------------------------------------------------+ - - """ - - TAG = AHABTags.SIGNATURE.tag - VERSION = 0x00 - - def __init__( - self, - signature_data: Optional[bytes] = None, - signature_provider: Optional[SignatureProvider] = None, - ) -> None: - """Class object initializer. - - :param signature_data: signature. - :param signature_provider: Signature provider use to sign the image. - """ - super().__init__(tag=self.TAG, length=-1, version=self.VERSION) - self._signature_data = signature_data or b"" - self.signature_provider = signature_provider - self.length = len(self) - - def __eq__(self, other: object) -> bool: - if isinstance(other, ContainerSignature): - if super().__eq__(other) and self._signature_data == other._signature_data: - return True - - return False - - def __len__(self) -> int: - if ( - not self._signature_data or len(self._signature_data) == 0 - ) and self.signature_provider: - return super().__len__() + self.signature_provider.signature_length - - sign_data_len = len(self._signature_data) - if sign_data_len == 0: - return 0 - - return super().__len__() + sign_data_len - - def __repr__(self) -> str: - return "AHAB Container Signature" - - def __str__(self) -> str: - return ( - "AHAB Container Signature:\n" - f" Signature provider: {self.signature_provider.info() if self.signature_provider else 'Not available'}\n" - f" Signature: {self.signature_data.hex() if self.signature_data else 'Not available'}" - ) - - @property - def signature_data(self) -> bytes: - """Get the signature data. - - :return: signature data. - """ - return self._signature_data - - @signature_data.setter - def signature_data(self, value: bytes) -> None: - """Set the signature data. - - :param value: signature data. - """ - self._signature_data = value - self.length = len(self) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return super().format() + UINT32 # reserved - - def sign(self, data_to_sign: bytes) -> None: - """Sign the data_to_sign and store signature into class. - - :param data_to_sign: Data to be signed by store private key - :raises SPSDKError: Missing private key or raw signature data. - """ - if not self.signature_provider and len(self._signature_data) == 0: - raise SPSDKError( - "The Signature container doesn't have specified the private key to sign." - ) - - if self.signature_provider: - self._signature_data = self.signature_provider.get_signature(data_to_sign) - - def export(self) -> bytes: - """Export signature data that is part of Signature Block. - - :return: bytes representing container signature content. - """ - if len(self) == 0: - return b"" - - data = ( - pack( - self.format(), - self.version, - self.length, - self.tag, - RESERVED, - ) - + self._signature_data - ) - - return data - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - self.validate_header() - if self._signature_data is None or len(self._signature_data) < 20: - raise SPSDKValueError( - f"Signature: Invalid Signature data: 0x{self.signature_data.hex()}" - ) - if self.length != len(self): - raise SPSDKValueError( - f"Signature: Invalid Signature length: {self.length} != {len(self)}." - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with Container signature block to parse. - :return: Object recreated from the binary data. - """ - ContainerSignature.check_container_head(data) - fix_len = ContainerSignature.fixed_length() - - _, container_length, _, _ = unpack(ContainerSignature.format(), data[:fix_len]) - signature_data = data[fix_len:container_length] - - return cls(signature_data=signature_data) - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "ContainerSignature": - """Converts the configuration option into an AHAB image object. - - "config" content of container configurations. - - :param config: array of AHAB containers configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: Container signature object. - """ - signature_provider = get_signature_provider( - sp_cfg=config.get("signature_provider"), - local_file_key=config.get("signing_key"), - search_paths=search_paths, - ) - assert signature_provider - return ContainerSignature(signature_provider=signature_provider) - - -class Certificate(HeaderContainer): - """Class representing certificate in the AHAB container as part of the signature block. - - The Certificate comes in two forms - with and without UUID. - - Certificate format 1:: - - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Tag | Length (MSB) | Length (LSB) | Version | - +-----+--------------+--------------+----------------+----------------+ - |0x04 | Permissions | Perm (invert)| Signature offset | - +-----+--------------+--------------+---------------------------------+ - |0x08 | Public Key | - +-----+---------------------------------------------------------------+ - |... | Signature | - +-----+---------------------------------------------------------------+ - - Certificate format 2:: - - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Tag | Length (MSB) | Length (LSB) | Version | - +-----+--------------+--------------+----------------+----------------+ - |0x04 | Permissions | Perm (invert)| Signature offset | - +-----+--------------+--------------+---------------------------------+ - |0x08 | UUID | - +-----+---------------------------------------------------------------+ - |... | Public Key | - +-----+---------------------------------------------------------------+ - |... | Signature | - +-----+---------------------------------------------------------------+ - - """ - - TAG = [AHABTags.CERTIFICATE_UUID.tag, AHABTags.CERTIFICATE_NON_UUID.tag] # type: ignore - UUID_LEN = 16 - UUID_OFFSET = 0x08 - VERSION = 0x00 - PERM_NXP = { - "secure_enclave_debug": 0x02, - "hdmi_debug": 0x04, - "life_cycle": 0x10, - "hdcp_fuses": 0x20, - } - PERM_OEM = { - "container": 0x01, - "phbc_debug": 0x02, - "soc_debug_domain_1": 0x04, - "soc_debug_domain_2": 0x08, - "life_cycle": 0x10, - "monotonic_counter": 0x20, - } - PERM_SIZE = 8 - - def __init__( - self, - permissions: int = 0, - uuid: Optional[bytes] = None, - public_key: Optional[SRKRecord] = None, - signature_provider: Optional[SignatureProvider] = None, - ): - """Class object initializer. - - :param permissions: used to indicate what a certificate can be used for. - :param uuid: optional 128-bit unique identifier. - :param public_key: public Key. SRK record entry describing the key. - :param signature_provider: Signature provider for certificate. Signature is calculated over - all data from beginning of the certificate up to, but not including the signature. - """ - tag = ( - AHABTags.CERTIFICATE_UUID.tag if uuid else AHABTags.CERTIFICATE_NON_UUID.tag - ) - super().__init__(tag=tag, length=-1, version=self.VERSION) - self._permissions = permissions - self.signature_offset = -1 - self._uuid = uuid - self.public_key = public_key - self.signature = ContainerSignature( - signature_data=b"", signature_provider=signature_provider - ) - - def __eq__(self, other: object) -> bool: - if isinstance(other, Certificate): - if ( - super().__eq__(other) # pylint: disable=too-many-boolean-expressions - and self._permissions == other._permissions - and self.signature_offset == other.signature_offset - and self._uuid == other._uuid - and self.public_key == other.public_key - and self.signature == other.signature - ): - return True - - return False - - def __repr__(self) -> str: - return "AHAB Certificate" - - def __str__(self) -> str: - return ( - "AHAB Certificate:\n" - f" Permission: {hex(self._permissions)}\n" - f" UUID: {self._uuid.hex() if self._uuid else 'Not Available'}\n" - f" Public Key: {str(self.public_key) if self.public_key else 'Not available'}\n" - f" Signature: {str(self.signature) if self.signature else 'Not available'}" - ) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() # endianness, header: version, length, tag - + UINT16 # signature offset - + UINT8 # inverted permissions - + UINT8 # permissions - ) - - def __len__(self) -> int: - assert self.public_key - uuid_len = len(self._uuid) if self._uuid else 0 - return super().__len__() + uuid_len + len(self.public_key) + len(self.signature) - - @staticmethod - def create_permissions(permissions: List[str]) -> int: - """Create integer representation of permission field. - - :param permissions: List of string permissions. - :return: Integer representation of permissions. - """ - ret = 0 - permission_map = {} - permission_map.update(Certificate.PERM_NXP) - permission_map.update(Certificate.PERM_OEM) - for permission in permissions: - ret |= permission_map[permission] - - return ret - - @property - def permission_to_sign_container(self) -> bool: - """Certificate has permission to sign container.""" - return bool(self._permissions & self.PERM_OEM["container"]) - - def create_config_permissions(self, srk_set: str) -> List[str]: - """Create list of string representation of permission field. - - :param srk_set: SRK set to get proper string values. - :return: List of string representation of permissions. - """ - ret = [] - perm_maps = {"nxp": self.PERM_NXP, "oem": self.PERM_OEM} - perm_map = perm_maps.get(srk_set) - - for i in range(self.PERM_SIZE): - if self._permissions & (1 << i): - ret.append( - get_key_by_val(perm_map, 1 << i) - if perm_map and (1 << i) in perm_map.values() - else f"Unknown permission {hex(1< bytes: - """Returns binary data to be signed. - - The certificate block must be properly initialized, so the data are valid for - signing. There is signed whole certificate block without signature part. - - - :raises SPSDKValueError: if Signature Block or SRK Table is missing. - :return: bytes representing data to be signed. - """ - assert self.public_key - cert_data_to_sign = ( - pack( - self.format(), - self.version, - self.length, - self.tag, - self.signature_offset, - ~self._permissions & 0xFF, - self._permissions, - ) - + self.public_key.export() - ) - # if uuid is present, insert it into the cert data - if self._uuid: - cert_data_to_sign = ( - cert_data_to_sign[: self.UUID_OFFSET] - + self._uuid - + cert_data_to_sign[self.UUID_OFFSET :] - ) - - return cert_data_to_sign - - def update_fields(self) -> None: - """Update all fields depended on input values.""" - assert self.public_key - self.public_key.update_fields() - self.tag = ( - AHABTags.CERTIFICATE_UUID.tag - if self._uuid - else AHABTags.CERTIFICATE_NON_UUID.tag - ) - self.signature_offset = ( - super().__len__() - + (len(self._uuid) if self._uuid else 0) - + len(self.public_key) - ) - self.length = len(self) - self.signature.sign(self.get_signature_data()) - - def export(self) -> bytes: - """Export container certificate object into bytes. - - :return: bytes representing container content. - """ - assert self.public_key - cert = ( - pack( - self.format(), - self.version, - self.length, - self.tag, - self.signature_offset, - ~self._permissions & 0xFF, - self._permissions, - ) - + self.public_key.export() - + self.signature.export() - ) - # if uuid is present, insert it into the cert data - if self._uuid: - cert = cert[: self.UUID_OFFSET] + self._uuid + cert[self.UUID_OFFSET :] - assert self.length == len(cert) - return cert - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - self.validate_header() - if self._permissions is None or not check_range(self._permissions, end=0xFF): - raise SPSDKValueError( - f"Certificate: Invalid Permission data: {self._permissions}" - ) - if self.public_key is None: - raise SPSDKValueError("Certificate: Missing public key.") - self.public_key.validate() - - if not self.signature: - raise SPSDKValueError("Signature must be provided") - - self.signature.validate() - - expected_signature_offset = ( - super().__len__() - + (len(self._uuid) if self._uuid else 0) - + len(self.public_key) - ) - if self.signature_offset != expected_signature_offset: - raise SPSDKValueError( - f"Certificate: Invalid signature offset. " - f"{self.signature_offset} != {expected_signature_offset}" - ) - if self._uuid and len(self._uuid) != self.UUID_LEN: - raise SPSDKValueError( - f"Certificate: Invalid UUID size. {len(self._uuid)} != {self.UUID_LEN}" - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with Certificate block to parse. - :raises SPSDKValueError: Certificate permissions are invalid. - :return: Object recreated from the binary data. - """ - Certificate.check_container_head(data) - certificate_data_offset = Certificate.fixed_length() - image_format = Certificate.format() - ( - _, # version, - container_length, - tag, - signature_offset, - inverted_permissions, - permissions, - ) = unpack(image_format, data[:certificate_data_offset]) - - if inverted_permissions != ~permissions & 0xFF: - raise SPSDKValueError("Certificate parser: Invalid permissions record.") - - uuid = None - - if AHABTags.CERTIFICATE_UUID == tag: - uuid = data[ - certificate_data_offset : certificate_data_offset + Certificate.UUID_LEN - ] - certificate_data_offset += Certificate.UUID_LEN - - public_key = SRKRecord.parse(data[certificate_data_offset:]) - - signature = ContainerSignature.parse(data[signature_offset:container_length]) - - cert = cls( - permissions=permissions, - uuid=uuid, - public_key=public_key, - ) - cert.signature = signature - return cert - - def create_config( - self, index: int, data_path: str, srk_set: str = "oem" - ) -> Dict[str, Any]: - """Create configuration of the AHAB Image Certificate. - - :param index: Container Index. - :param data_path: Path to store the data files of configuration. - :param srk_set: SRK set to know how to create certificate permissions. - :return: Configuration dictionary. - """ - ret_cfg: Dict[str, Any] = {} - assert self.public_key - ret_cfg["permissions"] = self.create_config_permissions(srk_set) - if self._uuid: - ret_cfg["uuid"] = "0x" + self._uuid.hex() - filename = f"container{index}_certificate_public_key_{self.public_key.get_key_name()}.PEM" - write_file( - data=self.public_key.get_public_key(), - path=os.path.join(data_path, filename), - mode="wb", - ) - ret_cfg["public_key"] = filename - ret_cfg["signature_provider"] = "N/A" - - return ret_cfg - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Certificate": - """Converts the configuration option into an AHAB image signature block certificate object. - - "config" content of container configurations. - - :param config: array of AHAB containers configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: Certificate object. - """ - cert_permissions_list = config.get("permissions", []) - cert_uuid_raw = config.get("uuid") - cert_uuid = value_to_bytes(cert_uuid_raw) if cert_uuid_raw else None - cert_public_key_path = config.get("public_key") - assert isinstance(cert_public_key_path, str) - cert_public_key_path = find_file( - cert_public_key_path, search_paths=search_paths - ) - cert_public_key = extract_public_key(cert_public_key_path) - cert_srk_rec = SRKRecord.create_from_key(cert_public_key) - cert_signature_provider = get_signature_provider( - config.get("signature_provider"), - config.get("signing_key"), - search_paths=search_paths, - ) - return Certificate( - permissions=Certificate.create_permissions(cert_permissions_list), - uuid=cert_uuid, - public_key=cert_srk_rec, - signature_provider=cert_signature_provider, - ) - - @staticmethod - def get_validation_schemas() -> List[Dict[str, Any]]: - """Get list of validation schemas. - - :return: Validation list of schemas. - """ - return [ - DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)[ - "ahab_certificate" - ] - ] - - @staticmethod - def generate_config_template() -> str: - """Generate AHAB configuration template. - - :return: Certificate configuration templates. - """ - yaml_data = CommentedConfig( - "Advanced High-Assurance Boot Certificate Configuration template.", - Certificate.get_validation_schemas(), - ).get_template() - - return yaml_data - - -class Blob(HeaderContainer): - """The Blob object used in Signature Container. - - Blob (DEK) content:: - - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Tag | Length (MSB) | Length (LSB) | Version | - +-----+--------------+--------------+----------------+----------------+ - |0x04 | Mode | Algorithm | Size | Flags | - +-----+--------------+--------------+----------------+----------------+ - |0x08 | Wrapped Key | - +-----+--------------+--------------+----------------+----------------+ - - """ - - TAG = AHABTags.BLOB.tag - VERSION = 0x00 - FLAGS = 0x80 # KEK key flag - SUPPORTED_KEY_SIZES = [128, 192, 256] - - def __init__( - self, - flags: int = 0x80, - size: int = 0, - algorithm: KeyBlobEncryptionAlgorithm = KeyBlobEncryptionAlgorithm.AES_CBC, - mode: int = 0, - dek: Optional[bytes] = None, - dek_keyblob: Optional[bytes] = None, - key_identifier: int = 0, - ) -> None: - """Class object initializer. - - :param flags: Keyblob flags - :param size: key size [128,192,256] - :param dek: DEK key - :param mode: DEK BLOB mode - :param algorithm: Encryption algorithm - :param dek_keyblob: DEK keyblob - :param key_identifier: Key identifier. Must be same as it was used for keyblob generation - """ - super().__init__(tag=self.TAG, length=56 + size // 8, version=self.VERSION) - self.mode = mode - self.algorithm = algorithm - self._size = size - self.flags = flags - self.dek = dek - self.dek_keyblob = dek_keyblob or b"" - self.key_identifier = key_identifier - - def __eq__(self, other: object) -> bool: - if isinstance(other, Blob): - if ( - super().__eq__(other) # pylint: disable=too-many-boolean-expressions - and self.mode == other.mode - and self.algorithm == other.algorithm - and self._size == other._size - and self.flags == other.flags - and self.dek_keyblob == other.dek_keyblob - and self.key_identifier == other.key_identifier - ): - return True - - return False - - def __repr__(self) -> str: - return "AHAB Blob" - - def __str__(self) -> str: - return ( - "AHAB Blob:\n" - f" Mode: {self.mode}\n" - f" Algorithm: {self.algorithm.label}\n" - f" Key Size: {self._size}\n" - f" Flags: {self.flags}\n" - f" Key identifier: {hex(self.key_identifier)}\n" - f" DEK keyblob: {self.dek_keyblob.hex() if self.dek_keyblob else 'N/A'}" - ) - - @staticmethod - def compute_keyblob_size(key_size: int) -> int: - """Compute Keyblob size. - - :param key_size: Input AES key size in bits - :return: Keyblob size in bytes. - """ - return (key_size // 8) + 48 - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() # endianness, header: tag, length, version - + UINT8 # mode - + UINT8 # algorithm - + UINT8 # size - + UINT8 # flags - ) - - def __len__(self) -> int: - # return super()._total_length() + len(self.dek_keyblob) - return self.length - - def export(self) -> bytes: - """Export Signature Block Blob. - - :return: bytes representing Signature Block Blob. - """ - blob = ( - pack( - self.format(), - self.version, - self.length, - self.tag, - self.flags, - self._size // 8, - self.algorithm.tag, - self.mode, - ) - + self.dek_keyblob - ) - - return blob - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of AHAB Blob - """ - self.validate_header() - - if self._size not in self.SUPPORTED_KEY_SIZES: - raise SPSDKValueError("AHAB Blob: Invalid key size.") - if self.mode is None: - raise SPSDKValueError("AHAB Blob: Invalid mode.") - if self.algorithm is None: - raise SPSDKValueError("AHAB Blob: Invalid algorithm.") - if self.dek and len(self.dek) != self._size // 8: - raise SPSDKValueError("AHAB Blob: Invalid DEK key size.") - if self.dek_keyblob is None or len( - self.dek_keyblob - ) != self.compute_keyblob_size(self._size): - raise SPSDKValueError("AHAB Blob: Invalid Wrapped key.") - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with Blob block to parse. - :return: Object recreated from the binary data. - """ - Blob.check_container_head(data) - ( - _, # version - container_length, - _, # tag - flags, - size, - algorithm, # algorithm - mode, # mode - ) = unpack(Blob.format(), data[: Blob.fixed_length()]) - - dek_keyblob = data[Blob.fixed_length() : container_length] - - return cls( - size=size * 8, - flags=flags, - dek_keyblob=dek_keyblob, - mode=mode, - algorithm=KeyBlobEncryptionAlgorithm.from_tag(algorithm), - ) - - def create_config(self, index: int, data_path: str) -> Dict[str, Any]: - """Create configuration of the AHAB Image Blob. - - :param index: Container Index. - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - ret_cfg: Dict[str, Any] = {} - assert self.dek_keyblob - filename = f"container{index}_dek_keyblob.bin" - write_file(self.export(), os.path.join(data_path, filename), "wb") - ret_cfg["dek_key_size"] = self._size - ret_cfg["dek_key"] = "N/A" - ret_cfg["dek_keyblob"] = filename - ret_cfg["key_identifier"] = self.key_identifier - - return ret_cfg - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Blob": - """Converts the configuration option into an AHAB image signature block blob object. - - "config" content of container configurations. - - :param config: Blob configuration - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKValueError: Invalid configuration - Invalid DEK KeyBlob - :return: Blob object. - """ - dek_size = value_to_int(config.get("dek_key_size", 128)) - dek_input = config.get("dek_key") - dek_keyblob_input = config.get("dek_keyblob") - key_identifier = config.get("key_identifier", 0) - assert dek_input, "Missing DEK value" - assert dek_keyblob_input, "Missing DEK KEYBLOB value" - - dek = load_hex_string(dek_input, dek_size // 8, search_paths) - dek_keyblob_value = load_hex_string( - dek_keyblob_input, Blob.compute_keyblob_size(dek_size) + 8, search_paths - ) - if not dek_keyblob_value: - raise SPSDKValueError("Invalid DEK KeyBlob.") - - keyblob = Blob.parse(dek_keyblob_value) - keyblob.dek = dek - keyblob.key_identifier = key_identifier - return keyblob - - def encrypt_data(self, iv: bytes, data: bytes) -> bytes: - """Encrypt data. - - :param iv: Initial vector 128 bits length - :param data: Data to encrypt - :raises SPSDKError: Missing DEK, unsupported algorithm - :return: Encrypted data - """ - if not self.dek: - raise SPSDKError("The AHAB keyblob hasn't defined DEK to encrypt data") - - encryption_methods = { - KeyBlobEncryptionAlgorithm.AES_CBC: aes_cbc_encrypt, - KeyBlobEncryptionAlgorithm.SM4_CBC: sm4_cbc_encrypt, - } - - if not encryption_methods.get(self.algorithm): - raise SPSDKError(f"Unsupported encryption algorithm: {self.algorithm}") - return encryption_methods[self.algorithm](self.dek, data, iv) - - def decrypt_data(self, iv: bytes, encrypted_data: bytes) -> bytes: - """Encrypt data. - - :param iv: Initial vector 128 bits length - :param encrypted_data: Data to decrypt - :raises SPSDKError: Missing DEK, unsupported algorithm - :return: Plain data - """ - if not self.dek: - raise SPSDKError("The AHAB keyblob hasn't defined DEK to encrypt data") - - decryption_methods = { - KeyBlobEncryptionAlgorithm.AES_CBC: aes_cbc_decrypt, - KeyBlobEncryptionAlgorithm.SM4_CBC: sm4_cbc_decrypt, - } - - if not decryption_methods.get(self.algorithm): - raise SPSDKError(f"Unsupported encryption algorithm: {self.algorithm}") - return decryption_methods[self.algorithm](self.dek, encrypted_data, iv) - - -class SignatureBlock(HeaderContainer): - """Class representing signature block in the AHAB container. - - Signature Block:: - - +---------------+----------------+----------------+----------------+-----+ - | Byte 3 | Byte 2 | Byte 1 | Byte 0 | Fix | - |---------------+----------------+----------------+----------------+ len | - | Tag | Length | Version | | - |---------------+---------------------------------+----------------+ | - | SRK Table Offset | Certificate Offset | | - |--------------------------------+---------------------------------+ | - | Blob Offset | Signature Offset | | - |--------------------------------+---------------------------------+ | - | Key identifier in case that Blob is present | | - +------------------------------------------------------------------+-----+ Starting offset - | SRK Table | | - +------------------------------------------------------------------+-----+ Padding length - | 64 bit alignment | | - +------------------------------------------------------------------+-----+ Starting offset - | Signature | | - +------------------------------------------------------------------+-----+ Padding length - | 64 bit alignment | | - +------------------------------------------------------------------+-----+ Starting offset - | Certificate | | - +------------------------------------------------------------------+-----+ Padding length - | 64 bit alignment | | - +------------------------------------------------------------------+-----+ Starting offset - | Blob | | - +------------------------------------------------------------------+-----+ - - """ - - TAG = AHABTags.SIGNATURE_BLOCK.tag - VERSION = 0x00 - - def __init__( - self, - srk_table: Optional["SRKTable"] = None, - container_signature: Optional["ContainerSignature"] = None, - certificate: Optional["Certificate"] = None, - blob: Optional["Blob"] = None, - ): - """Class object initializer. - - :param srk_table: SRK table. - :param container_signature: container signature. - :param certificate: container certificate. - :param blob: container blob. - """ - super().__init__(tag=self.TAG, length=-1, version=self.VERSION) - self._srk_table_offset = 0 - self._certificate_offset = 0 - self._blob_offset = 0 - self.signature_offset = 0 - self.srk_table = srk_table - self.signature = container_signature - self.certificate = certificate - self.blob = blob - - def __eq__(self, other: object) -> bool: - """Compares for equality with other Signature Block objects. - - :param other: object to compare with. - :return: True on match, False otherwise. - """ - if isinstance(other, SignatureBlock): - if ( - super().__eq__(other) # pylint: disable=too-many-boolean-expressions - and self._srk_table_offset == other._srk_table_offset - and self._certificate_offset == other._certificate_offset - and self._blob_offset == other._blob_offset - and self.signature_offset == other.signature_offset - and self.srk_table == other.srk_table - and self.signature == other.signature - and self.certificate == other.certificate - and self.blob == other.blob - ): - return True - - return False - - def __len__(self) -> int: - self.update_fields() - return self.length - - def __repr__(self) -> str: - return "AHAB Signature Block" - - def __str__(self) -> str: - return ( - "AHAB Signature Block:\n" - f" SRK Table: {bool(self.srk_table)}\n" - f" Certificate: {bool(self.certificate)}\n" - f" Signature: {bool(self.signature)}\n" - f" Blob: {bool(self.blob)}" - ) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() - + UINT16 # certificate offset - + UINT16 # SRK table offset - + UINT16 # signature offset - + UINT16 # blob offset - + UINT32 # key_identifier if blob is used - ) - - def update_fields(self) -> None: - """Update all fields depended on input values.""" - # 1: Update SRK Table - # Nothing to do with SRK Table - last_offset = 0 - last_block_size = align(calcsize(self.format()), CONTAINER_ALIGNMENT) - if self.srk_table: - self.srk_table.update_fields() - last_offset = self._srk_table_offset = last_offset + last_block_size - last_block_size = align(len(self.srk_table), CONTAINER_ALIGNMENT) - else: - self._srk_table_offset = 0 - - # 2: Update Signature (at least length) - # Nothing to do with Signature - in this time , it MUST be ready - if self.signature: - last_offset = self.signature_offset = last_offset + last_block_size - last_block_size = align(len(self.signature), CONTAINER_ALIGNMENT) - else: - self.signature_offset = 0 - # 3: Optionally update Certificate - if self.certificate: - self.certificate.update_fields() - last_offset = self._certificate_offset = last_offset + last_block_size - last_block_size = align(len(self.certificate), CONTAINER_ALIGNMENT) - else: - self._certificate_offset = 0 - # 4: Optionally update Blob - if self.blob: - last_offset = self._blob_offset = last_offset + last_block_size - last_block_size = align(len(self.blob), CONTAINER_ALIGNMENT) - else: - self._blob_offset = 0 - - # 5: Update length of Signature block - self.length = last_offset + last_block_size - - def export(self) -> bytes: - """Export Signature block. - - :raises SPSDKLengthError: if exported data length doesn't match container length. - :return: bytes signature block content. - """ - extended_header = pack( - self.format(), - self.version, - self.length, - self.tag, - self._certificate_offset, - self._srk_table_offset, - self.signature_offset, - self._blob_offset, - self.blob.key_identifier if self.blob else RESERVED, - ) - - signature_block = bytearray(len(self)) - signature_block[0 : self.fixed_length()] = extended_header - if self.srk_table: - signature_block[ - self._srk_table_offset : self._srk_table_offset + len(self.srk_table) - ] = self.srk_table.export() - if self.signature: - signature_block[ - self.signature_offset : self.signature_offset + len(self.signature) - ] = self.signature.export() - if self.certificate: - signature_block[ - self._certificate_offset : self._certificate_offset - + len(self.certificate) - ] = self.certificate.export() - if self.blob: - signature_block[ - self._blob_offset : self._blob_offset + len(self.blob) - ] = self.blob.export() - - return signature_block - - def validate(self, data: Dict[str, Any]) -> None: - """Validate object data. - - :param data: Additional validation data. - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - - def check_offset(name: str, min_offset: int, offset: int) -> None: - if offset < min_offset: - raise SPSDKValueError( - f"Signature Block: Invalid {name} offset: {offset} < minimal offset {min_offset}" - ) - if offset != align(offset, CONTAINER_ALIGNMENT): - raise SPSDKValueError( - f"Signature Block: Invalid {name} offset alignment: {offset} is not aligned to 64 bits!" - ) - - self.validate_header() - if self.length != len(self): - raise SPSDKValueError( - f"Signature Block: Invalid block length: {self.length} != {len(self)}" - ) - if bool(self._srk_table_offset) != bool(self.srk_table): - raise SPSDKValueError( - "Signature Block: Invalid setting of SRK table offset." - ) - if bool(self.signature_offset) != bool(self.signature): - raise SPSDKValueError( - "Signature Block: Invalid setting of Signature offset." - ) - if bool(self._certificate_offset) != bool(self.certificate): - raise SPSDKValueError( - "Signature Block: Invalid setting of Certificate offset." - ) - if bool(self._blob_offset) != bool(self.blob): - raise SPSDKValueError("Signature Block: Invalid setting of Blob offset.") - - min_offset = self.fixed_length() - if self.srk_table: - self.srk_table.validate(data) - check_offset("SRK table", min_offset, self._srk_table_offset) - min_offset = self._srk_table_offset + len(self.srk_table) - if self.signature: - self.signature.validate() - check_offset("Signature", min_offset, self.signature_offset) - min_offset = self.signature_offset + len(self.signature) - if self.certificate: - self.certificate.validate() - check_offset("Certificate", min_offset, self._certificate_offset) - min_offset = self._certificate_offset + len(self.certificate) - if self.blob: - self.blob.validate() - check_offset("Blob", min_offset, self._blob_offset) - min_offset = self._blob_offset + len(self.blob) - - if "flag_used_srk_id" in data.keys() and self.signature and self.srk_table: - public_keys = self.srk_table.get_source_keys() - if ( - self.signature.signature_provider - and self.certificate - and not self.certificate.permission_to_sign_container - ): - # Container is signed by SRK key. Get the matching key and verify that the private key - # belongs to the public key in SRK - srk_pair_id = get_matching_key_id( - public_keys, self.signature.signature_provider - ) - if srk_pair_id != data["flag_used_srk_id"]: - raise SPSDKValueError( - f"Signature Block: Configured SRK ID ({data['flag_used_srk_id']})" - f" doesn't match detected SRK ID for signing key ({srk_pair_id})." - ) - elif self.certificate and self.certificate.permission_to_sign_container: - # In this case the certificate is signed by the key with given SRK ID - if not public_keys[data["flag_used_srk_id"]].verify_signature( - self.certificate.signature.signature_data, - self.certificate.get_signature_data(), - ): - raise SPSDKValueError( - f"Certificate signature cannot be verified with the key with SRK ID {data['flag_used_srk_id']} " - ) - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary chunk to the container object. - - :param data: Binary data with Signature block to parse. - :return: Object recreated from the binary data. - """ - SignatureBlock.check_container_head(data) - ( - _, # version - _, # container_length - _, # tag - certificate_offset, - srk_table_offset, - signature_offset, - blob_offset, - key_identifier, - ) = unpack(SignatureBlock.format(), data[: SignatureBlock.fixed_length()]) - - signature_block = cls() - signature_block.srk_table = ( - SRKTable.parse(data[srk_table_offset:]) if srk_table_offset else None - ) - signature_block.certificate = ( - Certificate.parse(data[certificate_offset:]) if certificate_offset else None - ) - signature_block.signature = ( - ContainerSignature.parse(data[signature_offset:]) - if signature_offset - else None - ) - try: - signature_block.blob = ( - Blob.parse(data[blob_offset:]) if blob_offset else None - ) - if signature_block.blob: - signature_block.blob.key_identifier = key_identifier - except SPSDKParsingError as exc: - logger.warning( - "AHAB Blob parsing error. In case that no encrypted images" - " are presented in container, it should not be an big issue." - f"\n{str(exc)}" - ) - signature_block.blob = None - - return signature_block - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "SignatureBlock": - """Converts the configuration option into an AHAB Signature block object. - - "config" content of container configurations. - - :param config: array of AHAB signature block configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: AHAB Signature block object. - """ - signature_block = SignatureBlock() - # SRK Table - srk_table_cfg = config.get("srk_table") - signature_block.srk_table = ( - SRKTable.load_from_config(srk_table_cfg, search_paths) - if srk_table_cfg - else None - ) - - # Container Signature - srk_set = config.get("srk_set", "none") - signature_block.signature = ( - ContainerSignature.load_from_config(config, search_paths) - if srk_set != "none" - else None - ) - - # Certificate Block - signature_block.certificate = None - certificate_cfg = config.get("certificate") - - if certificate_cfg: - try: - cert_cfg = load_configuration(certificate_cfg) - check_config( - cert_cfg, - Certificate.get_validation_schemas(), - search_paths=search_paths, - ) - signature_block.certificate = Certificate.load_from_config(cert_cfg) - except SPSDKError: - # this could be pre-exported binary certificate :-) - signature_block.certificate = Certificate.parse( - load_binary(certificate_cfg, search_paths) - ) - - # DEK blob - blob_cfg = config.get("blob") - signature_block.blob = ( - Blob.load_from_config(blob_cfg, search_paths) if blob_cfg else None - ) - - return signature_block - - -class AHABContainerBase(HeaderContainer): - """Class representing AHAB container base class (common for Signed messages and AHAB Image). - - Container header:: - - +---------------+----------------+----------------+----------------+ - | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +---------------+----------------+----------------+----------------+ - | Tag | Length | Version | - +---------------+---------------------------------+----------------+ - | Flags | - +---------------+----------------+---------------------------------+ - | # of images | Fuse version | SW version | - +---------------+----------------+---------------------------------+ - | Reserved | Signature Block Offset | - +--------------------------------+---------------------------------+ - | Payload (Signed Message or Image Array) | - +------------------------------------------------------------------+ - | Signature block | - +------------------------------------------------------------------+ - - """ - - TAG = 0x00 # Need to be updated by child class - VERSION = 0x00 - FLAGS_SRK_SET_OFFSET = 0 - FLAGS_SRK_SET_SIZE = 2 - FLAGS_SRK_SET_VAL = {"none": 0, "nxp": 1, "oem": 2} - FLAGS_USED_SRK_ID_OFFSET = 4 - FLAGS_USED_SRK_ID_SIZE = 2 - FLAGS_SRK_REVOKE_MASK_OFFSET = 8 - FLAGS_SRK_REVOKE_MASK_SIZE = 4 - - def __init__( - self, - flags: int = 0, - fuse_version: int = 0, - sw_version: int = 0, - signature_block: Optional["SignatureBlock"] = None, - ): - """Class object initializer. - - :param flags: flags. - :param fuse_version: value must be equal to or greater than the version - stored in the fuses to allow loading this container. - :param sw_version: used by PHBC (Privileged Host Boot Companion) to select - between multiple images with same fuse version field. - :param signature_block: signature block. - """ - super().__init__(tag=self.TAG, length=-1, version=self.VERSION) - self.flags = flags - self.fuse_version = fuse_version - self.sw_version = sw_version - self.signature_block = signature_block or SignatureBlock() - self.search_paths: List[str] = [] - self.lock = False - - def __eq__(self, other: object) -> bool: - if isinstance(other, AHABContainerBase): - if ( - super().__eq__(other) - and self.flags == other.flags - and self.fuse_version == other.fuse_version - and self.sw_version == other.sw_version - ): - return True - - return False - - def set_flags( - self, srk_set: str = "none", used_srk_id: int = 0, srk_revoke_mask: int = 0 - ) -> None: - """Set the flags value. - - :param srk_set: Super Root Key (SRK) set, defaults to "none" - :param used_srk_id: Which key from SRK set is being used, defaults to 0 - :param srk_revoke_mask: SRK revoke mask, defaults to 0 - """ - flags = self.FLAGS_SRK_SET_VAL[srk_set.lower()] - flags |= used_srk_id << 4 - flags |= srk_revoke_mask << 8 - self.flags = flags - - @property - def flag_srk_set(self) -> str: - """SRK set flag in string representation. - - :return: Name of SRK Set flag. - """ - srk_set = (self.flags >> self.FLAGS_SRK_SET_OFFSET) & ( - (1 << self.FLAGS_SRK_SET_SIZE) - 1 - ) - return get_key_by_val(self.FLAGS_SRK_SET_VAL, srk_set) - - @property - def flag_used_srk_id(self) -> int: - """Used SRK ID flag. - - :return: Index of Used SRK ID. - """ - return (self.flags >> self.FLAGS_USED_SRK_ID_OFFSET) & ( - (1 << self.FLAGS_USED_SRK_ID_SIZE) - 1 - ) - - @property - def flag_srk_revoke_mask(self) -> str: - """SRK Revoke mask flag. - - :return: SRK revoke mask in HEX. - """ - srk_revoke_mask = (self.flags >> self.FLAGS_SRK_REVOKE_MASK_OFFSET) & ( - (1 << self.FLAGS_SRK_REVOKE_MASK_SIZE) - 1 - ) - return hex(srk_revoke_mask) - - @property - def _signature_block_offset(self) -> int: - """Returns current signature block offset. - - :return: Offset in bytes of Signature block. - """ - # Constant size of Container header + Image array Entry table - return align( - super().__len__(), - CONTAINER_ALIGNMENT, - ) - - @property - def image_array_len(self) -> int: - """Get image array length if available. - - :return: Length of image array. - """ - return 0 - - def __len__(self) -> int: - """Get total length of AHAB container. - - :return: Size in bytes of AHAB Container. - """ - # If there are no images just return length of header - return self.header_length() - - def header_length(self) -> int: - """Length of AHAB Container header. - - :return: Length in bytes of AHAB Container header. - """ - return ( - super().__len__() - + len( # This returns the fixed length of the container header - self.signature_block - ) - ) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() - + UINT32 # Flags - + UINT16 # SW version - + UINT8 # Fuse version - + UINT8 # Number of Images - + UINT16 # Signature Block Offset - + UINT16 # Reserved - ) - - def update_fields(self) -> None: - """Updates all volatile information in whole container structure. - - :raises SPSDKError: When inconsistent image array length is detected. - """ - # Update the signature block to get overall size of it - self.signature_block.update_fields() - # Update the Container header length - self.length = self.header_length() - # # Sign the image header - if self.flag_srk_set != "none": - assert self.signature_block.signature - self.signature_block.signature.sign(self.get_signature_data()) - - def get_signature_data(self) -> bytes: - """Returns binary data to be signed. - - The container must be properly initialized, so the data are valid for - signing, i.e. the offsets, lengths etc. must be set prior invoking this - method, otherwise improper data will be signed. - - The whole container gets serialized first. Afterwards the binary data - is sliced so only data for signing get's returned. The signature data - length is evaluated based on offsets, namely the signature block offset, - the container signature offset and the container signature fixed data length. - - Signature data structure:: - - +---------------------------------------------------+----------------+ - | Container header | | - +---+---+-----------+---------+--------+------------+ Data | - | S | | tag | length | length | version | | - | i | +-----------+---------+--------+------------+ | - | g | | flags | to | - | n | +---------------------+---------------------+ | - | a | | srk table offset | certificate offset | | - | t | +---------------------+---------------------+ Sign | - | u | | blob offset | signature offset | | - | r | +---------------------+---------------------+ | - | e | | SRK Table | | - | +---+-----------+---------+--------+------------+----------------+ - | B | S | tag | length | length | version | Signature data | - | l | i +-----------+---------+--------+------------+ fixed length | - | o | g | Reserved | | - | c | n +-------------------------------------------+----------------+ - | k | a | Signature data | - | | t | | - | | u | | - | | r | | - | | e | | - +---+---+-------------------------------------------+ - - :raises SPSDKValueError: if Signature Block or SRK Table is missing. - :return: bytes representing data to be signed. - """ - if not self.signature_block.signature or not self.signature_block.srk_table: - raise SPSDKValueError( - "Can't retrieve data block to sign. Signature or SRK table is missing!" - ) - - signature_offset = ( - self._signature_block_offset + self.signature_block.signature_offset - ) - return self._export()[:signature_offset] - - def _export(self) -> bytes: - """Export container header into bytes. - - :return: bytes representing container header content including the signature block. - """ - return pack( - self.format(), - self.version, - self.length, - self.tag, - self.flags, - self.sw_version, - self.fuse_version, - self.image_array_len, - self._signature_block_offset, - RESERVED, # Reserved field - ) - - def validate(self, data: Dict[str, Any]) -> None: - """Validate object data. - - :param data: Additional validation data. - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - self.validate_header() - - if self.flags is None or not check_range(self.flags, end=(1 << 32) - 1): - raise SPSDKValueError(f"Container Header: Invalid flags: {hex(self.flags)}") - if self.sw_version is None or not check_range( - self.sw_version, end=(1 << 16) - 1 - ): - raise SPSDKValueError( - f"Container Header: Invalid SW version: {hex(self.sw_version)}" - ) - if self.fuse_version is None or not check_range( - self.fuse_version, end=(1 << 8) - 1 - ): - raise SPSDKValueError( - f"Container Header: Invalid Fuse version: {hex(self.fuse_version)}" - ) - self.signature_block.validate(data) - - @staticmethod - def _parse(binary: bytes) -> Tuple[int, int, int, int, int]: - """Parse input binary chunk to the container object. - - :param parent: AHABImage object. - :param binary: Binary data with Container block to parse. - :return: Object recreated from the binary data. - """ - AHABContainer.check_container_head(binary) - image_format = AHABContainer.format() - ( - _, # version - _, # container_length - _, # tag - flags, - sw_version, - fuse_version, - number_of_images, - signature_block_offset, - _, # reserved - ) = unpack(image_format, binary[: AHABContainer.fixed_length()]) - - return ( - flags, - sw_version, - fuse_version, - number_of_images, - signature_block_offset, - ) - - def _create_config(self, index: int, data_path: str) -> Dict[str, Any]: - """Create configuration of the AHAB Image. - - :param index: Container index. - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - cfg: Dict[str, Any] = {} - - cfg["srk_set"] = self.flag_srk_set - cfg["used_srk_id"] = self.flag_used_srk_id - cfg["srk_revoke_mask"] = self.flag_srk_revoke_mask - cfg["fuse_version"] = self.fuse_version - cfg["sw_version"] = self.sw_version - cfg["signing_key"] = "N/A" - - if self.signature_block.srk_table: - cfg["srk_table"] = self.signature_block.srk_table.create_config( - index, data_path - ) - - if self.signature_block.certificate: - cert_cfg = self.signature_block.certificate.create_config( - index, data_path, self.flag_srk_set - ) - write_file( - CommentedConfig( - "Parsed AHAB Certificate", Certificate.get_validation_schemas() - ).get_config(cert_cfg), - os.path.join(data_path, "certificate.yaml"), - ) - cfg["certificate"] = "certificate.yaml" - - if self.signature_block.blob: - cfg["blob"] = self.signature_block.blob.create_config(index, data_path) - - return cfg - - def load_from_config_generic(self, config: Dict[str, Any]) -> None: - """Converts the configuration option into an AHAB image object. - - "config" content of container configurations. - - :param config: array of AHAB containers configuration dictionaries. - """ - self.set_flags( - srk_set=config.get("srk_set", "none"), - used_srk_id=value_to_int(config.get("used_srk_id", 0)), - srk_revoke_mask=value_to_int(config.get("srk_revoke_mask", 0)), - ) - self.fuse_version = value_to_int(config.get("fuse_version", 0)) - self.sw_version = value_to_int(config.get("sw_version", 0)) - - self.signature_block = SignatureBlock.load_from_config( - config, search_paths=self.search_paths - ) - - -class AHABContainer(AHABContainerBase): - """Class representing AHAB container. - - Container header:: - - +---------------+----------------+----------------+----------------+ - | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +---------------+----------------+----------------+----------------+ - | Tag | Length | Version | - +---------------+---------------------------------+----------------+ - | Flags | - +---------------+----------------+---------------------------------+ - | # of images | Fuse version | SW version | - +---------------+----------------+---------------------------------+ - | Reserved | Signature Block Offset | - +----+---------------------------+---------------------------------+ - | I |image0: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | - + m |-------------------------------------------------------------+ - | g |image1: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | - + . |-------------------------------------------------------------+ - | A |... | - | r |... | - | r | | - + a |-------------------------------------------------------------+ - | y |imageN: Offset, Size, LoadAddr, EntryPoint, Flags, Hash, IV | - +----+-------------------------------------------------------------+ - | Signature block | - +------------------------------------------------------------------+ - | | - | | - | | - +------------------------------------------------------------------+ - | Data block_0 | - +------------------------------------------------------------------+ - | | - | | - +------------------------------------------------------------------+ - | Data block_n | - +------------------------------------------------------------------+ - - """ - - TAG = AHABTags.CONTAINER_HEADER.tag - - def __init__( - self, - parent: "AHABImage", - flags: int = 0, - fuse_version: int = 0, - sw_version: int = 0, - image_array: Optional[List["ImageArrayEntry"]] = None, - signature_block: Optional["SignatureBlock"] = None, - container_offset: int = 0, - ): - """Class object initializer. - - :parent: Parent AHABImage object. - :param flags: flags. - :param fuse_version: value must be equal to or greater than the version - stored in the fuses to allow loading this container. - :param sw_version: used by PHBC (Privileged Host Boot Companion) to select - between multiple images with same fuse version field. - :param image_array: array of image entries, must be `number of images` long. - :param signature_block: signature block. - """ - super().__init__( - flags=flags, - fuse_version=fuse_version, - sw_version=sw_version, - signature_block=signature_block, - ) - self.parent = parent - assert self.parent is not None - self.image_array = image_array or [] - self.container_offset = container_offset - self.search_paths: List[str] = [] - - def __eq__(self, other: object) -> bool: - if isinstance(other, AHABContainer): - if super().__eq__(other) and self.image_array == other.image_array: - return True - - return False - - def __repr__(self) -> str: - return f"AHAB Container at offset {hex(self.container_offset)} " - - def __str__(self) -> str: - return ( - "AHAB Container:\n" - f" Index: {'0' if self.container_offset == 0 else '1'}\n" - f" Flags: {hex(self.flags)}\n" - f" Fuse version: {hex(self.fuse_version)}\n" - f" SW version: {hex(self.sw_version)}\n" - f" Images count: {self.image_array_len}" - ) - - @property - def image_array_len(self) -> int: - """Get image array length if available. - - :return: Length of image array. - """ - return len(self.image_array) - - @property - def _signature_block_offset(self) -> int: - """Returns current signature block offset. - - :return: Offset in bytes of Signature block. - """ - # Constant size of Container header + Image array Entry table - return align( - super().fixed_length() - + len(self.image_array) * ImageArrayEntry.fixed_length(), - CONTAINER_ALIGNMENT, - ) - - def __len__(self) -> int: - """Get total length of AHAB container. - - :return: Size in bytes of AHAB Container. - """ - # Get image which has biggest offset - possible_sizes = [self.header_length()] - possible_sizes.extend( - [align(x.image_offset + x.image_size) for x in self.image_array] - ) - - return align(max(possible_sizes), CONTAINER_ALIGNMENT) - - def header_length(self) -> int: - """Length of AHAB Container header. - - :return: Length in bytes of AHAB Container header. - """ - return ( - super().fixed_length() # This returns the fixed length of the container header - # This returns the total length of all image array entries - + len(self.image_array) * ImageArrayEntry.fixed_length() - # This returns the length of signature block (including SRK table, - # blob etc. if present) - + len(self.signature_block) - ) - - def update_fields(self) -> None: - """Updates all volatile information in whole container structure. - - :raises SPSDKError: When inconsistent image array length is detected. - """ - # 1. Encrypt all images if applicable - for image_entry in self.image_array: - if ( - image_entry.flags_is_encrypted - and not image_entry.already_encrypted_image - and self.signature_block.blob - ): - image_entry.encrypted_image = self.signature_block.blob.encrypt_data( - image_entry.image_iv[16:], image_entry.plain_image - ) - image_entry.already_encrypted_image = True - - # 2. Update the signature block to get overall size of it - self.signature_block.update_fields() - # 3. Updates Image Entries - for image_entry in self.image_array: - image_entry.update_fields() - # 4. Update the Container header length - self.length = self.header_length() - # 5. Sign the image header - if self.flag_srk_set != "none": - assert self.signature_block.signature - self.signature_block.signature.sign(self.get_signature_data()) - - def decrypt_data(self) -> None: - """Decrypt all images if possible.""" - for i, image_entry in enumerate(self.image_array): - if image_entry.flags_is_encrypted: - if self.signature_block.blob is None: - raise SPSDKError("Cannot decrypt image without Blob!") - - decrypted_data = self.signature_block.blob.decrypt_data( - image_entry.image_iv[16:], image_entry.encrypted_image - ) - if image_entry.image_iv == get_hash( - decrypted_data, algorithm=EnumHashAlgorithm.SHA256 - ): - image_entry.plain_image = decrypted_data - logger.info( - f" Image{i} from AHAB container at offset {hex(self.container_offset)} has been decrypted." - ) - else: - logger.warning( - f" Image{i} from AHAB container at offset {hex(self.container_offset)} decryption failed." - ) - - def _export(self) -> bytes: - """Export container header into bytes. - - :return: bytes representing container header content including the signature block. - """ - return self.export() - - def export(self) -> bytes: - """Export container header into bytes. - - :return: bytes representing container header content including the signature block. - """ - container_header = bytearray(align(self.header_length(), CONTAINER_ALIGNMENT)) - container_header_only = super()._export() - - for image_array_entry in self.image_array: - container_header_only += image_array_entry.export() - - container_header[: self._signature_block_offset] = container_header_only - # Add Signature Block - container_header[ - self._signature_block_offset : self._signature_block_offset - + align(len(self.signature_block), CONTAINER_ALIGNMENT) - ] = self.signature_block.export() - - return container_header - - def validate(self, data: Dict[str, Any]) -> None: - """Validate object data. - - :param data: Additional validation data. - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - data["flag_used_srk_id"] = self.flag_used_srk_id - self.validate_header() - if self.length != self.header_length(): - raise SPSDKValueError( - f"Container 0x{self.container_offset:04X} " - f"Header: Invalid block length: {self.length} != {self.header_length()}" - ) - - super().validate(data) - - if self.image_array is None or len(self.image_array) == 0: - raise SPSDKValueError( - f"Container 0x{self.container_offset:04X} Header: Invalid Image Array: {self.image_array}" - ) - - for container, offset in zip( - self.parent.ahab_containers, self.parent.ahab_address_map - ): - if self == container: - if self.container_offset != offset: - raise SPSDKValueError( - f"AHAB Container 0x{self.container_offset:04X}: Invalid Container Offset." - ) - - if self.signature_block.srk_table and self.signature_block.signature: - # Get public key with the SRK ID - key = self.signature_block.srk_table.get_source_keys()[ - self.flag_used_srk_id - ] - if self.signature_block.certificate: - # Verify signature of certificate - if not key.verify_signature( - self.signature_block.certificate.signature.signature_data, - self.signature_block.certificate.get_signature_data(), - ): - raise SPSDKValueError( - f"AHAB Container 0x{self.container_offset:04X}: Certificate block signature " - f"cannot be verified with SRK ID {self.flag_used_srk_id}" - ) - - if ( - self.signature_block.certificate - and self.signature_block.certificate.permission_to_sign_container - ): - # Container is signed by certificate, get public key from certificate - assert ( - self.signature_block.certificate.public_key - ), "Certificate must contain public key" - key = PublicKey.parse( - self.signature_block.certificate.public_key.get_public_key() - ) - - if not key.verify_signature( - self.signature_block.signature.signature_data, self.get_signature_data() - ): - if ( - self.signature_block.certificate - and self.signature_block.certificate.permission_to_sign_container - ): - raise SPSDKValueError( - f"AHAB Container 0x{self.container_offset:04X}: " - "Signature cannot be verified with the public key from certificate" - ) - raise SPSDKValueError( - f"AHAB Container 0x{self.container_offset:04X}: " - f"Signature cannot be verified with SRK ID {self.flag_used_srk_id}" - ) - - for image in self.image_array: - image.validate() - - @classmethod - def parse(cls, data: bytes, parent: "AHABImage", container_id: int) -> Self: # type: ignore# type: ignore # pylint: disable=arguments-differ - """Parse input binary chunk to the container object. - - :param data: Binary data with Container block to parse. - :param parent: AHABImage object. - :param container_id: AHAB container ID. - :return: Object recreated from the binary data. - """ - if parent is None: - raise SPSDKValueError("Ahab Image must be specified.") - ( - flags, - sw_version, - fuse_version, - number_of_images, - signature_block_offset, - ) = AHABContainerBase._parse(data) - - parsed_container = cls( - parent=parent, - flags=flags, - fuse_version=fuse_version, - sw_version=sw_version, - container_offset=parent.ahab_address_map[container_id], - ) - parsed_container.signature_block = SignatureBlock.parse( - data[signature_block_offset:] - ) - - for i in range(number_of_images): - image_array_entry = ImageArrayEntry.parse( - data[ - AHABContainer.fixed_length() + i * ImageArrayEntry.fixed_length() : - ], - parsed_container, - ) - parsed_container.image_array.append(image_array_entry) - # Lock the parsed container to any updates of offsets - parsed_container.lock = True - return parsed_container - - def create_config(self, index: int, data_path: str) -> Dict[str, Any]: - """Create configuration of the AHAB Image. - - :param index: Container index. - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - ret_cfg = {} - cfg = self._create_config(index, data_path) - images_cfg = [] - - for img_ix, image in enumerate(self.image_array): - images_cfg.append(image.create_config(index, img_ix, data_path)) - cfg["images"] = images_cfg - - ret_cfg["container"] = cfg - return ret_cfg - - @staticmethod - def load_from_config( - parent: "AHABImage", config: Dict[str, Any], container_ix: int - ) -> "AHABContainer": - """Converts the configuration option into an AHAB image object. - - "config" content of container configurations. - - :param parent: AHABImage object. - :param config: array of AHAB containers configuration dictionaries. - :param container_ix: Container index that is loaded. - :return: AHAB Container object. - """ - ahab_container = AHABContainer(parent) - ahab_container.search_paths = parent.search_paths or [] - ahab_container.container_offset = parent.ahab_address_map[container_ix] - ahab_container.load_from_config_generic(config) - images = config.get("images") - assert isinstance(images, list) - for image in images: - ahab_container.image_array.append( - ImageArrayEntry.load_from_config(ahab_container, image) - ) - - return ahab_container - - def image_info(self) -> BinaryImage: - """Get Image info object. - - :return: AHAB Container Info object. - """ - ret = BinaryImage( - name="AHAB Container", - size=self.header_length(), - offset=0, - binary=self.export(), - description=( - f"AHAB Container for {self.flag_srk_set}" f"_SWver:{self.sw_version}" - ), - ) - return ret - - -class AHABImage: - """Class representing an AHAB image. - - The image consists of multiple AHAB containers. - """ - - TARGET_MEMORIES = [ - TARGET_MEMORY_SERIAL_DOWNLOADER, - TARGET_MEMORY_NOR, - TARGET_MEMORY_NAND_4K, - TARGET_MEMORY_NAND_2K, - ] - - def __init__( - self, - family: str, - revision: str = "latest", - target_memory: str = TARGET_MEMORY_NOR, - ahab_containers: Optional[List[AHABContainer]] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """AHAB Image constructor. - - :param family: Name of device family. - :param revision: Device silicon revision, defaults to "latest" - :param target_memory: Target memory for AHAB image [serial_downloader, nor, nand], defaults to "nor" - :param ahab_containers: _description_, defaults to None - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKValueError: Invalid input configuration. - """ - if target_memory not in self.TARGET_MEMORIES: - raise SPSDKValueError( - f"Invalid AHAB target memory [{target_memory}]." - f" The list of supported images: [{','.join(self.TARGET_MEMORIES)}]" - ) - self.target_memory = target_memory - self.family = family - self.search_paths = search_paths - self._database = get_db(family, revision) - self.revision = self._database.name - self.ahab_address_map: List[int] = self._database.get_list( - DatabaseManager.AHAB, "ahab_map" - ) - self.start_image_address = ( - START_IMAGE_ADDRESS_NAND - if target_memory in [TARGET_MEMORY_NAND_2K, TARGET_MEMORY_NAND_4K] - else START_IMAGE_ADDRESS - ) - self.containers_max_cnt = self._database.get_int( - DatabaseManager.AHAB, "containers_max_cnt" - ) - self.images_max_cnt = self._database.get_int( - DatabaseManager.AHAB, "oem_images_max_cnt" - ) - self.srkh_sha_supports: List[str] = self._database.get_list( - DatabaseManager.AHAB, "srkh_sha_supports" - ) - self.ahab_containers: List[AHABContainer] = ahab_containers or [] - - def __repr__(self) -> str: - return f"AHAB Image for {self.family}" - - def __str__(self) -> str: - return ( - "AHAB Image:\n" - f" Family: {self.family}\n" - f" Revision: {self.revision}\n" - f" Target memory: {self.target_memory}\n" - f" Max cont. count: {self.containers_max_cnt}" - f" Max image. count: {self.images_max_cnt}" - f" Containers count: {len(self.ahab_containers)}" - ) - - def add_container(self, container: AHABContainer) -> None: - """Add new container into AHAB Image. - - The order of the added images is important. - :param container: New AHAB Container to be added. - :raises SPSDKLengthError: The container count in image is overflowed. - """ - if len(self.ahab_containers) >= self.containers_max_cnt: - raise SPSDKLengthError( - "Cannot add new container because the AHAB Image already reached" - f" the maximum count: {self.containers_max_cnt}" - ) - - self.ahab_containers.append(container) - - def clear(self) -> None: - """Clear list of containers.""" - self.ahab_containers.clear() - - def update_fields(self, update_offsets: bool = True) -> None: - """Automatically updates all volatile fields in every AHAB container. - - :param update_offsets: Update also offsets for serial_downloader. - """ - for ahab_container in self.ahab_containers: - ahab_container.update_fields() - - if self.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER and update_offsets: - # Update the Image offsets to be without gaps - offset = self.start_image_address - for ahab_container in self.ahab_containers: - for image in ahab_container.image_array: - if ahab_container.lock: - offset = image.image_offset - else: - image.image_offset = offset - offset = image.get_valid_offset(offset + image.image_size) - - ahab_container.update_fields() - - def __len__(self) -> int: - """Get maximal size of AHAB Image. - - :return: Size in Bytes of AHAB Image. - """ - lengths = [0] - for container in self.ahab_containers: - lengths.append(len(container)) - return align(max(lengths), CONTAINER_ALIGNMENT) - - def get_containers_size(self) -> int: - """Get maximal containers size. - - In fact get the offset where could be stored first data. - - :return: Size of containers. - """ - if len(self.ahab_containers) == 0: - return 0 - sizes = [ - container.header_length() + address - for container, address in zip(self.ahab_containers, self.ahab_address_map) - ] - return align(max(sizes), CONTAINER_ALIGNMENT) - - def get_first_data_image_address(self) -> int: - """Get first data image address. - - :return: Address of first data image. - """ - addresses = [] - for container in self.ahab_containers: - addresses.extend([x.image_offset for x in container.image_array]) - return min(addresses) - - def export(self) -> bytes: - """Export AHAB Image. - - :raises SPSDKValueError: mismatch between number of containers and offsets. - :raises SPSDKValueError: number of images mismatch. - :return: bytes AHAB Image. - """ - self.update_fields() - self.validate() - return self.image_info().export() - - def image_info(self) -> BinaryImage: - """Get Image info object.""" - ret = BinaryImage( - name="AHAB Image", - size=len(self), - offset=0, - description=f"AHAB Image for {self.family}_{self.revision}", - pattern=BinaryPattern("0xCA"), - ) - ahab_containers = BinaryImage( - name="AHAB Containers", - size=self.start_image_address, - offset=0, - description="AHAB Containers block", - pattern=BinaryPattern("zeros"), - ) - ret.add_image(ahab_containers) - - for cnt_ix, (container, address) in enumerate( - zip(self.ahab_containers, self.ahab_address_map) - ): - container_image = container.image_info() - container_image.name = container_image.name + f" {cnt_ix}" - container_image.offset = address - ahab_containers.add_image(container_image) - - # Add also all data images - for img_ix, image_entry in enumerate(container.image_array): - data_image = BinaryImage( - name=f"Container {cnt_ix} AHAB Data Image {img_ix}", - binary=image_entry.image, - size=image_entry.image_size, - offset=image_entry.image_offset, - description=( - f"AHAB {'encrypted ' if image_entry.flags_is_encrypted else ''}" - f"data block with {image_entry.flags_image_type} Image Type." - ), - ) - - ret.add_image(data_image) - - return ret - - def validate(self) -> None: - """Validate object data. - - :raises SPSDKValueError: Invalid any value of Image Array entry. - :raises SPSDKError: In case of Binary Image validation fail. - """ - if self.ahab_containers is None or len(self.ahab_containers) == 0: - raise SPSDKValueError("AHAB Image: Missing Containers.") - if len(self.ahab_containers) > self.containers_max_cnt: - raise SPSDKValueError( - "AHAB Image: Too much AHAB containers in image." - f" {len(self.ahab_containers)} > {self.containers_max_cnt}" - ) - # prepare additional validation data - data = {} - data["srkh_sha_supports"] = self.srkh_sha_supports - - for cnt_ix, container in enumerate(self.ahab_containers): - container.validate(data) - if len(container.image_array) > self.images_max_cnt: - raise SPSDKValueError( - f"AHAB Image: Too many binary images in AHAB Container [{cnt_ix}]." - f" {len(container.image_array)} > {self.images_max_cnt}" - ) - if self.target_memory != TARGET_MEMORY_SERIAL_DOWNLOADER: - for img_ix, image_entry in enumerate(container.image_array): - if image_entry.image_offset_real < self.start_image_address: - raise SPSDKValueError( - "AHAB Data Image: The offset of data image (container" - f"{cnt_ix}/image{img_ix}) is under minimal allowed value." - f" 0x{hex(image_entry.image_offset_real)} < {hex(self.start_image_address)}" - ) - - # Validate correct data image offsets - offset = self.start_image_address - alignment = self.ahab_containers[0].image_array[0].get_valid_alignment() - for container in self.ahab_containers: - for image in container.image_array: - if image.image_offset_real != align(image.image_offset_real, alignment): - raise SPSDKValueError( - f"Image Entry: Invalid Image Offset alignment for target memory '{self.target_memory}': " - f"{hex(image.image_offset_real)} " - f"should be with alignment {hex(alignment)}.\n" - f"For example: Bootable image offset ({hex(TARGET_MEMORY_BOOT_OFFSETS[self.target_memory])})" - " + offset (" - f"{hex(align(image.image_offset, alignment) - TARGET_MEMORY_BOOT_OFFSETS[self.target_memory])})" - " is correctly aligned." - ) - if self.target_memory == TARGET_MEMORY_SERIAL_DOWNLOADER: - if offset != image.image_offset and not container.lock: - raise SPSDKValueError( - "Invalid image offset for Serial Downloader mode." - f"\n Expected {hex(offset)}, Used:{hex(image.image_offset_real)}" - ) - else: - offset = image.image_offset - offset = image.get_valid_offset(offset + image.image_size) - alignment = image.get_valid_alignment() - - # Validate also overlapped images - try: - self.image_info().validate() - except SPSDKError as exc: - logger.error(self.image_info().draw()) - raise SPSDKError("Validation failed") from exc - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "AHABImage": - """Converts the configuration option into an AHAB image object. - - "config" content array of containers configurations. - - :param config: array of AHAB containers configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKValueError: if the count of AHAB containers is invalid. - :raises SPSDKParsingError: Cannot parse input binary AHAB container. - :return: Initialized AHAB Image. - """ - containers_config: List[Dict[str, Any]] = config["containers"] - family = config["family"] - revision = config.get("revision", "latest") - target_memory = config.get("target_memory") - if target_memory is None: - # backward compatible reading of obsolete image type - image_type = config["image_type"] - target_memory = { - "xip": "nor", - "non_xip": "nor", - "nand": "nand_2k", - "serial_downloader": "serial_downloader", - }[image_type] - logger.warning( - f"The obsolete key 'image_type':{image_type} has been converted into 'target_memory':{target_memory}" - ) - ahab = AHABImage( - family=family, - revision=revision, - target_memory=target_memory, - search_paths=search_paths, - ) - i = 0 - for container_config in containers_config: - binary_container = container_config.get("binary_container") - if binary_container: - assert isinstance(binary_container, dict) - path = binary_container.get("path") - assert path - ahab_bin = load_binary(path, search_paths=search_paths) - for j in range(ahab.containers_max_cnt): - try: - ahab.add_container( - AHABContainer.parse( - ahab_bin[ahab.ahab_address_map[j] :], - parent=ahab, - container_id=i, - ) - ) - i += 1 - except SPSDKError as exc: - if j == 0: - raise SPSDKParsingError( - f"AHAB Binary Container parsing failed. ({str(exc)})" - ) from exc - else: - break - - else: - ahab.add_container( - AHABContainer.load_from_config( - ahab, container_config["container"], i - ) - ) - i += 1 - - return ahab - - def parse(self, binary: bytes) -> None: - """Parse input binary chunk to the container object. - - :raises SPSDKError: No AHAB container found in binary data. - """ - self.clear() - - for i, address in enumerate(self.ahab_address_map): - try: - container = AHABContainer.parse( - binary[address:], parent=self, container_id=i - ) - self.ahab_containers.append(container) - except SPSDKParsingError as exc: - logger.debug(f"AHAB Image parsing error:\n{str(exc)}") - except SPSDKError as exc: - raise SPSDKError(f"AHAB Container parsing failed: {str(exc)}.") from exc - if len(self.ahab_containers) == 0: - raise SPSDKError("No AHAB Container has been found in binary data.") - - @staticmethod - def get_supported_families() -> List[str]: - """Get all supported families for AHAB container. - - :return: List of supported families. - """ - return get_families(DatabaseManager.AHAB) - - @staticmethod - def get_validation_schemas() -> List[Dict[str, Any]]: - """Get list of validation schemas. - - :return: Validation list of schemas. - """ - sch = DatabaseManager().db.get_schema_file(DatabaseManager.AHAB)[ - "whole_ahab_image" - ] - sch["properties"]["family"]["enum"] = AHABImage.get_supported_families() - return [sch] - - @staticmethod - def generate_config_template(family: str) -> Dict[str, Any]: - """Generate AHAB configuration template. - - :param family: Family for which the template should be generated. - :return: Dictionary of individual templates (key is name of template, value is template itself). - """ - val_schemas = AHABImage.get_validation_schemas() - val_schemas[0]["properties"]["family"]["template_value"] = family - - yaml_data = CommentedConfig( - f"Advanced High-Assurance Boot Configuration template for {family}.", - val_schemas, - ).get_template() - - return {f"{family}_ahab": yaml_data} - - def create_config(self, data_path: str) -> Dict[str, Any]: - """Create configuration of the AHAB Image. - - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - cfg: Dict[str, Any] = {} - cfg["family"] = self.family - cfg["revision"] = self.revision - cfg["target_memory"] = self.target_memory - cfg["output"] = "N/A" - cfg_containers = [] - for cnt_ix, container in enumerate(self.ahab_containers): - cfg_containers.append(container.create_config(cnt_ix, data_path)) - cfg["containers"] = cfg_containers - - return cfg - - def create_srk_hash_blhost_script(self, container_ix: int = 0) -> str: - """Create BLHOST script to load SRK hash into fuses. - - :param container_ix: Container index. - :raises SPSDKValueError: Invalid input value - Non existing container or unsupported type. - :raises SPSDKError: Invalid SRK hash. - :return: Script used by BLHOST to load SRK hash. - """ - if container_ix > len(self.ahab_containers): - raise SPSDKValueError(f"Invalid Container index: {container_ix}.") - container_type = self.ahab_containers[container_ix].flag_srk_set - - fuses_start = self._database.get_int( - DatabaseManager.AHAB, f"{container_type}_srkh_fuses_start" - ) - fuses_count = self._database.get_int( - DatabaseManager.AHAB, f"{container_type}_srkh_fuses_count" - ) - fuses_size = self._database.get_int( - DatabaseManager.AHAB, f"{container_type}_srkh_fuses_size" - ) - if fuses_start is None or fuses_count is None or fuses_size is None: - raise SPSDKValueError( - f"Unsupported container type({container_type}) to create BLHOST script" - ) - - srk_table = self.ahab_containers[container_ix].signature_block.srk_table - if srk_table is None: - raise SPSDKError("The selected AHAB container doesn't contain SRK table.") - - srkh = srk_table.compute_srk_hash() - - if len(srkh) != fuses_count * fuses_size: - raise SPSDKError( - f"The SRK hash length ({len(srkh)}) doesn't fit to fuses space ({fuses_count*fuses_size})." - ) - ret = ( - "# BLHOST SRK Hash fuses programming script\n" - f"# Generated by SPSDK {spsdk_version}\n" - f"# Chip: {self.family} rev:{self.revision}\n" - f"# SRK Hash(Big Endian): {srkh.hex()}\n\n" - ) - srkh_rev = reverse_bytes_in_longs(srkh) - for fuse_ix in range(fuses_count): - value = srkh_rev[fuse_ix * 4 : fuse_ix * 4 + 4] - ret += f"# OEM SRKH{fuses_count-1-fuse_ix} fuses.\n" - ret += f"efuse-program-once {hex(fuses_start+fuse_ix)} 0x{value.hex()}\n" - - return ret diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py deleted file mode 100644 index 9c33910b..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/signed_msg.py +++ /dev/null @@ -1,1623 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2021-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Implementation of raw AHAB container support. - -This module represents a generic AHAB container implementation. You can set the -containers values at will. From this perspective, consult with your reference -manual of your device for allowed values. -""" -import datetime -import logging -from abc import abstractmethod -from inspect import isclass -from struct import calcsize, pack, unpack -from typing import Any, Dict, List, Optional, Tuple, Type - -from typing_extensions import Self - -from ...exceptions import SPSDKError, SPSDKValueError -from ...image.ahab.ahab_abstract_interfaces import LITTLE_ENDIAN, Container -from ...image.ahab.ahab_container import ( - CONTAINER_ALIGNMENT, - RESERVED, - UINT8, - UINT16, - UINT32, - AHABContainerBase, - AHABImage, - SignatureBlock, -) -from ...utils.database import DatabaseManager -from ...utils.images import BinaryImage -from ...utils.misc import ( - Endianness, - align_block, - check_range, - load_hex_string, - value_to_int, -) -from ...utils.schema_validator import CommentedConfig -from ...utils.spsdk_enum import SpsdkEnum - -logger = logging.getLogger(__name__) - - -class SignedMessageTags(SpsdkEnum): - """Signed message container related tags.""" - - SIGNED_MSG = (0x89, "SIGNED_MSG", "Signed message.") - - -class MessageCommands(SpsdkEnum): - """Signed messages commands.""" - - KEYSTORE_REPROVISIONING_ENABLE_REQ = ( - 0x3F, - "KEYSTORE_REPROVISIONING_ENABLE_REQ", - "Key store reprovisioning enable", - ) - - KEY_EXCHANGE_REQ = ( - 0x47, - "KEY_EXCHANGE_REQ", - "Key exchange signed message content", - ) - - RETURN_LIFECYCLE_UPDATE_REQ = ( - 0xA0, - "RETURN_LIFECYCLE_UPDATE_REQ", - "Return lifecycle update request.", - ) - WRITE_SEC_FUSE_REQ = (0x91, "WRITE_SEC_FUSE_REQ", "Write secure fuse request.") - - -class Message(Container): - """Class representing the Signed message. - - Message:: - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Message header | - +-----+---------------------------------------------------------------+ - |0x10 | Message payload | - +-----+---------------------------------------------------------------+ - - - Message header:: - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Cert version | Permission | Issue date | - +-----+--------------+--------------+---------------------------------+ - |0x04 | Reserved | Command | Reserved | - +-----+--------------+--------------+---------------------------------+ - |0x08 | Unique ID (Lower 32 bits) | - +-----+---------------------------------------------------------------+ - |0x0c | Unique ID (Upper 32 bits) | - +-----+---------------------------------------------------------------+ - - The message header is common for all signed messages. - - """ - - UNIQUE_ID_LEN = 8 - TAG = 0 - PAYLOAD_LENGTH = 0 - - def __init__( - self, - cert_ver: int = 0, - permissions: int = 0, - issue_date: Optional[int] = None, - cmd: int = 0, - unique_id: Optional[bytes] = None, - ) -> None: - """Message used to sign and send to device with EdgeLock. - - :param cert_ver: Certificate version, defaults to 0 - :param permissions: Certificate permission, to be used in future - The stated permission must allow the operation requested by the signed message - , defaults to 0 - :param issue_date: Issue date, defaults to None (Current date will be applied) - :param cmd: Message command ID, defaults to 0 - :param unique_id: UUID of device (least 64 bits is used), defaults to None - """ - self.cert_ver = cert_ver - self.permissions = permissions - now = datetime.datetime.now() - self.issue_date = issue_date or (now.month << 12 | now.year) - self.cmd = cmd - self.unique_id = unique_id or b"" - - def __repr__(self) -> str: - return f"Message, {MessageCommands.get_description(self.TAG, 'Base Class')}" - - def __str__(self) -> str: - ret = repr(self) + ":\n" - ret += ( - f" Certificate version:{self.cert_ver}\n" - f" Permissions: {hex(self.permissions)}\n" - f" Issue date: {hex(self.issue_date)}\n" - f" UUID: {self.unique_id.hex() if self.unique_id else 'Not Available'}" - ) - return ret - - def __len__(self) -> int: - """Returns the total length of a container. - - The length includes the fixed as well as the variable length part. - """ - return self.fixed_length() + self.payload_len - - @property - def payload_len(self) -> int: - """Message payload length in bytes.""" - return self.PAYLOAD_LENGTH - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() - + UINT16 # Issue Date - + UINT8 # Permission - + UINT8 # Certificate version - + UINT16 # Reserved to zero - + UINT8 # Command - + UINT8 # Reserved - + "4s" # Unique ID (Lower 32 bits) - + "4s" # Unique ID (Upper 32 bits) - ) - - def validate(self) -> None: - """Validate general message properties.""" - if self.cert_ver is None or not check_range(self.cert_ver, end=(1 << 8) - 1): - raise SPSDKValueError( - f"Message: Invalid certificate version: {hex(self.cert_ver) if self.cert_ver else 'None'}" - ) - - if self.permissions is None or not check_range( - self.permissions, end=(1 << 8) - 1 - ): - raise SPSDKValueError( - f"Message: Invalid certificate permission: {hex(self.permissions) if self.permissions else 'None'}" - ) - - if self.issue_date is None or not check_range( - self.issue_date, start=1, end=(1 << 16) - 1 - ): - raise SPSDKValueError( - f"Message: Invalid issue date: {hex(self.issue_date) if self.issue_date else 'None'}" - ) - - if self.cmd is None or self.cmd not in MessageCommands.tags(): - raise SPSDKValueError( - f"Message: Invalid command: {hex(self.cmd) if self.cmd else 'None'}" - ) - - if self.unique_id is None or len(self.unique_id) < Message.UNIQUE_ID_LEN: - raise SPSDKValueError( - f"Message: Invalid unique ID: {self.unique_id.hex() if self.unique_id else 'None'}" - ) - - def export(self) -> bytes: - """Exports message into to bytes array. - - :return: Bytes representation of message object. - """ - msg = pack( - self.format(), - self.issue_date, - self.permissions, - self.cert_ver, - RESERVED, - self.cmd, - RESERVED, - self.unique_id[:4], - self.unique_id[4:8], - ) - msg += self.export_payload() - return msg - - @abstractmethod - def export_payload(self) -> bytes: - """Exports message payload to bytes array. - - :return: Bytes representation of message payload. - """ - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Message": - """Converts the configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: Message object. - """ - command = config.get("command") - assert command and len(command) == 1 - msg_cls = Message.get_message_class(list(command.keys())[0]) - return msg_cls.load_from_config(config, search_paths=search_paths) - - @staticmethod - def load_from_config_generic( - config: Dict[str, Any] - ) -> Tuple[int, int, Optional[int], bytes]: - """Converts the general configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :return: Message object. - """ - cert_ver = value_to_int(config.get("cert_version", 0)) - permission = value_to_int(config.get("cert_permission", 0)) - issue_date_raw = config.get("issue_date", None) - if issue_date_raw: - assert isinstance(issue_date_raw, str) - year, month = issue_date_raw.split("-") - issue_date = max(min(12, int(month)), 1) << 12 | int(year) - else: - issue_date = None - - uuid = bytes.fromhex(config.get("uuid", bytes(Message.UNIQUE_ID_LEN).hex())) - return (cert_ver, permission, issue_date, uuid) - - def _create_general_config(self) -> Dict[str, Any]: - """Create configuration of the general parts of Message. - - :return: Configuration dictionary. - """ - assert self.unique_id - cfg: Dict[str, Any] = {} - cfg["cert_version"] = self.cert_ver - cfg["cert_permission"] = self.permissions - cfg["issue_date"] = f"{(self.issue_date & 0xfff)}-{(self.issue_date>>12) & 0xf}" - cfg["uuid"] = self.unique_id.hex() - - return cfg - - @abstractmethod - def create_config(self) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :return: Configuration dictionary. - """ - - @classmethod - def get_message_class(cls, cmd: str) -> Type[Self]: - """Get the dedicated message class for command.""" - for var in globals(): - obj = globals()[var] - if isclass(obj) and issubclass(obj, Message) and obj is not Message: - assert issubclass(obj, Message) - if MessageCommands.from_label(cmd) == obj.TAG: - return obj # type: ignore - - raise SPSDKValueError(f"Command {cmd} is not supported.") - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary to the signed message object. - - :param data: Binary data with Container block to parse. - :return: Object recreated from the binary data. - """ - ( - issue_date, # issue Date - permission, # permission - certificate_version, # certificate version - _, # Reserved to zero - command, # Command - _, # Reserved - uuid_lower, # Unique ID (Lower 32 bits) - uuid_upper, # Unique ID (Upper 32 bits) - ) = unpack(Message.format(), data[: Message.fixed_length()]) - - cmd_name = MessageCommands.get_label(command) - msg_cls = Message.get_message_class(cmd_name) - parsed_msg = msg_cls( - cert_ver=certificate_version, - permissions=permission, - issue_date=issue_date, - unique_id=uuid_lower + uuid_upper, - ) - parsed_msg.parse_payload(data[Message.fixed_length() :]) - return parsed_msg # type: ignore - - @abstractmethod - def parse_payload(self, data: bytes) -> None: - """Parse payload. - - :param data: Binary data with Payload to parse. - """ - - -class MessageReturnLifeCycle(Message): - """Return life cycle request message class representation.""" - - TAG = MessageCommands.RETURN_LIFECYCLE_UPDATE_REQ.tag - PAYLOAD_LENGTH = 4 - - def __init__( - self, - cert_ver: int = 0, - permissions: int = 0, - issue_date: Optional[int] = None, - unique_id: Optional[bytes] = None, - life_cycle: int = 0, - ) -> None: - """Message used to sign and send to device with EdgeLock. - - :param cert_ver: Certificate version, defaults to 0 - :param permissions: Certificate permission, to be used in future - The stated permission must allow the operation requested by the signed message - , defaults to 0 - :param issue_date: Issue date, defaults to None (Current date will be applied) - :param unique_id: UUID of device (least 64 bits is used), defaults to None - :param life_cycle: Requested life cycle, defaults to 0 - """ - super().__init__( - cert_ver=cert_ver, - permissions=permissions, - issue_date=issue_date, - cmd=self.TAG, - unique_id=unique_id, - ) - self.life_cycle = life_cycle - - def __str__(self) -> str: - ret = super().__str__() - ret += f" Life Cycle: {hex(self.life_cycle)}" - return ret - - def export_payload(self) -> bytes: - """Exports message payload to bytes array. - - :return: Bytes representation of message payload. - """ - return self.life_cycle.to_bytes(length=4, byteorder=Endianness.LITTLE.value) - - def parse_payload(self, data: bytes) -> None: - """Parse payload. - - :param data: Binary data with Payload to parse. - """ - self.life_cycle = int.from_bytes(data[:4], byteorder=Endianness.LITTLE.value) - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Message": - """Converts the configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKError: Invalid configuration detected. - :return: Message object. - """ - command = config.get("command", {}) - if not isinstance(command, dict) or len(command) != 1: - raise SPSDKError(f"Invalid config field command: {command}") - command_name = list(command.keys())[0] - if MessageCommands.from_label(command_name) != MessageReturnLifeCycle.TAG: - raise SPSDKError( - "Invalid configuration for Return Life Cycle Request command." - ) - - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( - config - ) - - life_cycle = command.get("RETURN_LIFECYCLE_UPDATE_REQ") - assert isinstance(life_cycle, int) - - return MessageReturnLifeCycle( - cert_ver=cert_ver, - permissions=permission, - issue_date=issue_date, - unique_id=uuid, - life_cycle=life_cycle, - ) - - def create_config(self) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :return: Configuration dictionary. - """ - cfg = self._create_general_config() - cmd_cfg = {} - cmd_cfg[MessageCommands.get_label(self.TAG)] = self.life_cycle - cfg["command"] = cmd_cfg - - return cfg - - def validate(self) -> None: - """Validate general message properties.""" - super().validate() - if self.life_cycle is None: - raise SPSDKValueError( - "Message Return Life Cycle request: Invalid life cycle" - ) - - -class MessageWriteSecureFuse(Message): - """Write secure fuse request message class representation.""" - - TAG = MessageCommands.WRITE_SEC_FUSE_REQ.tag - PAYLOAD_FORMAT = LITTLE_ENDIAN + UINT16 + UINT8 + UINT8 - - def __init__( - self, - cert_ver: int = 0, - permissions: int = 0, - issue_date: Optional[int] = None, - unique_id: Optional[bytes] = None, - fuse_id: int = 0, - length: int = 0, - flags: int = 0, - data: Optional[List[int]] = None, - ) -> None: - """Message used to sign and send to device with EdgeLock. - - :param cert_ver: Certificate version, defaults to 0 - :param permissions: Certificate permission, to be used in future - The stated permission must allow the operation requested by the signed message - , defaults to 0 - :param issue_date: Issue date, defaults to None (Current date will be applied) - :param unique_id: UUID of device (least 64 bits is used), defaults to None - :param fuse_id: Fuse ID, defaults to 0 - :param length: Fuse length, defaults to 0 - :param flags: Fuse flags, defaults to 0 - :param data: List of fuse values - """ - super().__init__( - cert_ver=cert_ver, - permissions=permissions, - issue_date=issue_date, - cmd=self.TAG, - unique_id=unique_id, - ) - self.fuse_id = fuse_id - self.length = length - self.flags = flags - self.fuse_data: List[int] = data or [] - - def __str__(self) -> str: - ret = super().__str__() - ret += f" Fuse Index: {hex(self.fuse_id)}, {self.fuse_id}\n" - ret += f" Fuse Length: {self.length}\n" - ret += f" Fuse Flags: {hex(self.flags)}\n" - for i, data in enumerate(self.fuse_data): - ret += f" Fuse{i} Value: 0x{data:08X}" - return ret - - @property - def payload_len(self) -> int: - """Message payload length in bytes.""" - return 4 + len(self.fuse_data) * 4 - - def export_payload(self) -> bytes: - """Exports message payload to bytes array. - - :return: Bytes representation of message payload. - """ - payload = pack(self.PAYLOAD_FORMAT, self.fuse_id, self.length, self.flags) - for data in self.fuse_data: - payload += data.to_bytes(4, Endianness.LITTLE.value) - return payload - - def parse_payload(self, data: bytes) -> None: - """Parse payload. - - :param data: Binary data with Payload to parse. - """ - self.fuse_id, self.length, self.flags = unpack(self.PAYLOAD_FORMAT, data[:4]) - self.fuse_data.clear() - for i in range(self.length): - self.fuse_data.append( - int.from_bytes(data[4 + i * 4 : 8 + i * 4], Endianness.LITTLE.value) - ) - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Message": - """Converts the configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKError: Invalid configuration detected. - :return: Message object. - """ - command = config.get("command", {}) - if not isinstance(command, dict) or len(command) != 1: - raise SPSDKError(f"Invalid config field command: {command}") - command_name = list(command.keys())[0] - if MessageCommands.from_label(command_name) != MessageWriteSecureFuse.TAG: - raise SPSDKError( - "Invalid configuration for Write secure fuse Request command." - ) - - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( - config - ) - - secure_fuse = command.get("WRITE_SEC_FUSE_REQ") - assert isinstance(secure_fuse, dict) - fuse_id = secure_fuse.get("id") - assert isinstance(fuse_id, int) - flags: int = secure_fuse.get("flags", 0) - data_list: List = secure_fuse.get("data", []) - data = [] - for x in data_list: - data.append(value_to_int(x)) - length = len(data_list) - return MessageWriteSecureFuse( - cert_ver=cert_ver, - permissions=permission, - issue_date=issue_date, - unique_id=uuid, - fuse_id=fuse_id, - length=length, - flags=flags, - data=data, - ) - - def create_config(self) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :return: Configuration dictionary. - """ - cfg = self._create_general_config() - write_fuse_cfg: Dict[str, Any] = {} - cmd_cfg = {} - write_fuse_cfg["id"] = self.fuse_id - write_fuse_cfg["flags"] = self.flags - write_fuse_cfg["data"] = [f"0x{x:08X}" for x in self.fuse_data] - - cmd_cfg[MessageCommands.get_label(self.TAG)] = write_fuse_cfg - cfg["command"] = cmd_cfg - - return cfg - - def validate(self) -> None: - """Validate general message properties.""" - super().validate() - if self.fuse_data is None: - raise SPSDKValueError( - "Message Write secure fuse request: Missing fuse data" - ) - if len(self.fuse_data) != self.length: - raise SPSDKValueError( - "Message Write secure fuse request: The fuse value list " - f"has invalid length: ({len(self.fuse_data)} != {self.length})" - ) - - for i, val in enumerate(self.fuse_data): - if val >= 1 << 32: - raise SPSDKValueError( - f"Message Write secure fuse request: The fuse value({i}) is bigger than 32 bit: ({val})" - ) - - -class MessageKeyStoreReprovisioningEnable(Message): - """Key store reprovisioning enable request message class representation.""" - - TAG = MessageCommands.KEYSTORE_REPROVISIONING_ENABLE_REQ.tag - PAYLOAD_LENGTH = 12 - PAYLOAD_FORMAT = LITTLE_ENDIAN + UINT8 + UINT8 + UINT16 + UINT32 + UINT32 - - FLAGS = 0 # 0 : HSM storage. - TARGET = 0 # Target ELE - - def __init__( - self, - cert_ver: int = 0, - permissions: int = 0, - issue_date: Optional[int] = None, - unique_id: Optional[bytes] = None, - monotonic_counter: int = 0, - user_sab_id: int = 0, - ) -> None: - """Key store reprovisioning enable signed message class init. - - :param cert_ver: Certificate version, defaults to 0 - :param permissions: Certificate permission, to be used in future - The stated permission must allow the operation requested by the signed message - , defaults to 0 - :param issue_date: Issue date, defaults to None (Current date will be applied) - :param unique_id: UUID of device (least 64 bits is used), defaults to None - :param monotonic_counter: Monotonic counter value, defaults to 0 - :param user_sab_id: User SAB id, defaults to 0 - """ - super().__init__( - cert_ver=cert_ver, - permissions=permissions, - issue_date=issue_date, - cmd=self.TAG, - unique_id=unique_id, - ) - self.flags = self.FLAGS - self.target = self.TARGET - self.reserved = RESERVED - self.monotonic_counter = monotonic_counter - self.user_sab_id = user_sab_id - - def export_payload(self) -> bytes: - """Exports message payload to bytes array. - - :return: Bytes representation of message payload. - """ - return pack( - self.PAYLOAD_FORMAT, - self.flags, - self.target, - self.reserved, - self.monotonic_counter, - self.user_sab_id, - ) - - def parse_payload(self, data: bytes) -> None: - """Parse payload. - - :param data: Binary data with Payload to parse. - """ - ( - self.flags, - self.target, - self.reserved, - self.monotonic_counter, - self.user_sab_id, - ) = unpack(self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH]) - - def validate(self) -> None: - """Validate general message properties.""" - super().validate() - if self.flags != self.FLAGS: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid flags: {self.flags}" - ) - if self.target != self.TARGET: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid target: {self.target}" - ) - if self.reserved != RESERVED: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid reserved field: {self.reserved}" - ) - if self.monotonic_counter >= 1 << 32: - raise SPSDKValueError( - "Message Key store reprovisioning enable request: Invalid monotonic " - f"counter field (not fit in 32bit): {self.monotonic_counter}" - ) - - if self.user_sab_id >= 1 << 32: - raise SPSDKValueError( - "Message Key store reprovisioning enable request: Invalid user SAB ID " - f"field (not fit in 32bit): {self.user_sab_id}" - ) - - def __str__(self) -> str: - ret = super().__str__() - ret += f" Monotonic counter value: 0x{self.monotonic_counter:08X}, {self.monotonic_counter}\n" - ret += ( - f" User SAB id: 0x{self.user_sab_id:08X}, {self.user_sab_id}" - ) - return ret - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Message": - """Converts the configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKError: Invalid configuration detected. - :return: Message object. - """ - command = config.get("command", {}) - if not isinstance(command, dict) or len(command) != 1: - raise SPSDKError(f"Invalid config field command: {command}") - command_name = list(command.keys())[0] - if ( - MessageCommands.from_label(command_name) - != MessageKeyStoreReprovisioningEnable.TAG - ): - raise SPSDKError( - "Invalid configuration for Write secure fuse Request command." - ) - - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( - config - ) - - keystore_repr_en = command.get("KEYSTORE_REPROVISIONING_ENABLE_REQ") - assert isinstance(keystore_repr_en, dict) - monotonic_counter = value_to_int(keystore_repr_en.get("monotonic_counter", 0)) - user_sab_id = value_to_int(keystore_repr_en.get("user_sab_id", 0)) - return MessageKeyStoreReprovisioningEnable( - cert_ver=cert_ver, - permissions=permission, - issue_date=issue_date, - unique_id=uuid, - monotonic_counter=monotonic_counter, - user_sab_id=user_sab_id, - ) - - def create_config(self) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :return: Configuration dictionary. - """ - cfg = self._create_general_config() - keystore_repr_en_cfg: Dict[str, Any] = {} - cmd_cfg = {} - keystore_repr_en_cfg["monotonic_counter"] = f"0x{self.monotonic_counter:08X}" - keystore_repr_en_cfg["user_sab_id"] = f"0x{self.user_sab_id:08X}" - - cmd_cfg[MessageCommands.get_label(self.TAG)] = keystore_repr_en_cfg - cfg["command"] = cmd_cfg - - return cfg - - -class MessageKeyExchange(Message): - """Key exchange request message class representation.""" - - TAG = MessageCommands.KEY_EXCHANGE_REQ.tag - PAYLOAD_LENGTH = 27 * 4 - PAYLOAD_VERSION = 0x07 - PAYLOAD_FORMAT = ( - LITTLE_ENDIAN - + UINT8 # TAG - + UINT8 # Version - + UINT16 # Reserved - + UINT32 # Key store ID - + UINT32 # Key exchange algorithm - + UINT16 # Salt Flags - + UINT16 # Derived key group - + UINT16 # Derived key size bits - + UINT16 # Derived key type - + UINT32 # Derived key lifetime - + UINT32 # Derived key usage - + UINT32 # Derived key permitted algorithm - + UINT32 # Derived key lifecycle - + UINT32 # Derived key ID - + UINT32 # Private key ID - + "32s" # Input peer public key digest word [0-7] - + "32s" # Input user fixed info digest word [0-7] - ) - - class KeyExchangeAlgorithm(SpsdkEnum): - """Key Exchange Algorithm valid values.""" - - HKDF_SHA256 = (0x09020109, "HKDF SHA256") - HKDF_SHA384 = (0x0902010A, "HKDF SHA384") - - class KeyDerivationAlgorithm(SpsdkEnum): - """Key Derivation Algorithm valid values.""" - - HKDF_SHA256 = (0x08000109, "HKDF SHA256", "HKDF SHA256 (HMAC two-step)") - HKDF_SHA384 = (0x0800010A, "HKDF SHA384", "HKDF SHA384 (HMAC two-step)") - - class DerivedKeyType(SpsdkEnum): - """Derived Key Type valid values.""" - - AES = (0x2400, "AES SHA256", "Possible bit widths: 128/192/256") - HMAC = (0x1100, "HMAC SHA384", "Possible bit widths: 224/256/384/512") - OEM_IMPORT_MK_SK = ( - 0x9200, - "OEM_IMPORT_MK_SK", - "Possible bit widths: 128/192/256", - ) - - class LifeCycle(SpsdkEnum): - """Chip life cycle valid values.""" - - CURRENT = (0x00, "CURRENT", "Current device lifecycle") - OPEN = (0x01, "OPEN") - CLOSED = (0x02, "CLOSED") - LOCKED = (0x04, "LOCKED") - - class LifeTime(SpsdkEnum): - """Edgelock Enclave life time valid values.""" - - VOLATILE = (0x00, "VOLATILE", "Standard volatile key") - PERSISTENT = (0x01, "PERSISTENT", "Standard persistent key") - PERMANENT = (0xFF, "PERMANENT", "Standard permanent key") - - class DerivedKeyUsage(SpsdkEnum): - """Derived Key Usage valid values.""" - - CACHE = ( - 0x00000004, - "Cache", - ( - "Permission to cache the key in the ELE internal secure memory. " - "This usage is set by default by ELE FW for all keys generated or imported." - ), - ) - ENCRYPT = ( - 0x00000100, - "Encrypt", - ( - "Permission to encrypt a message with the key. It could be cipher encryption," - " AEAD encryption or asymmetric encryption operation." - ), - ) - DECRYPT = ( - 0x00000200, - "Decrypt", - ( - "Permission to decrypt a message with the key. It could be cipher decryption," - " AEAD decryption or asymmetric decryption operation." - ), - ) - SIGN_MSG = ( - 0x00000400, - "Sign message", - ( - "Permission to sign a message with the key. It could be a MAC generation or an " - "asymmetric message signature operation." - ), - ) - VERIFY_MSG = ( - 0x00000800, - "Verify message", - ( - "Permission to verify a message signature with the key. It could be a MAC " - "verification or an asymmetric message signature verification operation." - ), - ) - SIGN_HASH = ( - 0x00001000, - "Sign hash", - ( - "Permission to sign a hashed message with the key with an asymmetric signature " - "operation. Setting this permission automatically sets the Sign Message usage." - ), - ) - VERIFY_HASH = ( - 0x00002000, - "Sign message", - ( - "Permission to verify a hashed message signature with the key with an asymmetric " - "signature verification operation. Setting this permission automatically sets the Verify Message usage." - ), - ) - DERIVE = ( - 0x00004000, - "Derive", - "Permission to derive other keys from this key.", - ) - - def __init__( - self, - cert_ver: int = 0, - permissions: int = 0, - issue_date: Optional[int] = None, - unique_id: Optional[bytes] = None, - key_store_id: int = 0, - key_exchange_algorithm: KeyExchangeAlgorithm = KeyExchangeAlgorithm.HKDF_SHA256, - salt_flags: int = 0, - derived_key_grp: int = 0, - derived_key_size_bits: int = 0, - derived_key_type: DerivedKeyType = DerivedKeyType.AES, - derived_key_lifetime: LifeTime = LifeTime.PERSISTENT, - derived_key_usage: Optional[List[DerivedKeyUsage]] = None, - derived_key_permitted_algorithm: KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDF_SHA256, - derived_key_lifecycle: LifeCycle = LifeCycle.OPEN, - derived_key_id: int = 0, - private_key_id: int = 0, - input_peer_public_key_digest: bytes = bytes(), - input_user_fixed_info_digest: bytes = bytes(), - ) -> None: - """Key exchange signed message class init. - - :param cert_ver: Certificate version, defaults to 0 - :param permissions: Certificate permission, to be used in future - The stated permission must allow the operation requested by the signed message - , defaults to 0 - :param issue_date: Issue date, defaults to None (Current date will be applied) - :param unique_id: UUID of device (least 64 bits is used), defaults to None - :param key_store_id: Key store ID where to store the derived key. It must be the key store ID - related to the key management handle set in the command API, defaults to 0 - :param key_exchange_algorithm: Algorithm used by the key exchange process: - - | HKDF SHA256 0x09020109 - | HKDF SHA384 0x0902010A - | , defaults to HKDF_SHA256 - - :param salt_flags: Bit field indicating the requested operations: - - | Bit 0: Salt in step #1 (HKDF-extract) of HMAC based two-step key derivation process: - | - 0: Use zeros salt; - | - 1:Use peer public key hash as salt; - | Bit 1: In case of ELE import, salt used to derive OEM_IMPORT_WRAP_SK and OEM_IMPORT_CMAC_SK: - | - 0: Zeros string; - | - 1: Device SRKH. - | Bit 2 to 15: Reserved, defaults to 0 - - :param derived_key_grp: Derived key group. 100 groups are available per key store. It must be a - value in the range [0; 99]. Keys belonging to the same group can be managed through - the Manage key group command, defaults to 0 - :param derived_key_size_bits: Derived key size bits attribute, defaults to 0 - :param derived_key_type: - - +-------------------+-------+------------------+ - |Key type | Value | Key size in bits | - +===================+=======+==================+ - | AES |0x2400 | 128/192/256 | - +-------------------+-------+------------------+ - | HMAC |0x1100 | 224/256/384/512 | - +-------------------+-------+------------------+ - | OEM_IMPORT_MK_SK* |0x9200 | 128/192/256 | - +-------------------+-------+------------------+ - - , defaults to AES - - :param derived_key_lifetime: Derived key lifetime attribute - - | VOLATILE 0x00 Standard volatile key. - | PERSISTENT 0x01 Standard persistent key. - | PERMANENT 0xFF Standard permanent key., defaults to PERSISTENT - - :param derived_key_usage: Derived key usage attribute. - - | Cache 0x00000004 Permission to cache the key in the ELE internal secure memory. - | This usage is set by default by ELE FW for all keys generated or imported. - | Encrypt 0x00000100 Permission to encrypt a message with the key. It could be cipher - | encryption, AEAD encryption or asymmetric encryption operation. - | Decrypt 0x00000200 Permission to decrypt a message with the key. It could be - | cipher decryption, AEAD decryption or asymmetric decryption operation. - | Sign message 0x00000400 Permission to sign a message with the key. It could be - | a MAC generation or an asymmetric message signature operation. - | Verify message 0x00000800 Permission to verify a message signature with the key. - | It could be a MAC verification or an asymmetric message signature - | verification operation. - | Sign hash 0x00001000 Permission to sign a hashed message with the key - | with an asymmetric signature operation. Setting this permission automatically - | sets the Sign Message usage. - | Verify hash 0x00002000 Permission to verify a hashed message signature with - | the key with an asymmetric signature verification operation. - | Setting this permission automatically sets the Verify Message usage. - | Derive 0x00004000 Permission to derive other keys from this key. - | , defaults to 0 - - :param derived_key_permitted_algorithm: Derived key permitted algorithm attribute - - | HKDF SHA256 (HMAC two-step) 0x08000109 - | HKDF SHA384 (HMAC two-step) 0x0800010A, defaults to HKDF_SHA256 - - :param derived_key_lifecycle: Derived key lifecycle attribute - - | CURRENT 0x00 Key is usable in current lifecycle. - | OPEN 0x01 Key is usable in open lifecycle. - | CLOSED 0x02 Key is usable in closed lifecycle. - | CLOSED and LOCKED 0x04 Key is usable in closed and locked lifecycle. - | , defaults to OPEN - - :param derived_key_id: Derived key ID attribute. It could be: - - - Wanted key identifier of the generated key: only supported by persistent - and permanent keys; - - 0x00000000 to let the FW chose the key identifier: supported by all - keys (all persistence levels). , defaults to 0 - - :param private_key_id: Identifier in the ELE key storage of the private key to use with the peer - public key during the key agreement process, defaults to 0 - :param input_peer_public_key_digest: Input peer public key digest buffer. - The algorithm used to generate the digest must be SHA256, defaults to list(8) - :param input_user_fixed_info_digest: Input user fixed info digest buffer. - The algorithm used to generate the digest must be SHA256, defaults to list(8) - """ - super().__init__( - cert_ver=cert_ver, - permissions=permissions, - issue_date=issue_date, - cmd=self.TAG, - unique_id=unique_id, - ) - self.tag = self.TAG - self.version = self.PAYLOAD_VERSION - self.reserved = RESERVED - self.key_store_id = key_store_id - self.key_exchange_algorithm = key_exchange_algorithm - self.salt_flags = salt_flags - self.derived_key_grp = derived_key_grp - self.derived_key_size_bits = derived_key_size_bits - self.derived_key_type = derived_key_type - self.derived_key_lifetime = derived_key_lifetime - self.derived_key_usage = derived_key_usage or [] - self.derived_key_permitted_algorithm = derived_key_permitted_algorithm - self.derived_key_lifecycle = derived_key_lifecycle - self.derived_key_id = derived_key_id - self.private_key_id = private_key_id - self.input_peer_public_key_digest = input_peer_public_key_digest - self.input_user_fixed_info_digest = input_user_fixed_info_digest - - def export_payload(self) -> bytes: - """Exports message payload to bytes array. - - :return: Bytes representation of message payload. - """ - derived_key_usage = 0 - for usage in self.derived_key_usage: - derived_key_usage |= usage.tag - return pack( - self.PAYLOAD_FORMAT, - self.tag, - self.version, - self.reserved, - self.key_store_id, - self.key_exchange_algorithm.tag, - self.derived_key_grp, - self.salt_flags, - self.derived_key_type.tag, - self.derived_key_size_bits, - self.derived_key_lifetime.tag, - derived_key_usage, - self.derived_key_permitted_algorithm.tag, - self.derived_key_lifecycle.tag, - self.derived_key_id, - self.private_key_id, - self.input_peer_public_key_digest, - self.input_user_fixed_info_digest, - ) - - def parse_payload(self, data: bytes) -> None: - """Parse payload. - - :param data: Binary data with Payload to parse. - """ - ( - self.tag, - self.version, - self.reserved, - self.key_store_id, - key_exchange_algorithm, - self.derived_key_grp, - self.salt_flags, - derived_key_type, - self.derived_key_size_bits, - derived_key_lifetime, - derived_key_usage, - derived_key_permitted_algorithm, - derived_key_lifecycle, - self.derived_key_id, - self.private_key_id, - input_peer_public_key_digest, - input_user_fixed_info_digest, - ) = unpack(self.PAYLOAD_FORMAT, data[: self.PAYLOAD_LENGTH]) - - # Do some post process - self.key_exchange_algorithm = self.KeyExchangeAlgorithm.from_tag( - key_exchange_algorithm - ) - self.derived_key_type = self.DerivedKeyType.from_tag(derived_key_type) - self.derived_key_lifetime = self.LifeTime.from_tag(derived_key_lifetime) - self.derived_key_permitted_algorithm = self.KeyDerivationAlgorithm.from_tag( - derived_key_permitted_algorithm - ) - self.derived_key_lifecycle = self.LifeCycle.from_tag(derived_key_lifecycle) - - self.input_peer_public_key_digest = input_peer_public_key_digest - self.input_user_fixed_info_digest = input_user_fixed_info_digest - self.derived_key_usage.clear() - for tag in self.DerivedKeyUsage.tags(): - if tag & derived_key_usage: - self.derived_key_usage.append(self.DerivedKeyUsage.from_tag(tag)) - - def validate(self) -> None: - """Validate general message properties.""" - super().validate() - if self.tag != self.TAG: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid tag: {self.tag}" - ) - if self.version != self.version: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid verssion: {self.version}" - ) - if self.reserved != RESERVED: - raise SPSDKValueError( - f"Message Key store reprovisioning enable request: Invalid reserved field: {self.reserved}" - ) - - def __str__(self) -> str: - ret = super().__str__() - ret += f" KeyStore ID value: 0x{self.key_store_id:08X}, {self.key_store_id}\n" - ret += f" Key exchange algorithm value: {self.key_exchange_algorithm.label}\n" - ret += f" Salt flags value: 0x{self.salt_flags:08X}, {self.salt_flags}\n" - ret += f" Derived key group value: 0x{self.derived_key_grp:08X}, {self.derived_key_grp}\n" - ret += f" Derived key bit size value: 0x{self.derived_key_size_bits:08X}, {self.derived_key_size_bits}\n" - ret += f" Derived key type value: {self.derived_key_type.label}\n" - ret += f" Derived key life time value: {self.derived_key_lifetime.label}\n" - ret += ( - f" Derived key usage value: {[x.label for x in self.derived_key_usage]}\n" - ) - ret += f" Derived key permitted algorithm value: {self.derived_key_permitted_algorithm.label}\n" - ret += f" Derived key life cycle value: {self.derived_key_lifecycle.label}\n" - ret += f" Derived key ID value: 0x{self.derived_key_id:08X}, {self.derived_key_id}\n" - ret += f" Private key ID value: 0x{self.private_key_id:08X}, {self.private_key_id}\n" - ret += f" Input peer public key digest value: {self.input_peer_public_key_digest.hex()}\n" - ret += f" Input user public fixed info digest value: {self.input_peer_public_key_digest.hex()}\n" - return ret - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "Message": - """Converts the configuration option into an message object. - - "config" content of container configurations. - - :param config: Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :raises SPSDKError: Invalid configuration detected. - :return: Message object. - """ - command = config.get("command", {}) - if not isinstance(command, dict) or len(command) != 1: - raise SPSDKError(f"Invalid config field command: {command}") - command_name = list(command.keys())[0] - if MessageCommands.from_label(command_name) != MessageKeyExchange.TAG: - raise SPSDKError("Invalid configuration forKey Exchange Request command.") - - cert_ver, permission, issue_date, uuid = Message.load_from_config_generic( - config - ) - - key_exchange = command.get("KEY_EXCHANGE_REQ") - assert isinstance(key_exchange, dict) - - key_store_id = value_to_int(key_exchange.get("key_store_id", 0)) - key_exchange_algorithm = MessageKeyExchange.KeyExchangeAlgorithm.from_attr( - key_exchange.get("key_exchange_algorithm", "HKDF SHA256") - ) - salt_flags = value_to_int(key_exchange.get("salt_flags", 0)) - derived_key_grp = value_to_int(key_exchange.get("derived_key_grp", 0)) - derived_key_size_bits = value_to_int( - key_exchange.get("derived_key_size_bits", 128) - ) - derived_key_type = MessageKeyExchange.DerivedKeyType.from_attr( - key_exchange.get("derived_key_type", "AES SHA256") - ) - derived_key_lifetime = MessageKeyExchange.LifeTime.from_attr( - key_exchange.get("derived_key_lifetime", "PERSISTENT") - ) - derived_key_usage = [ - MessageKeyExchange.DerivedKeyUsage.from_attr(x) - for x in key_exchange.get("derived_key_usage", []) - ] - derived_key_permitted_algorithm = ( - MessageKeyExchange.KeyDerivationAlgorithm.from_attr( - key_exchange.get("derived_key_permitted_algorithm", "HKDF SHA256") - ) - ) - derived_key_lifecycle = MessageKeyExchange.LifeCycle.from_attr( - key_exchange.get("derived_key_lifecycle", "OPEN") - ) - derived_key_id = value_to_int(key_exchange.get("derived_key_id", 0)) - private_key_id = value_to_int(key_exchange.get("private_key_id", 0)) - input_peer_public_key_digest = load_hex_string( - source=key_exchange.get("input_peer_public_key_digest", bytes(32)), - expected_size=32, - search_paths=search_paths, - ) - input_user_fixed_info_digest = load_hex_string( - source=key_exchange.get("input_user_fixed_info_digest", bytes(32)), - expected_size=32, - search_paths=search_paths, - ) - - return MessageKeyExchange( - cert_ver=cert_ver, - permissions=permission, - issue_date=issue_date, - unique_id=uuid, - key_store_id=key_store_id, - key_exchange_algorithm=key_exchange_algorithm, - salt_flags=salt_flags, - derived_key_grp=derived_key_grp, - derived_key_size_bits=derived_key_size_bits, - derived_key_type=derived_key_type, - derived_key_lifetime=derived_key_lifetime, - derived_key_usage=derived_key_usage, - derived_key_permitted_algorithm=derived_key_permitted_algorithm, - derived_key_lifecycle=derived_key_lifecycle, - derived_key_id=derived_key_id, - private_key_id=private_key_id, - input_peer_public_key_digest=input_peer_public_key_digest, - input_user_fixed_info_digest=input_user_fixed_info_digest, - ) - - def create_config(self) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :return: Configuration dictionary. - """ - cfg = self._create_general_config() - key_exchange_cfg: Dict[str, Any] = {} - cmd_cfg = {} - key_exchange_cfg["key_store_id"] = f"0x{self.key_store_id:08X}" - key_exchange_cfg["key_exchange_algorithm"] = self.key_exchange_algorithm.label - key_exchange_cfg["salt_flags"] = f"0x{self.salt_flags:08X}" - key_exchange_cfg["derived_key_grp"] = self.derived_key_grp - key_exchange_cfg["derived_key_size_bits"] = self.derived_key_size_bits - key_exchange_cfg["derived_key_type"] = self.derived_key_type.label - key_exchange_cfg["derived_key_lifetime"] = self.derived_key_lifetime.label - key_exchange_cfg["derived_key_usage"] = [ - x.label for x in self.derived_key_usage - ] - key_exchange_cfg[ - "derived_key_permitted_algorithm" - ] = self.derived_key_permitted_algorithm.label - key_exchange_cfg["derived_key_lifecycle"] = self.derived_key_lifecycle.label - key_exchange_cfg["derived_key_id"] = self.derived_key_id - key_exchange_cfg["private_key_id"] = self.private_key_id - key_exchange_cfg[ - "input_peer_public_key_digest" - ] = self.input_peer_public_key_digest.hex() - key_exchange_cfg["input_user_fixed_info_digest"] = ( - self.input_user_fixed_info_digest.hex() - if self.input_user_fixed_info_digest - else bytes(32).hex() - ) - - cmd_cfg[MessageCommands.get_label(self.TAG)] = key_exchange_cfg - cfg["command"] = cmd_cfg - - return cfg - - -class SignedMessage(AHABContainerBase): - """Class representing the Signed message. - - Signed Message:: - - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Tag | Length (MSB) | Length (LSB) | Version | - +-----+--------------+--------------+----------------+----------------+ - |0x04 | Flags | - +-----+--------------+--------------+---------------------------------+ - |0x08 | Reserved | Fuse version | Software version | - +-----+--------------+--------------+---------------------------------+ - |0x10 | Message descriptor | - +-----+---------------------------------------------------------------+ - |0x34 | Message header | - +-----+---------------------------------------------------------------+ - |0x44 | Message payload | - +-----+---------------------------------------------------------------+ - |0xXX | Signature Block | - +-----+---------------------------------------------------------------+ - - Message descriptor:: - +-----+--------------+--------------+----------------+----------------+ - |Off | Byte 3 | Byte 2 | Byte 1 | Byte 0 | - +-----+--------------+--------------+----------------+----------------+ - |0x00 | Reserved | Flags | - +-----+----------------------------------------------+----------------+ - |0x04 | IV (256 bits) | - +-----+---------------------------------------------------------------+ - - """ - - TAG = SignedMessageTags.SIGNED_MSG.tag - ENCRYPT_IV_LEN = 32 - - def __init__( - self, - flags: int = 0, - fuse_version: int = 0, - sw_version: int = 0, - message: Optional[Message] = None, - signature_block: Optional[SignatureBlock] = None, - encrypt_iv: Optional[bytes] = None, - ): - """Class object initializer. - - :param flags: flags. - :param fuse_version: value must be equal to or greater than the version - stored in the fuses to allow loading this container. - :param sw_version: used by PHBC (Privileged Host Boot Companion) to select - between multiple images with same fuse version field. - :param message: Message command to be signed. - :param signature_block: signature block. - :param encrypt_iv: Encryption Initial Vector - if defined the encryption is used. - """ - super().__init__( - flags=flags, - fuse_version=fuse_version, - sw_version=sw_version, - signature_block=signature_block, - ) - self.message = message - self.encrypt_iv = encrypt_iv - - def __eq__(self, other: object) -> bool: - if isinstance(other, SignedMessage): - if super().__eq__(other) and self.message == other.message: - return True - - return False - - def __repr__(self) -> str: - return f"Signed Message, {'Encrypted' if self.encrypt_iv else 'Plain'}" - - def __str__(self) -> str: - return ( - f" Flags: {hex(self.flags)}\n" - f" Fuse version: {hex(self.fuse_version)}\n" - f" SW version: {hex(self.sw_version)}\n" - f" Signature Block:\n{str(self.signature_block)}\n" - f" Message:\n{str(self.message)}\n" - f" Encryption IV: {self.encrypt_iv.hex() if self.encrypt_iv else 'Not Available'}" - ) - - @property - def _signature_block_offset(self) -> int: - """Returns current signature block offset. - - :return: Offset in bytes of Signature block. - """ - # Constant size of Container header + Image array Entry table - assert self.message - return calcsize(self.format()) + len(self.message) - - def __len__(self) -> int: - """Get total length of AHAB container. - - :return: Size in bytes of Message. - """ - return self._signature_block_offset + len(self.signature_block) - - @classmethod - def format(cls) -> str: - """Format of binary representation.""" - return ( - super().format() - + UINT8 # Descriptor Flags - + UINT8 # Reserved - + UINT16 # Reserved - + "32s" # IV - Initial Vector if encryption is enabled - ) - - def update_fields(self) -> None: - """Updates all volatile information in whole container structure. - - :raises SPSDKError: When inconsistent image array length is detected. - """ - # 0. Update length - self.length = len(self) - # 1. Update the signature block to get overall size of it - self.signature_block.update_fields() - # 2. Sign the image header - if self.flag_srk_set != "none": - assert self.signature_block.signature - self.signature_block.signature.sign(self.get_signature_data()) - - def _export(self) -> bytes: - """Export raw data without updates fields into bytes. - - :return: bytes representing container header content including the signature block. - """ - signed_message = pack( - self.format(), - self.version, - len(self), - self.tag, - self.flags, - self.sw_version, - self.fuse_version, - RESERVED, - self._signature_block_offset, - RESERVED, # Reserved field - 1 if self.encrypt_iv else 0, - RESERVED, - RESERVED, - self.encrypt_iv if self.encrypt_iv else bytes(32), - ) - # Add Message Header + Message Payload - assert self.message - signed_message += self.message.export() - # Add Signature Block - signed_message += align_block( - self.signature_block.export(), CONTAINER_ALIGNMENT - ) - return signed_message - - def export(self) -> bytes: - """Export the signed image into one chunk. - - :raises SPSDKValueError: if the number of images doesn't correspond the the number of - entries in image array info. - :return: images exported into single binary - """ - self.update_fields() - self.validate({}) - return self._export() - - def validate(self, data: Dict[str, Any]) -> None: - """Validate object data. - - :param data: Additional validation data. - :raises SPSDKValueError: Invalid any value of Image Array entry - """ - data["flag_used_srk_id"] = self.flag_used_srk_id - - if self.length != len(self): - raise SPSDKValueError( - f"Container Header: Invalid block length: {self.length} != {len(self)}" - ) - super().validate(data) - if self.encrypt_iv and len(self.encrypt_iv) != self.ENCRYPT_IV_LEN: - raise SPSDKValueError( - "Signed Message: Invalid Encryption initialization vector length: " - f"{len(self.encrypt_iv)*8} Bits != {self.ENCRYPT_IV_LEN * 8} Bits" - ) - if self.message is None: - raise SPSDKValueError("Signed Message: Invalid Message payload.") - self.message.validate() - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse input binary to the signed message object. - - :param data: Binary data with Container block to parse. - :return: Object recreated from the binary data. - """ - SignedMessage.check_container_head(data) - image_format = SignedMessage.format() - ( - _, # version - _, # container_length - _, # tag - flags, - sw_version, - fuse_version, - _, # number_of_images - signature_block_offset, - _, # reserved - descriptor_flags, - _, # reserved - _, # reserved - iv, - ) = unpack(image_format, data[: SignedMessage.fixed_length()]) - - parsed_signed_msg = cls( - flags=flags, - fuse_version=fuse_version, - sw_version=sw_version, - encrypt_iv=iv if bool(descriptor_flags & 0x01) else None, - ) - parsed_signed_msg.signature_block = SignatureBlock.parse( - data[signature_block_offset:] - ) - - # Parse also Message itself - parsed_signed_msg.message = Message.parse( - data[SignedMessage.fixed_length() : signature_block_offset] - ) - return parsed_signed_msg - - def create_config(self, data_path: str) -> Dict[str, Any]: - """Create configuration of the Signed Message. - - :param data_path: Path to store the data files of configuration. - :return: Configuration dictionary. - """ - self.validate({}) - cfg = self._create_config(0, data_path) - cfg["family"] = "N/A" - cfg["revision"] = "N/A" - cfg["output"] = "N/A" - - assert self.message - cfg["message"] = self.message.create_config() - - return cfg - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "SignedMessage": - """Converts the configuration option into an Signed message object. - - "config" content of container configurations. - - :param config: Signed Message configuration dictionaries. - :param search_paths: List of paths where to search for the file, defaults to None - :return: Message object. - """ - signed_msg = SignedMessage() - signed_msg.search_paths = search_paths or [] - AHABContainerBase.load_from_config_generic(signed_msg, config) - - message = config.get("message") - assert isinstance(message, dict) - - signed_msg.message = Message.load_from_config( - message, search_paths=search_paths - ) - - return signed_msg - - def image_info(self) -> BinaryImage: - """Get Image info object. - - :return: Signed Message Info object. - """ - self.validate({}) - assert self.message - ret = BinaryImage( - name="Signed Message", - size=len(self), - offset=0, - binary=self.export(), - description=( - f"Signed Message for {MessageCommands.get_label(self.message.TAG)}" - ), - ) - return ret - - @staticmethod - def get_validation_schemas() -> List[Dict[str, Any]]: - """Get list of validation schemas. - - :return: Validation list of schemas. - """ - sch = DatabaseManager().db.get_schema_file(DatabaseManager.SIGNED_MSG) - sch["properties"]["family"]["enum"] = AHABImage.get_supported_families() - return [sch] - - @staticmethod - def generate_config_template( - family: str, message: Optional[MessageCommands] = None - ) -> Dict[str, Any]: - """Generate AHAB configuration template. - - :param family: Family for which the template should be generated. - :param message: Generate the template just for one message type, if not used , its generated for all messages - :return: Dictionary of individual templates (key is name of template, value is template itself). - """ - val_schemas = SignedMessage.get_validation_schemas() - val_schemas[0]["properties"]["family"]["template_value"] = family - - if family not in AHABImage.get_supported_families(): - raise SPSDKValueError( - f"Unsupported value for family: {family} not in {AHABImage.get_supported_families()}" - ) - - if message: - for cmd_sch in val_schemas[0]["properties"]["message"]["properties"][ - "command" - ]["oneOf"]: - cmd_sch["skip_in_template"] = bool( - message.label not in cmd_sch["properties"] - ) - - yaml_data = CommentedConfig( - f"Signed message Configuration template for {family}.", val_schemas - ).get_template() - - return {f"{family}_signed_msg": yaml_data} diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py deleted file mode 100644 index e7044b36..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/ahab/utils.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""AHAB utils module.""" -import logging -from typing import Optional - -from ...apps.utils.utils import SPSDKError -from ...image.ahab.ahab_container import ( - AHABContainerBase, - AHABImage, - Blob, - SignatureBlock, -) -from ...utils.database import DatabaseManager, get_db -from ...utils.misc import load_binary - -logger = logging.getLogger(__name__) - - -def ahab_update_keyblob( - family: str, - binary: str, - keyblob: str, - container_id: int, - mem_type: Optional[str], -) -> None: - """Update keyblob in AHAB image. - - :param family: MCU family - :param binary: Path to AHAB image binary - :param keyblob: Path to keyblob - :param container_id: Index of the container to be updated - :param mem_type: Memory type used for bootable image - :raises SPSDKError: In case the container id not present - :raises SPSDKError: In case the AHAB image does not contain blob - :raises SPSDKError: In case the length of keyblobs don't match - """ - DATA_READ = 0x2000 - offset = 0 - if mem_type: - database = get_db(family) - offset = database.get_dict( - DatabaseManager.BOOTABLE_IMAGE, ["mem_types", mem_type, "segments"] - )["ahab_container"] - - keyblob_data = load_binary(keyblob) - image = AHABImage(family) - - try: - address = image.ahab_address_map[container_id] - except IndexError as exc: - raise SPSDKError(f"No container ID {container_id}") from exc - - with open(binary, "r+b") as f: - logger.debug( - f"Trying to find AHAB container header at offset {hex(address + offset)}" - ) - f.seek(address + offset) - data = f.read(DATA_READ) - ( - _, - _, - _, - _, - signature_block_offset, - ) = AHABContainerBase._parse(data) - f.seek(signature_block_offset + address + offset) - signature_block = SignatureBlock.parse(f.read(DATA_READ)) - blob = Blob.parse(keyblob_data) - blob.validate() - signature_block.update_fields() - signature_block.validate({}) - if not signature_block.blob: - raise SPSDKError("AHAB Container must contain BLOB in order to update it") - if not len(signature_block.blob.export()) == len(blob.export()): - raise SPSDKError("The size of the BLOB must be same") - logger.debug(f"AHAB container found at offset {hex(address + offset)}") - logger.debug(f"New keyblob: \n{blob}") - logger.debug(f"Old keyblob: \n{signature_block.blob}") - f.seek(signature_block_offset + address + signature_block._blob_offset + offset) - f.write(blob.export()) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py deleted file mode 100644 index d168dfdd..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/header.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2017-2018 Martin Olejar -# Copyright 2019-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Header.""" - -from struct import calcsize, pack, unpack_from -from typing import Optional, Union - -from typing_extensions import Self - -from ..exceptions import SPSDKError, SPSDKParsingError -from ..utils.abstract import BaseClass -from ..utils.spsdk_enum import SpsdkEnum - -######################################################################################################################## -# Enums -######################################################################################################################## - - -class SegTag(SpsdkEnum): - """Segments Tag.""" - - XMCD = (0xC0, "XMCD", "External Memory Configuration Data") - DCD = (0xD2, "DCD", "Device Configuration Data") - CSF = (0xD4, "CSF", "Command Sequence File Data") - # i.MX6, i.MX7, i.MX8M - IVT2 = (0xD1, "IVT2", "Image Vector Table (Version 2)") - CRT = (0xD7, "CRT", "Certificate") - SIG = (0xD8, "SIG", "Signature") - EVT = (0xDB, "EVT", "Event") - RVT = (0xDD, "RVT", "ROM Vector Table") - WRP = (0x81, "WRP", "Wrapped Key") - MAC = (0xAC, "MAC", "Message Authentication Code") - # i.MX8QXP_A0, i.MX8QM_A0 - IVT3 = (0xDE, "IVT3", "Image Vector Table (Version 3)") - # i.MX8QXP_B0, i.MX8QM_B0 - BIC1 = (0x87, "BIC1", "Boot Images Container") - SIGB = (0x90, "SIGB", "Signature block") - - -class CmdTag(SpsdkEnum): - """CSF/DCD Command Tag.""" - - SET = (0xB1, "SET", "Set") - INS_KEY = (0xBE, "INS_KEY", "Install Key") - AUT_DAT = (0xCA, "AUT_DAT", "Authenticate Data") - WRT_DAT = (0xCC, "WRT_DAT", "Write Data") - CHK_DAT = (0xCF, "CHK_DAT", "Check Data") - NOP = (0xC0, "NOP", "No Operation (NOP)") - INIT = (0xB4, "INIT", "Initialize") - UNLK = (0xB2, "UNLK", "Unlock") - - -######################################################################################################################## -# Classes -######################################################################################################################## - - -class Header(BaseClass): - """Header element type.""" - - FORMAT = ">BHB" - SIZE = calcsize(FORMAT) - - @property - def size(self) -> int: - """Header size in bytes.""" - return self.SIZE - - def __init__( - self, tag: int = 0, param: int = 0, length: Optional[int] = None - ) -> None: - """Constructor. - - :param tag: section tag - :param param: TODO - :param length: length of the segment or command; if not specified, size of the header is used - :raises SPSDKError: If invalid length - """ - self._tag = tag - self.param: int = param - self.length: int = self.SIZE if length is None else length - if self.SIZE > self.length or self.length >= 65536: - raise SPSDKError("Invalid length") - - @property - def tag(self) -> int: - """:return: section tag: command tag or segment tag, ...""" - return self._tag - - @property - def tag_name(self) -> str: - """Returns the header's tag name.""" - return SegTag.get_label(self.tag) - - def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}({self.tag_name}, {self.param}, {self.length})" - ) - - def __str__(self) -> str: - return ( - f"{self.__class__.__name__} " - ) - - def export(self) -> bytes: - """Binary representation of the header.""" - return pack(self.FORMAT, self.tag, self.length, self.param) - - @classmethod - def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: - """Parse header. - - :param data: Raw data as bytes or bytearray - :param required_tag: Check header TAG if specified value or ignore if is None - :return: Header object - :raises SPSDKParsingError: if required header tag does not match - """ - tag, length, param = unpack_from(cls.FORMAT, data) - if required_tag is not None and tag != required_tag: - raise SPSDKParsingError( - f" Invalid header tag: '0x{tag:02X}' expected '0x{required_tag:02X}' " - ) - - return cls(tag, param, length) - - -class CmdHeader(Header): - """Command header.""" - - def __init__( - self, tag: Union[CmdTag, int], param: int = 0, length: Optional[int] = None - ) -> None: - """Constructor. - - :param tag: command tag - :param param: TODO - :param length: of the command binary section, in bytes - :raises SPSDKError: If invalid command tag - """ - tag = tag.tag if isinstance(tag, CmdTag) else tag - super().__init__(tag, param, length) - if tag not in CmdTag.tags(): - raise SPSDKError("Invalid command tag") - - @property - def tag(self) -> int: - """Command tag.""" - return self._tag - - @classmethod - def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: - """Create Header from binary data. - - :param data: binary data to convert into header - :param required_tag: CmdTag, None if not required - :return: parsed instance - :raises SPSDKParsingError: If required header tag does not match - :raises SPSDKError: If invalid tag - """ - if required_tag is not None: - if required_tag not in CmdTag.tags(): - raise SPSDKError("Invalid tag") - return super(CmdHeader, cls).parse(data, required_tag) - - -class Header2(Header): - """Header element type.""" - - FORMAT = " bytes: - """Binary representation of the header.""" - return pack(self.FORMAT, self.param, self.length, self.tag) - - @classmethod - def parse(cls, data: bytes, required_tag: Optional[int] = None) -> Self: - """Parse header. - - :param data: Raw data as bytes or bytearray - :param required_tag: Check header TAG if specified value or ignore if is None - :raises SPSDKParsingError: Raises an error if required tag is empty or not valid - :return: Header2 object - """ - param, length, tag = unpack_from(cls.FORMAT, data) - if required_tag is not None and tag != required_tag: - raise SPSDKParsingError( - f" Invalid header tag: '0x{tag:02X}' expected '0x{required_tag:02X}' " - ) - - return cls(tag, param, length) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py deleted file mode 100644 index d592c340..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/misc.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2017-2018 Martin Olejar -# Copyright 2019-2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Misc.""" -import io -from io import SEEK_CUR -from typing import Optional, Union - -from ..exceptions import SPSDKError -from ..utils.registers import value_to_int -from .header import Header - - -class RawDataException(SPSDKError): - """Raw data read failed.""" - - -class StreamReadFailed(RawDataException): - """Read_raw_data could not read stream.""" - - -class NotEnoughBytesException(RawDataException): - """Read_raw_data could not read enough data.""" - - -def hexdump_fmt(data: bytes, tab: int = 4, length: int = 16, sep: str = ":") -> str: - """Dump some potentially larger data in hex.""" - text = " " * tab - for i, j in enumerate(data): - text += f"{j:02x}{sep}" - if ((i + 1) % length) == 0: - text += "\n" + " " * tab - return text - - -def modulus_fmt(modulus: bytes, tab: int = 4, length: int = 15, sep: str = ":") -> str: - """Modulus format.""" - return hexdump_fmt(b"\0" + modulus, tab, length, sep) - - -def read_raw_data( - stream: Union[io.BufferedReader, io.BytesIO], - length: int, - index: Optional[int] = None, - no_seek: bool = False, -) -> bytes: - """Read raw data.""" - if index is not None: - if index < 0: - raise SPSDKError(f" Index must be non-negative, found {index}") - if index != stream.tell(): - stream.seek(index) - - if length < 0: - raise SPSDKError(f" Length must be non-negative, found {length}") - - try: - data = stream.read(length) - except Exception as exc: - raise StreamReadFailed( - f" stream.read() failed, requested {length} bytes" - ) from exc - - if len(data) != length: - raise NotEnoughBytesException( - f" Could not read enough bytes, expected {length}, found {len(data)}" - ) - - if no_seek: - stream.seek(-length, SEEK_CUR) - - return data - - -def read_raw_segment( - buffer: Union[io.BufferedReader, io.BytesIO], - segment_tag: int, - index: Optional[int] = None, -) -> bytes: - """Read raw segment.""" - hrdata = read_raw_data(buffer, Header.SIZE, index) - length = Header.parse(hrdata, segment_tag).length - Header.SIZE - return hrdata + read_raw_data(buffer, length) - - -def dict_diff(main: dict, mod: dict) -> dict: - """Return a difference between two dictionaries if key is not present in main, it's skipped.""" - diff = {} - for key, value in mod.items(): - if isinstance(value, dict): - sub = dict_diff(main[key], value) - if sub: - diff[key] = sub - else: - if key not in main: - continue - main_value = main[key] if isinstance(main, dict) else main - try: - if value_to_int(main_value) != value_to_int(value): - diff[key] = value - except (SPSDKError, TypeError): - # Not a number! - if main_value != value: - diff[key] = value - return diff diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py b/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py deleted file mode 100644 index 11e78bba..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/image/secret.py +++ /dev/null @@ -1,951 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2017-2018 Martin Olejar -# Copyright 2019-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Commands and responses used by SDP module.""" -import math -from hashlib import sha256 -from struct import pack, unpack, unpack_from -from typing import Any, Iterator, List, Optional, Union - -from typing_extensions import Self - -from ..crypto.certificate import Certificate, ExtensionNotFound -from ..crypto.keys import EccCurve, PublicKeyEcc, PublicKeyRsa, get_ecc_curve -from ..crypto.types import SPSDKKeyUsage -from ..exceptions import SPSDKError -from ..utils.abstract import BaseClass -from ..utils.misc import Endianness -from ..utils.spsdk_enum import SpsdkEnum -from .header import Header, SegTag -from .misc import hexdump_fmt, modulus_fmt - - -class EnumAlgorithm(SpsdkEnum): - """Algorithm types.""" - - ANY = (0x00, "ANY", "Algorithm type ANY") - HASH = (0x01, "HASH", "Hash algorithm type") - SIG = (0x02, "SIG", "Signature algorithm type") - F = (0x03, "F", "Finite field arithmetic") - EC = (0x04, "EC", "Elliptic curve arithmetic") - CIPHER = (0x05, "CIPHER", "Cipher algorithm type") - MODE = (0x06, "MODE", "Cipher/hash modes") - WRAP = (0x07, "WRAP", "Key wrap algorithm type") - # Hash algorithms - SHA1 = (0x11, "SHA1", "SHA-1 algorithm ID") - SHA256 = (0x17, "SHA256", "SHA-256 algorithm ID") - SHA512 = (0x1B, "SHA512", "SHA-512 algorithm ID") - # Signature algorithms - PKCS1 = (0x21, "PKCS1", "PKCS#1 RSA signature algorithm") - ECDSA = (0x27, "ECDSA", "NIST ECDSA signature algorithm") - # Cipher algorithms - AES = (0x55, "AES", "AES algorithm ID") - # Cipher or hash modes - CCM = (0x66, "CCM", "Counter with CBC-MAC") - # Key wrap algorithms - BLOB = (0x71, "BLOB", "SHW-specific key wrap") - - -class EnumSRK(SpsdkEnum): - """Entry type in the System Root Key Table.""" - - KEY_PUBLIC = (0xE1, "KEY_PUBLIC", "Public key type: data present") - KEY_HASH = (0xEE, "KEY_HASH", "Any key: hash only") - - -class BaseSecretClass(BaseClass): - """Base SPSDK class.""" - - def __init__(self, tag: SegTag, version: int = 0x40): - """Constructor. - - :param tag: section TAG - :param version: format version - """ - self._header = Header(tag=tag.tag, param=version) - - @property - def version(self) -> int: - """Format version.""" - return self._header.param - - @property - def version_major(self) -> int: - """Major format version.""" - return self.version >> 4 - - @property - def version_minor(self) -> int: - """Minor format version.""" - return self.version & 0xF - - @property - def size(self) -> int: - """Size of the exported binary data. - - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - -class SecretKeyBlob: - """Secret Key Blob.""" - - @property - def blob(self) -> bytes: - """Data of Secret Key Blob.""" - return self._data - - @blob.setter - def blob(self, value: Union[bytes, bytearray]) -> None: - assert isinstance(value, (bytes, bytearray)) - self._data = value - - @property - def size(self) -> int: - """Size of Secret Key Blob.""" - return len(self._data) + 4 - - def __init__(self, mode: int, algorithm: int, flag: int) -> None: - """Initialize Secret Key Blob.""" - self.mode = mode - self.algorithm = algorithm - self.flag = flag - self._data = bytearray() - - def __eq__(self, obj: Any) -> bool: - return isinstance(obj, SecretKeyBlob) and vars(obj) == vars(self) - - def __ne__(self, obj: Any) -> bool: - return not self.__eq__(obj) - - def __repr__(self) -> str: - return ( - f"SecKeyBlob " - ) - - def __str__(self) -> str: - """String representation of the Secret Key Blob.""" - msg = "-" * 60 + "\n" - msg += "SecKeyBlob\n" - msg += "-" * 60 + "\n" - msg += f"Mode: {self.mode}\n" - msg += f"Algorithm: {self.algorithm}\n" - msg += f"Flag: 0x{self.flag:02X}\n" - msg += f"Size: {len(self._data)} Bytes\n" - return msg - - def export(self) -> bytes: - """Export of Secret Key Blob.""" - raw_data = pack("4B", self.mode, self.algorithm, self.size, self.flag) - raw_data += bytes(self._data) - return raw_data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse of Secret Key Blob.""" - (mode, alg, size, flg) = unpack_from("4B", data) - obj = cls(mode, alg, flg) - obj.blob = data[4 : 4 + size] - return obj - - -class CertificateImg(BaseSecretClass): - """Certificate structure for bootable image.""" - - @property - def size(self) -> int: - """Size of Certificate structure for bootable image.""" - return Header.SIZE + len(self._data) - - def __init__(self, version: int = 0x40, data: Optional[bytes] = None) -> None: - """Initialize the certificate structure for bootable image.""" - super().__init__(SegTag.CRT, version) - self._data = bytearray() if data is None else bytearray(data) - - def __len__(self) -> int: - return len(self._data) - - def __getitem__(self, key: int) -> int: - return self._data[key] - - def __setitem__(self, key: int, value: int) -> None: - self._data[key] = value - - def __iter__(self) -> Iterator[int]: - return self._data.__iter__() - - def __repr__(self) -> str: - return f"Certificate " - - def __str__(self) -> str: - """String representation of the CertificateImg.""" - msg = "-" * 60 + "\n" - msg += ( - f"Certificate (Ver: {self.version >> 4:X}.{self.version & 0xF:X}, " - f"Size: {len(self._data)})\n" - ) - msg += "-" * 60 + "\n" - return msg - - def export(self) -> bytes: - """Export.""" - self._header.length = self.size - raw_data = self._header.export() - raw_data += self._data - return raw_data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse.""" - header = Header.parse(data, SegTag.CRT.tag) - return cls(header.param, data[Header.SIZE : header.length]) - - -class Signature(BaseSecretClass): - """Class representing a signature.""" - - @property - def size(self) -> int: - """Size of a signature.""" - return Header.SIZE + len(self._data) - - def __init__(self, version: int = 0x40, data: Optional[bytes] = None) -> None: - """Initialize the signature.""" - super().__init__(tag=SegTag.SIG, version=version) - self._data = bytearray() if data is None else bytearray(data) - - def __len__(self) -> int: - return len(self._data) - - def __getitem__(self, key: int) -> int: - return self._data[key] - - def __setitem__(self, key: int, value: int) -> None: - self._data[key] = value - - def __iter__(self) -> Iterator[int]: - return self._data.__iter__() - - def __repr__(self) -> str: - return f"Signature > 4}.{self.version & 0xF}, Size: {len(self._data)}>" - - def __str__(self) -> str: - """String representation of the signature.""" - msg = "-" * 60 + "\n" - msg += f"Signature (Ver: {self.version >> 4:X}.{self.version & 0xF:X}, Size: {len(self._data)})\n" - msg += "-" * 60 + "\n" - return msg - - @property - def data(self) -> bytes: - """Signature data.""" - return bytes(self._data) - - @data.setter - def data(self, value: Union[bytes, bytearray]) -> None: - """Signature data.""" - self._data = bytearray(value) - - def export(self) -> bytes: - """Export.""" - self._header.length = self.size - raw_data = self._header.export() - raw_data += self.data - return raw_data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse.""" - header = Header.parse(data, SegTag.SIG.tag) - return cls(header.param, data[Header.SIZE : header.length]) - - -class MAC(BaseSecretClass): - """Structure that holds initial parameter for AES encryption/decryption. - - - nonce - initialization vector for AEAD AES128 decryption - - mac - message authentication code to verify the decryption was successful - """ - - # AES block size in bytes; This also match size of the MAC and - AES128_BLK_LEN = 16 - - def __init__( - self, - version: int = 0x40, - nonce_len: int = 0, - mac_len: int = AES128_BLK_LEN, - data: Optional[bytes] = None, - ): - """Constructor. - - :param version: format version, should be 0x4x - :param nonce_len: number of NONCE bytes - :param mac_len: number of MAC bytes - :param data: nonce and mac bytes joined together - """ - super().__init__(tag=SegTag.MAC, version=version) - self.nonce_len = nonce_len - self.mac_len = mac_len - self._data: bytes = bytes() if data is None else bytes(data) - if data: - self._validate_data() - - @property - def size(self) -> int: - """Size of binary representation in bytes.""" - return Header.SIZE + 4 + self.nonce_len + self.mac_len - - def _validate_data(self) -> None: - """Validates the data. - - :raises SPSDKError: If data length does not match with parameters - """ - if len(self.data) != self.nonce_len + self.mac_len: - raise SPSDKError( - f"length of data ({len(self.data)}) does not match with " - f"nonce_bytes({self.nonce_len})+mac_bytes({self.mac_len})" - ) - - @property - def data(self) -> bytes: - """NONCE and MAC bytes joined together.""" - return self._data - - @data.setter - def data(self, value: bytes) -> None: - """Setter. - - :param value: NONCE and MAC bytes joined together - """ - self._data = value - self._validate_data() - - @property - def nonce(self) -> bytes: - """NONCE bytes for the encryption/decryption.""" - self._validate_data() - return self._data[0 : self.nonce_len] - - @property - def mac(self) -> bytes: - """MAC bytes for the encryption/decryption.""" - self._validate_data() - return self._data[self.nonce_len : self.nonce_len + self.mac_len] - - def update_aead_encryption_params(self, nonce: bytes, mac: bytes) -> None: - """Update AEAD encryption parameters for encrypted image. - - :param nonce: initialization vector, length depends on image size, - :param mac: message authentication code used to authenticate decrypted data, 16 bytes - :raises SPSDKError: If incorrect length of mac - :raises SPSDKError: If incorrect length of nonce - :raises SPSDKError: If incorrect number of MAC bytes" - """ - if len(mac) != MAC.AES128_BLK_LEN: - raise SPSDKError("Incorrect length of mac") - if len(nonce) < 11 or len(nonce) > 13: - raise SPSDKError("Incorrect length of nonce") - self.nonce_len = len(nonce) - if self.mac_len != MAC.AES128_BLK_LEN: - raise SPSDKError("Incorrect number of MAC bytes") - self.data = nonce + mac - - def __len__(self) -> int: - return len(self._data) - - def __repr__(self) -> str: - return ( - f"MAC " - ) - - def __str__(self) -> str: - """Text info about the instance.""" - msg = "-" * 60 + "\n" - msg += f"MAC (Version: {self.version >> 4:X}.{self.version & 0xF:X})\n" - msg += "-" * 60 + "\n" - msg += f"Nonce Len: {self.nonce_len} Bytes\n" - msg += f"MAC Len: {self.mac_len} Bytes\n" - msg += f"[{self._data.hex()}]\n" - return msg - - def export(self) -> bytes: - """Export instance into binary form (serialization). - - :return: binary form - """ - self._validate_data() - self._header.length = self.size - raw_data = self._header.export() - raw_data += pack(">4B", 0, self.nonce_len, 0, self.mac_len) - raw_data += self.data - return raw_data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse binary data and creates the instance (deserialization). - - :param data: being parsed - :return: the instance - """ - header = Header.parse(data, SegTag.MAC.tag) - (_, nonce_bytes, _, mac_bytes) = unpack_from(">4B", data, Header.SIZE) - return cls( - header.param, - nonce_bytes, - mac_bytes, - data[Header.SIZE + 4 : header.length], - ) - - -class SRKException(SPSDKError): - """SRK table processing exceptions.""" - - -class NotImplementedSRKPublicKeyType(SRKException): - """This SRK public key algorithm is not yet implemented.""" - - -class NotImplementedSRKCertificate(SRKException): - """This SRK public key algorithm is not yet implemented.""" - - -class NotImplementedSRKItem(SRKException): - """This type of SRK table item is not implemented.""" - - -class SrkItem: - """Base class for items in the SRK Table, see `SrkTable` class. - - We do not inherit from BaseClass because our header parameter - is an algorithm identifier, not a version number. - """ - - def __eq__(self, other: Any) -> bool: - return isinstance(other, self.__class__) and vars(other) == vars(self) - - def __ne__(self, obj: Any) -> bool: - return not self.__eq__(obj) - - @property - def size(self) -> int: - """Size of the exported binary data. - - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - def __str__(self) -> str: - """Description about the instance. - - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - def sha256(self) -> bytes: - """Export SHA256 hash of the original data. - - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - def hashed_entry(self) -> "SrkItem": - """This SRK item should be replaced with an incomplete entry with its digest. - - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - def export(self) -> bytes: - """Serialization to binary form. - - :return: binary representation of the instance - :raises NotImplementedError: Derived class has to implement this method - """ - raise NotImplementedError("Derived class has to implement this method.") - - @classmethod - def parse(cls, data: bytes) -> Self: - """Pick up the right implementation of an SRK item. - - :param data: The bytes array of SRK segment - :return: SrkItem: One of the SrkItem subclasses - :raises NotImplementedSRKPublicKeyType: Unsupported key algorithm - :raises NotImplementedSRKItem: Unsupported tag - """ - header = Header.parse(data) - if header.tag == EnumSRK.KEY_PUBLIC: - if header.param == EnumAlgorithm.PKCS1: - return SrkItemRSA.parse(data) # type: ignore - elif header.param == EnumAlgorithm.ECDSA: - return SrkItemEcc.parse(data) # type: ignore - raise NotImplementedSRKPublicKeyType(f"{header.param}") - if header.tag == EnumSRK.KEY_HASH: - return SrkItemHash.parse(data) # type: ignore - raise NotImplementedSRKItem(f"TAG = {header.tag}, PARAM = {header.param}") - - @classmethod - def from_certificate(cls, cert: Certificate) -> "SrkItem": - """Pick up the right implementation of an SRK item.""" - assert isinstance(cert, Certificate) - try: - return SrkItemRSA.from_certificate(cert) - except SPSDKError: - pass - try: - return SrkItemEcc.from_certificate(cert) - except SPSDKError: - pass - raise NotImplementedSRKCertificate() - - -class SrkItemHash(SrkItem): - """Hashed stub of some public key. - - This is a valid entry of the SRK table, it represents - some public key of unknown algorithm. - Can only provide its hashed value of itself. - """ - - @property - def algorithm(self) -> int: - """Hashing algorithm used.""" - return self._header.param - - @property - def size(self) -> int: - """Size of an SRK item.""" - return self._header.length - - def __init__(self, algorithm: int, digest: bytes) -> None: - """Build the stub entry with public key hash only. - - :param algorithm: int: Hash algorithm, only SHA256 now - :param digest: bytes: Hash digest value - :raises SPSDKError: If incorrect algorithm - """ - if algorithm != EnumAlgorithm.SHA256: - raise SPSDKError("Incorrect algorithm") - self._header = Header(tag=EnumSRK.KEY_HASH.tag, param=algorithm) - self.digest = digest - self._header.length += len(digest) - - def __repr__(self) -> str: - return f"SRK Hash " - - def __str__(self) -> str: - """String representation of SrkItemHash.""" - msg = str() - msg += f"Hash algorithm: {EnumAlgorithm.from_tag(self._header.param)}\n" - msg += "Hash value:\n" - msg += hexdump_fmt(self.digest) - return msg - - def sha256(self) -> bytes: - """Export SHA256 hash of the original data.""" - return self.digest - - def hashed_entry(self) -> "SrkItemHash": - """This SRK item should be replaced with an incomplete entry with its digest.""" - return self - - def export(self) -> bytes: - """Export.""" - data = self._header.export() - data += self.digest - return data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse SRK table item data. - - :param data: The bytes array of SRK segment - :return: SrkItemHash: SrkItemHash object - :raises NotImplementedSRKItem: Unknown tag - """ - header = Header.parse(data, EnumSRK.KEY_HASH.tag) - rest = data[header.SIZE :] - if header.param == EnumAlgorithm.SHA256: - digest = rest[: sha256().digest_size] - return cls(EnumAlgorithm.SHA256.tag, digest) - raise NotImplementedSRKItem(f"TAG = {header.tag}, PARAM = {header.param}") - - -class SrkItemRSA(SrkItem): - """RSA public key in SRK Table, see `SrkTable` class.""" - - @property - def algorithm(self) -> int: - """Algorithm.""" - return self._header.param - - @property - def size(self) -> int: - """Size of an SRK item.""" - return self._header.length - - @property - def flag(self) -> int: - """Flag.""" - return self._flag - - @flag.setter - def flag(self, value: int) -> None: - if value not in (0, 0x80): - raise SPSDKError("Incorrect flag") - self._flag = value - - @property - def key_length(self) -> int: - """Key length of Item in SRK Table.""" - return len(self.modulus) * 8 - - def __init__(self, modulus: bytes, exponent: bytes, flag: int = 0) -> None: - """Initialize the srk table item.""" - assert isinstance(modulus, bytes) - assert isinstance(exponent, bytes) - self._header = Header(tag=EnumSRK.KEY_PUBLIC.tag, param=EnumAlgorithm.PKCS1.tag) - self.flag = flag - self.modulus = modulus - self.exponent = exponent - self._header.length += 8 + len(self.modulus) + len(self.exponent) - - def __repr__(self) -> str: - return ( - f"SRK " - ) - - def __str__(self) -> str: - """String representation of SrkItemRSA.""" - exp = int.from_bytes(self.exponent, Endianness.BIG.value) - return ( - f"Algorithm: {EnumAlgorithm.from_tag(self.algorithm)}\n" - f"Flag: 0x{self.flag:02X} {'(CA)' if self.flag == 0x80 else ''}\n" - f"Length: {self.key_length} bit\n" - "Modulus:\n" - f"{modulus_fmt(self.modulus)}\n" - f"Exponent: {exp} (0x{exp:X})\n" - ) - - def sha256(self) -> bytes: - """Export SHA256 hash of the data.""" - srk_data = self.export() - return sha256(srk_data).digest() - - def hashed_entry(self) -> "SrkItemHash": - """This SRK item should be replaced with an incomplete entry with its digest.""" - return SrkItemHash(EnumAlgorithm.SHA256.tag, self.sha256()) - - def export(self) -> bytes: - """Export.""" - data = self._header.export() - data += pack(">4B2H", 0, 0, 0, self.flag, len(self.modulus), len(self.exponent)) - data += bytes(self.modulus) - data += bytes(self.exponent) - return data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse SRK table item data. - - :param data: The bytes array of SRK segment - :return: SrkItemRSA: SrkItemRSA object - """ - Header.parse(data, EnumSRK.KEY_PUBLIC.tag) - (flag, modulus_len, exponent_len) = unpack_from(">B2H", data, Header.SIZE + 3) - offset = 5 + Header.SIZE + 3 - modulus = data[offset : offset + modulus_len] - offset += modulus_len - exponent = data[offset : offset + exponent_len] - return cls(modulus, exponent, flag) - - @classmethod - def from_certificate(cls, cert: Certificate) -> "SrkItemRSA": - """Create SRKItemRSA from certificate.""" - assert isinstance(cert, Certificate) - flag = 0 - try: - key_usage = cert.extensions.get_extension_for_class(SPSDKKeyUsage) - assert isinstance(key_usage.value, SPSDKKeyUsage) - if key_usage.value.key_cert_sign: - flag = 0x80 - except ExtensionNotFound: - pass - try: - public_key = cert.get_public_key() - if not isinstance(public_key, PublicKeyRsa): - raise SPSDKError("Not an RSA key") - # get modulus and exponent of public key since we are RSA - modulus_len = math.ceil(public_key.n.bit_length() / 8) - exponent_len = math.ceil(public_key.e.bit_length() / 8) - modulus = public_key.n.to_bytes(modulus_len, Endianness.BIG.value) - exponent = public_key.e.to_bytes(exponent_len, Endianness.BIG.value) - - return cls(modulus, exponent, flag) - except SPSDKError as exc: - raise NotImplementedSRKCertificate() from exc - - -class SrkItemEcc(SrkItem): - """ECC public key in SRK Table, see `SrkTable` class.""" - - ECC_KEY_TYPE = { - EccCurve.SECP256R1: 0x4B, - EccCurve.SECP384R1: 0x4D, - EccCurve.SECP521R1: 0x4E, - } - - @property - def algorithm(self) -> int: - """Algorithm.""" - return self._header.param - - @property - def size(self) -> int: - """Size of an SRK item.""" - return self._header.length - - @property - def flag(self) -> int: - """Flag.""" - return self._flag - - @flag.setter - def flag(self, value: int) -> None: - # Check - if value not in (0, 0x80): - raise SPSDKError("Incorrect flag") - self._flag = value - - def __init__( - self, key_size: int, x_coordinate: int, y_coordinate: int, flag: int = 0 - ) -> None: - """Initialize the srk table item.""" - self._header = Header(tag=EnumSRK.KEY_PUBLIC.tag, param=EnumAlgorithm.ECDSA.tag) - self.x_coordinate = x_coordinate - self.y_coordinate = y_coordinate - self.key_size = key_size - self.coordinate_size = math.ceil(key_size / 8) - self.flag = flag - self._header.length += ( - 8 - + len( - self.x_coordinate.to_bytes( - self.coordinate_size, byteorder=Endianness.BIG.value - ) - ) - + len( - self.y_coordinate.to_bytes( - self.coordinate_size, byteorder=Endianness.BIG.value - ) - ) - ) - - def __repr__(self) -> str: - return ( - f"SRK " - ) - - def __str__(self) -> str: - """String representation of SrkItemEcc.""" - return ( - f"Algorithm: {EnumAlgorithm.from_tag(self.algorithm)}\n" - f"Flag: 0x{self.flag:02X} {'(CA)' if self.flag == 0x80 else ''}\n" - f"Key size: {self.key_size} bit\n" - f"X coordinate: {self.x_coordinate}\n" - f"Y coordinate: {self.y_coordinate}\n" - ) - - def sha256(self) -> bytes: - """Export SHA256 hash of the data.""" - srk_data = self.export() - return sha256(srk_data).digest() - - def hashed_entry(self) -> "SrkItemHash": - """This SRK item should be replaced with an incomplete entry with its digest.""" - return SrkItemHash(EnumAlgorithm.SHA256.tag, self.sha256()) - - def export(self) -> bytes: - """Export.""" - data = self._header.export() - curve_id = self.ECC_KEY_TYPE[get_ecc_curve(self.key_size // 8)] - data += pack( - ">8B", - 0, - 0, - 0, - self.flag, - curve_id, - 0, - self.key_size >> 8 & 0xFF, - self.key_size & 0xFF, - ) - data += self.x_coordinate.to_bytes( - self.coordinate_size, byteorder=Endianness.BIG.value - ) - data += self.y_coordinate.to_bytes( - self.coordinate_size, byteorder=Endianness.BIG.value - ) - return data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse SRK table item data. - - :param data: The bytes array of SRK segment - :return: SrkItemEcc: SrkItemEcc object - """ - Header.parse(data, EnumSRK.KEY_PUBLIC.tag) - (flag, curve_id, _, key_size) = unpack_from(">3BH", data, Header.SIZE + 3) - if curve_id not in list(cls.ECC_KEY_TYPE.values()): - raise SPSDKError(f"Unknown curve with id {curve_id}") - offset = 5 + Header.SIZE + 3 - coordinate_size = math.ceil(key_size / 8) - x_coordinate = data[offset : offset + coordinate_size] - offset += coordinate_size - y_coordinate = data[offset : offset + coordinate_size] - return cls( - key_size, - int.from_bytes(x_coordinate, Endianness.BIG.value), - int.from_bytes(y_coordinate, Endianness.BIG.value), - flag, - ) - - @classmethod - def from_certificate(cls, cert: Certificate) -> "SrkItemEcc": - """Create SrkItemEcc from certificate.""" - flag = 0 - try: - key_usage = cert.extensions.get_extension_for_class(SPSDKKeyUsage) - assert isinstance(key_usage.value, SPSDKKeyUsage) - if key_usage.value.key_cert_sign: - flag = 0x80 - except ExtensionNotFound: - pass - - try: - public_key = cert.get_public_key() - if not isinstance(public_key, PublicKeyEcc): - raise SPSDKError("Not an ECC key") - return cls(public_key.key_size, public_key.x, public_key.y, flag) - except SPSDKError as exc: - raise NotImplementedSRKCertificate() from exc - - -class SrkTable(BaseSecretClass): - """SRK table.""" - - @property - def size(self) -> int: - """Size of SRK table.""" - size = Header.SIZE - for key in self._keys: - size += key.size - return size - - def __init__(self, version: int = 0x40) -> None: - """Initialize SRT Table. - - :param version: format version - """ - super().__init__(tag=SegTag.CRT, version=version) - self._keys: List[SrkItem] = [] - - def __len__(self) -> int: - return len(self._keys) - - def __getitem__(self, key: int) -> SrkItem: - return self._keys[key] - - def __setitem__(self, key: int, value: SrkItem) -> None: - assert isinstance(value, SrkItem) - self._keys[key] = value - - def __iter__(self) -> Iterator[SrkItem]: - return self._keys.__iter__() - - def __repr__(self) -> str: - return ( - f"SRK_Table " - ) - - def __str__(self) -> str: - """Text info about the instance.""" - msg = "-" * 60 + "\n" - msg += ( - f"SRK Table (Version: {self.version_major:X}.{self.version_minor:X}, " - f"#Keys: {len(self._keys)})\n" - ) - msg += "-" * 60 + "\n" - for i, srk in enumerate(self._keys): - msg += f"SRK Key Index: {i} \n" - msg += str(srk) - msg += "\n" - return msg - - def append(self, srk: SrkItem) -> None: - """Add SRK item. - - :param srk: item to be added - """ - self._keys.append(srk) - - def get_fuse(self, index: int) -> int: - """Retrieve fuse value for the given index. - - :param index: of the fuse, 0-7 - :return: value of the specified fuse; the value is in format, that cane be used as parameter for SDP - `efuse_read_once` or `efuse_write_once` - :raises SPSDKError: If incorrect index of the fuse - :raises SPSDKError: If incorrect length of SRK items - """ - if index < 0 or index >= 8: - raise SPSDKError("Incorrect index of the fuse") - int_data = self.export_fuses()[index * 4 : (1 + index) * 4] - if len(int_data) != 4: - raise SPSDKError("Incorrect length of SRK items") - return unpack(" bytes: - """SRK items in binary form, see `SRK_fuses.bin` file.""" - data = b"" - for srk in self._keys: - data += srk.sha256() - return sha256(data).digest() - - def export(self) -> bytes: - """Export into binary form (serialization). - - :return: binary representation of the instance - """ - self._header.length = self.size - raw_data = self._header.export() - for srk in self._keys: - raw_data += srk.export() - return raw_data - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse of SRK table.""" - header = Header.parse(data, SegTag.CRT.tag) - offset = Header.SIZE - obj = cls(header.param) - obj._header.length = header.length # pylint: disable=protected-access - length = header.length - Header.SIZE - while length > 0: - srk = SrkItem.parse(data[offset:]) - offset += srk.size - length -= srk.size - obj.append(srk) - return obj diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py index f6af88e5..b74e48fb 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/__init__.py @@ -7,22 +7,4 @@ # SPDX-License-Identifier: BSD-3-Clause """Module implementing communication with the MCU Bootloader.""" - -from typing import Union - -from .interfaces.buspal import MbootBuspalI2CInterface, MbootBuspalSPIInterface -from .interfaces.sdio import MbootSdioInterface -from .interfaces.uart import MbootUARTInterface -from .interfaces.usb import MbootUSBInterface -from .interfaces.usbsio import MbootUsbSioI2CInterface, MbootUsbSioSPIInterface -from .mcuboot import McuBoot - -MbootDeviceTypes = Union[ - MbootBuspalI2CInterface, - MbootBuspalSPIInterface, - MbootSdioInterface, - MbootUARTInterface, - MbootUSBInterface, - MbootUsbSioI2CInterface, - MbootUsbSioSPIInterface, -] +# diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py deleted file mode 100644 index 92a7e93b..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/buspal.py +++ /dev/null @@ -1,566 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2020-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Buspal Mboot device implementation.""" -import datetime -import logging -import struct -import time -from dataclasses import dataclass -from enum import Enum -from typing import Any, Dict, List, Optional, Tuple - -from serial import SerialException -from serial.tools.list_ports import comports -from typing_extensions import Self - -from ...exceptions import SPSDKError -from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from ...mboot.protocol.serial_protocol import FPType, MbootSerialProtocol, to_int -from ...utils.interfaces.device.serial_device import SerialDevice - -logger = logging.getLogger(__name__) - - -@dataclass -class ScanArgs: - """Scan arguments dataclass.""" - - port: Optional[str] - props: Optional[List[str]] - - @classmethod - def parse(cls, params: str, extra_params: Optional[str] = None) -> Self: - """Parse given scanning parameters and extra parameters into ScanArgs class. - - :param params: Parameters as a string - :param extra_params: Optional extra parameters as a string - """ - props = [] - if extra_params: - props = extra_params.split(",") - target = props.pop(0) - if target not in ["spi", "i2c"]: - raise SPSDKError(f"Target must be either 'spi' or 'ic2', not {target}") - port_parts = params.split(",") - return cls(port=port_parts.pop(0), props=props) - - -class SpiModeCommand(Enum): - """Spi mode commands.""" - - exit = 0x00 # 00000000 - Exit to bit bang mode - version = 0x01 # 00000001 - Enter raw SPI mode, display version string - chip_select = 0x02 # 0000001x - CS high (1) or low (0) - sniff = 0x0C # 000011XX - Sniff SPI traffic when CS low(10)/all(01) - bulk_transfer = ( - 0x10 # 0001xxxx - Bulk SPI transfer, send/read 1-16 bytes (0=1byte!) - ) - config_periph = ( - 0x40 # 0100wxyz - Configure peripherals w=power, x=pull-ups, y=AUX, z=CS - ) - set_speed = 0x60 # 01100xxx - SPI speed - config_spi = ( - 0x80 # 1000wxyz - SPI config, w=HiZ/3.3v, x=CKP idle, y=CKE edge, z=SMP sample - ) - write_then_read = 0x04 # 00000100 - Write then read extended command - - -# pylint: disable=invalid-name -class SpiConfigShift(Enum): - """Spi configuration shifts for the mask.""" - - direction = 0 - phase = 1 - polarity = 2 - - -# pylint: disable=invalid-name -class SpiClockPolarity(Enum): - """SPI clock polarity configuration.""" - - active_high = 0 # Active-high SPI clock (idles low). - active_low = 1 # Active-low SPI clock (idles high). - - -# pylint: disable=invalid-name -class SpiClockPhase(Enum): - """SPI clock phase configuration.""" - - # First edge on SPSCK occurs at the middle of the first cycle of a data transfer. - first_edge = 0 - # First edge on SPSCK occurs at the start of the first cycle of a data transfer. - second_edge = 1 - - -# pylint: disable=invalid-name -class SpiShiftDirection(Enum): - """SPI clock phase configuration.""" - - msb_first = 0 # Data transfers start with most significant bit. - lsb_first = 1 # Data transfers start with least significant bit. - - -class SpiConfiguration: - """Dataclass to store SPI configuration.""" - - speed: int - polarity: SpiClockPolarity - phase: SpiClockPhase - direction: SpiShiftDirection - - -# pylint: disable=invalid-name -class BBConstants(Enum): - """Constants.""" - - reset_count = 20 # Max number of nulls to send to enter BBIO mode - response_ok = 0x01 # Successful command response - bulk_transfer_max = 4096 # Max number of bytes per bulk transfer - packet_timeout_ms = 10 # Packet timeout in milliseconds - - -class Response(str, Enum): - """Response to enter bit bang mode.""" - - BITBANG = "BBIO1" - SPI = "SPI1" - I2C = "I2C1" - - -class BuspalMode(Enum): - """Bit Bang mode command.""" - - RESET = 0x00 # Reset, responds "BBIO1" - SPI = 0x01 # Enter binary SPI mode, responds "SPI1" - I2C = 0x02 # Enter binary I2C mode, responds "I2C1" - - -MODE_COMMANDS_RESPONSES = { - BuspalMode.RESET: Response.BITBANG, - BuspalMode.SPI: Response.SPI, - BuspalMode.I2C: Response.I2C, -} - - -class MbootBuspalProtocol(MbootSerialProtocol): - """Mboot Serial protocol.""" - - default_baudrate = 57600 - default_timeout = 5000 - device: SerialDevice - mode: BuspalMode - - def __init__(self, device: SerialDevice) -> None: - """Initialize the MbootBuspalProtocol object. - - :param device: The device instance - """ - super().__init__(device) - - def open(self) -> None: - """Open the interface.""" - self.device.open() - # reset first, send bit-bang command - self._enter_mode(BuspalMode.RESET) - logger.debug("Entered BB mode") - self._enter_mode(self.mode) - - @classmethod - def scan( - cls, - port: Optional[str] = None, - props: Optional[List[str]] = None, - timeout: Optional[int] = None, - ) -> List[SerialDevice]: - """Scan connected serial ports and set BUSPAL properties. - - Returns list of serial ports with devices that respond to BUSPAL communication protocol. - If 'port' is specified, only that serial port is checked - If no devices are found, return an empty list. - - :param port: name of preferred serial port, defaults to None - :param timeout: timeout in milliseconds - :param props: buspal target properties - :return: list of available interfaces - """ - timeout = timeout or cls.default_timeout - if port: - device = cls._check_port_buspal(port, timeout, props) - devices = [device] if device else [] - else: - all_ports = [ - cls._check_port_buspal(comport.device, timeout, props) - for comport in comports(include_links=True) - ] - devices = list(filter(None, all_ports)) - return devices - - @classmethod - def _check_port_buspal( - cls, port: str, timeout: int, props: Optional[List[str]] = None - ) -> Optional[SerialDevice]: - """Check if device on comport 'port' can connect using BUSPAL communication protocol. - - :param port: name of port to check - :param timeout: timeout in milliseconds - :param props: buspal settings - :return: None if device doesn't respond to PING, instance of Interface if it does - """ - props = props if props is not None else [] - try: - device = SerialDevice( - port=port, timeout=timeout, baudrate=cls.default_baudrate - ) - interface = cls(device) - interface.open() - interface._configure(props) - interface._ping() - return device - except (AssertionError, SerialException, McuBootConnectionError) as e: - logger.error(str(e)) - return None - - def _send_frame(self, frame: bytes, wait_for_ack: bool = True) -> None: - """Send frame method to be implemented by child class.""" - raise NotImplementedError() - - def _read(self, size: int, timeout: Optional[int] = None) -> bytes: - """Implementation done by child class.""" - raise NotImplementedError() - - def _configure(self, props: List[str]) -> None: - """Configure the BUSPAL interface. - - :param props: buspal settings - """ - raise NotImplementedError() - - def _enter_mode(self, mode: BuspalMode) -> None: - """Enter BUSPAL mode. - - :param mode: buspal mode - """ - response = MODE_COMMANDS_RESPONSES[mode] - self._send_command_check_response( - bytes([mode.value]), bytes(response.value.encode("utf-8")) - ) - - def _send_command_check_response(self, command: bytes, response: bytes) -> None: - """Send a command and check if expected response is received. - - :param command: command to send - :param response: expected response - """ - self.device.write(command) - data_recvd = self.device.read(len(response)) - format_received = " ".join(hex(x) for x in data_recvd) - format_expected = " ".join(hex(x) for x in response) - assert ( - format_received == format_expected - ), f"Received data '{format_received}' but expected '{format_expected}'" - - def _read_frame_header( - self, expected_frame_type: Optional[FPType] = None - ) -> Tuple[int, int]: - """Read frame header and frame type. Return them as tuple of integers. - - :param expected_frame_type: Check if the frame_type is exactly as expected - :return: Tuple of integers representing frame header and frame type - :raises AssertionError: Unexpected frame header or frame type (if specified) - :raises McuBootDataAbortError: Abort frame received - """ - header = None - time_start = datetime.datetime.now() - time_end = time_start + datetime.timedelta(milliseconds=self.device.timeout) - - # read uart until start byte is equal to FRAME_START_BYTE, max. 'retry_count' times - while header != self.FRAME_START_BYTE and datetime.datetime.now() < time_end: - header = to_int(self._read(1)) - if header == FPType.ABORT: - raise McuBootDataAbortError() - if header != self.FRAME_START_BYTE: - time.sleep(BBConstants.packet_timeout_ms.value / 1000) - assert ( - header == self.FRAME_START_BYTE - ), f"Received invalid frame header '{header:#X}' expected '{self.FRAME_START_BYTE:#X}'" - - frame_type = to_int(self._read(1)) - - if frame_type == FPType.ABORT: - raise McuBootDataAbortError() - return header, frame_type - - -class MbootBuspalSPIInterface(MbootBuspalProtocol): - """BUSPAL SPI interface.""" - - TARGET_SETTINGS = ["speed", "polarity", "phase", "direction"] - - HDR_FRAME_RETRY_CNT = 3 - ACK_WAIT_DELAY = 0.01 # in seconds - device: SerialDevice - identifier = "buspal_spi" - - def __init__(self, device: SerialDevice): - """Initialize the BUSPAL SPI interface. - - :param port: name of the serial port, defaults to None - :param timeout: read/write timeout in milliseconds - """ - self.mode = BuspalMode.SPI - super().__init__(device) - - @classmethod - def scan_from_args( - cls, - params: str, - timeout: int, - extra_params: Optional[str] = None, - ) -> List[Self]: - """Scan connected Buspal devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Timeout for the scan - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params, extra_params) - devices = cls.scan(port=scan_args.port, props=scan_args.props, timeout=timeout) - interfaces = [] - for device in devices: - interfaces.append(cls(device)) - return interfaces - - def _configure(self, props: List[str]) -> None: - """Configure the BUSPAL SPI interface. - - :param props: buspal settings - """ - spi_props: Dict[str, Any] = dict(zip(self.TARGET_SETTINGS, props)) - - speed = int(spi_props.get("speed", 100)) - polarity = SpiClockPolarity( - spi_props.get("polarity", SpiClockPolarity.active_low) - ) - phase = SpiClockPhase(spi_props.get("phase", SpiClockPhase.second_edge)) - direction = SpiShiftDirection( - spi_props.get("direction", SpiShiftDirection.msb_first) - ) - - # set SPI config - logger.debug("Set SPI config") - spi_data = polarity.value << SpiConfigShift.polarity.value - spi_data |= phase.value << SpiConfigShift.phase.value - spi_data |= direction.value << SpiConfigShift.direction.value - spi_data |= SpiModeCommand.config_spi.value - self._send_command_check_response( - bytes([spi_data]), bytes([BBConstants.response_ok.value]) - ) - - # set SPI speed - logger.debug(f"Set SPI speed to {speed}bps") - spi_speed = struct.pack(" None: - """Send data to BUSPAL I2C device. - - :param data: Data to send - """ - self._send_frame_retry(data, wait_for_ack, self.HDR_FRAME_RETRY_CNT) - - def _send_frame_retry( - self, - data: bytes, - wait_for_ack: bool = True, - retry_cnt: int = HDR_FRAME_RETRY_CNT, - ) -> None: - """Send a frame to BUSPAL SPI device. - - :param data: Data to send - :param wait_for_ack: Wait for ACK frame from device, defaults to True - :param retry_cnt: Number of retry in case the header frame is incorrect - :raises AssertionError: Unexpected frame header or frame type (if specified) - """ - size = min(len(data), BBConstants.bulk_transfer_max.value) - command = struct.pack(" 0: - logger.error( - f"{error} (retry {self.HDR_FRAME_RETRY_CNT-retry_cnt+1}/{self.HDR_FRAME_RETRY_CNT})" - ) - retry_cnt -= 1 - self._send_frame_retry(data, wait_for_ack, retry_cnt) - else: - raise SPSDKError( - "Failed retrying reading the SPI header frame" - ) from error - - def _read(self, size: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount of bytes from BUSPAL SPI device. - - :return: Data read from the device - """ - size = min(size, BBConstants.bulk_transfer_max.value) - command = struct.pack(" List[Self]: - """Scan connected Buspal devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Timeout for the scan - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params, extra_params) - devices = cls.scan(port=scan_args.port, props=scan_args.props, timeout=timeout) - interfaces = [] - for device in devices: - interfaces.append(cls(device)) - return interfaces - - def _configure(self, props: List[str]) -> None: - """Initialize the BUSPAL I2C interface. - - :param props: buspal settings - """ - i2c_props: Dict[str, Any] = dict(zip(self.TARGET_SETTINGS, props)) - - # get I2C configuration values, use default values if settings are not defined in input string) - speed = int(i2c_props.get("speed", 100)) - address = int(i2c_props.get("address", 0x10)) - - # set I2C address - logger.debug(f"Set I2C address to {address}") - i2c_data = struct.pack(" None: - """Send data to BUSPAL I2C device. - - :param data: Data to send - """ - self._send_frame_retry(data, wait_for_ack, self.HDR_FRAME_RETRY_CNT) - - def _send_frame_retry( - self, - data: bytes, - wait_for_ack: bool = True, - retry_cnt: int = HDR_FRAME_RETRY_CNT, - ) -> None: - """Send data to BUSPAL I2C device. - - :param data: Data to send - :param wait_for_ack: Wait for ACK frame from device, defaults to True - :param retry_cnt: Number of retry in case the header frame is incorrect - :raises AssertionError: Unexpected frame header or frame type (if specified) - """ - retry_cnt = self.HDR_FRAME_RETRY_CNT - size = min(len(data), BBConstants.bulk_transfer_max.value) - command = struct.pack(" 0: - logger.error( - f"{error} (retry {self.HDR_FRAME_RETRY_CNT-retry_cnt+1}/{self.HDR_FRAME_RETRY_CNT})" - ) - retry_cnt -= 1 - self._send_frame_retry(data, wait_for_ack, retry_cnt) - else: - raise SPSDKError( - "Failed retrying reading the I2C header frame" - ) from error - - def _read(self, size: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount of bytes from BUSPAL I2C device. - - :return: Data read from the device - """ - size = min(size, BBConstants.bulk_transfer_max.value) - command = struct.pack(" Self: - """Parse given scanning parameters into ScanArgs class. - - :param params: Parameters as a string - """ - return cls(device_path=params) - - -class MbootSdioInterface(MbootSerialProtocol): - """Sdio interface.""" - - identifier = "sdio" - device: SdioDevice - sdio_devices = SDIO_DEVICES - - def __init__(self, device: SdioDevice) -> None: - """Initialize the MbootSdioInterface object. - - :param device: The device instance - """ - super().__init__(device=device) - - @property - def name(self) -> str: - """Get the name of the device. - - :return: Name of the device. - """ - assert isinstance(self.device, SdioDevice) - for name, value in self.sdio_devices.items(): - if value[0] == self.device.vid and value[1] == self.device.pid: - return name - return "Unknown" - - @classmethod - def scan_from_args( - cls, - params: str, - timeout: int, - extra_params: Optional[str] = None, - ) -> List[Self]: - """Scan connected USB devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Interface timeout - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params) - interfaces = cls.scan(device_path=scan_args.device_path, timeout=timeout) - return interfaces - - @classmethod - def scan( - cls, - device_path: str, - timeout: Optional[int] = None, - ) -> List[Self]: - """Scan connected SDIO devices. - - :param device_path: device path string - :param timeout: Interface timeout - :return: matched SDIO device - """ - devices = SdioDevice.scan(device_path=device_path, timeout=timeout) - return [cls(device) for device in devices] - - def open(self) -> None: - """Open the interface.""" - self.device.open() - - def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: - """Read data on the IN endpoint associated to the HID interface. - - :return: Return CmdResponse object. - :raises McuBootConnectionError: Raises an error if device is not opened for reading - :raises McuBootConnectionError: Raises if device is not available - :raises McuBootDataAbortError: Raises if reading fails - :raises TimeoutError: When timeout occurs - """ - raw_data = self._read(1024) - if not raw_data: - logger.error("Cannot read from SDIO device") - raise TimeoutError() - - _, frame_type = self._parse_frame_header(raw_data) - _length, crc = struct.unpack_from(" Tuple[int, int]: - """Read frame header and frame type. Return them as tuple of integers. - - :param expected_frame_type: Check if the frame_type is exactly as expected - :return: Tuple of integers representing frame header and frame type - :raises McuBootDataAbortError: Target sens Data Abort frame - :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) - :raises McuBootConnectionError: When received invalid ACK - """ - data = self._read(2) - return self._parse_frame_header(data, FPType.ACK) - - def _parse_frame_header( - self, frame: bytes, expected_frame_type: Optional[FPType] = None - ) -> Tuple[int, int]: - """Read frame header and frame type. Return them as tuple of integers. - - :param expected_frame_type: Check if the frame_type is exactly as expected - :return: Tuple of integers representing frame header and frame type - :raises McuBootDataAbortError: Target sens Data Abort frame - :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) - :raises McuBootConnectionError: When received invalid ACK - """ - header, frame_type = struct.unpack_from(" Self: - """Parse given scanning parameters into ScanArgs class. - - :param params: Parameters as a string - """ - port_parts = params.split(",") - return cls( - port=port_parts.pop(0), - baudrate=int(port_parts.pop(), 0) if port_parts else None, - ) - - -class MbootUARTInterface(MbootSerialProtocol): - """UART interface.""" - - default_baudrate = 57600 - device: SerialDevice - identifier = "uart" - - def __init__(self, device: SerialDevice): - """Initialize the MbootUARTInterface object. - - :param device: The device instance - """ - assert isinstance(device, SerialDevice) - super().__init__(device=device) - - @classmethod - def scan_from_args( - cls, - params: str, - timeout: int, - extra_params: Optional[str] = None, - ) -> List[Self]: - """Scan connected UART devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Timeout for the scan - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params=params) - interfaces = cls.scan( - port=scan_args.port, - baudrate=scan_args.baudrate or cls.default_baudrate, - timeout=timeout, - ) - return interfaces - - @classmethod - def scan( - cls, - port: Optional[str] = None, - baudrate: Optional[int] = None, - timeout: Optional[int] = None, - ) -> List[Self]: - """Scan connected UART devices. - - Returns list of serial ports with devices that respond to PING command. - If 'port' is specified, only that serial port is checked - If no devices are found, return an empty list. - - :param port: name of preferred serial port, defaults to None - :param baudrate: speed of the UART interface, defaults to 56700 - :param timeout: timeout in milliseconds, defaults to 5000 - :return: list of interfaces responding to the PING command - """ - devices = SerialDevice.scan( - port=port, baudrate=baudrate or cls.default_baudrate, timeout=timeout - ) - interfaces = [] - for device in devices: - try: - interface = cls(device) - interface.open() - interface._ping() - interface.close() - interfaces.append(interface) - except Exception: - interface.close() - return interfaces diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py deleted file mode 100644 index 41d3123d..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/interfaces/usbsio.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright (c) 2019-2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""USBSIO Mboot interface implementation.""" -from typing import List, Optional - -from typing_extensions import Self - -from ...mboot.protocol.serial_protocol import MbootSerialProtocol -from ...utils.interfaces.device.usbsio_device import ( - ScanArgs, - UsbSioI2CDevice, - UsbSioSPIDevice, -) - - -class MbootUsbSioI2CInterface(MbootSerialProtocol): - """USBSIO I2C interface.""" - - device: UsbSioI2CDevice - identifier = "usbsio_i2c" - - def __init__(self, device: UsbSioI2CDevice): - """Initialize the UsbSioI2CDevice object. - - :param device: The device instance - """ - super().__init__(device=device) - - @classmethod - def scan_from_args( - cls, - params: str, - timeout: int, - extra_params: Optional[str] = None, - ) -> List[Self]: - """Scan connected USBSIO devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Timeout for the scan - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params=params) - interfaces = cls.scan(config=scan_args.config, timeout=timeout) - return interfaces - - @classmethod - def scan(cls, config: Optional[str] = None, timeout: int = 5000) -> List[Self]: - """Scan connected USB-SIO bridge devices. - - :param config: Configuration string identifying spi or i2c SIO interface - and could filter out USB devices - :param timeout: Read timeout in milliseconds, defaults to 5000 - :return: List of interfaces - """ - devices = UsbSioI2CDevice.scan(config, timeout) - spi_devices = [x for x in devices if isinstance(x, UsbSioI2CDevice)] - return [cls(device) for device in spi_devices] - - -class MbootUsbSioSPIInterface(MbootSerialProtocol): - """USBSIO I2C interface.""" - - # START_NOT_READY may be 0x00 or 0xFF depending on the implementation - FRAME_START_NOT_READY_LIST = [0x00, 0xFF] - device: UsbSioSPIDevice - identifier = "usbsio_spi" - - def __init__(self, device: UsbSioSPIDevice) -> None: - """Initialize the UsbSioSPIDevice object. - - :param device: The device instance - """ - super().__init__(device) - - @classmethod - def scan_from_args( - cls, - params: str, - timeout: int, - extra_params: Optional[str] = None, - ) -> List[Self]: - """Scan connected USBSIO devices. - - :param params: Params as a configuration string - :param extra_params: Extra params configuration string - :param timeout: Timeout for the scan - :return: list of matching RawHid devices - """ - scan_args = ScanArgs.parse(params=params) - interfaces = cls.scan(config=scan_args.config, timeout=timeout) - return interfaces - - @classmethod - def scan(cls, config: Optional[str] = None, timeout: int = 5000) -> List[Self]: - """Scan connected USB-SIO bridge devices. - - :param config: Configuration string identifying spi or i2c SIO interface - and could filter out USB devices - :param timeout: Read timeout in milliseconds, defaults to 5000 - :return: List of interfaces - """ - devices = UsbSioSPIDevice.scan(config, timeout) - spi_devices = [x for x in devices if isinstance(x, UsbSioSPIDevice)] - return [cls(device) for device in spi_devices] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py deleted file mode 100644 index 49385825..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/protocol/serial_protocol.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2016-2018 Martin Olejar -# Copyright 2019-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Mboot serial implementation.""" -import logging -import struct -import time -from contextlib import contextmanager -from typing import Generator, NamedTuple, Optional, Tuple, Union - -from crcmod.predefined import mkPredefinedCrcFun -from typing_extensions import Self - -from ...exceptions import SPSDKAttributeError -from ...mboot.commands import CmdResponse, parse_cmd_response -from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError -from ...mboot.protocol.base import MbootProtocolBase -from ...utils.interfaces.commands import CmdPacketBase -from ...utils.misc import Endianness, Timeout -from ...utils.spsdk_enum import SpsdkEnum - -logger = logging.getLogger(__name__) - - -class PingResponse(NamedTuple): - """Special type of response for Ping Command.""" - - version: int - options: int - crc: int - - @classmethod - def parse(cls, data: bytes) -> Self: - """Parse raw data into PingResponse object. - - :param data: bytes to be unpacked to PingResponse object - 4B version, 2B data, 2B CRC16 - :raises McuBootConnectionError: Received invalid ping response - :return: PingResponse - """ - try: - version, options, crc = struct.unpack(" int: - """Convert bytes into single integer. - - :param data: bytes to convert - :param little_endian: indicate byte ordering in data, defaults to True - :return: integer - """ - byte_order = Endianness.LITTLE if little_endian else Endianness.BIG - return int.from_bytes(data, byteorder=byte_order.value) - - -class MbootSerialProtocol(MbootProtocolBase): - """Mboot Serial protocol.""" - - FRAME_START_BYTE = 0x5A - FRAME_START_NOT_READY_LIST = [0x00] - PING_TIMEOUT_MS = 500 - MAX_PING_RESPONSE_DUMMY_BYTES = 50 - MAX_UART_OPEN_ATTEMPTS = 3 - protocol_version: int = 0 - options: int = 0 - - def open(self) -> None: - """Open the interface. - - :raises McuBootConnectionError: In any case of fail of UART open operation. - """ - for i in range(self.MAX_UART_OPEN_ATTEMPTS): - try: - self.device.open() - self._ping() - logger.debug(f"Interface opened after {i + 1} attempts.") - return - except TimeoutError as e: - # Closing may take up 30-40 seconds - self.close() - logger.debug(f"Timeout when pinging the device: {repr(e)}") - except McuBootConnectionError as e: - self.close() - logger.debug(f"Opening interface failed with: {repr(e)}") - except Exception as exc: - self.close() - raise McuBootConnectionError( - "UART Interface open operation fails." - ) from exc - raise McuBootConnectionError( - f"Cannot open UART interface after {self.MAX_UART_OPEN_ATTEMPTS} attempts." - ) - - def close(self) -> None: - """Close the interface.""" - self.device.close() - - @property - def is_opened(self) -> bool: - """Indicates whether interface is open.""" - return self.device.is_opened - - def write_data(self, data: bytes) -> None: - """Encapsulate data into frames and send them to device. - - :param data: Data to be sent - """ - frame = self._create_frame(data, FPType.DATA) - self._send_frame(frame) - - def write_command(self, packet: CmdPacketBase) -> None: - """Encapsulate command into frames and send them to device. - - :param packet: Command packet object to be sent - :raises SPSDKAttributeError: Command packed contains no data to be sent - """ - data = packet.to_bytes(padding=False) - if not data: - raise SPSDKAttributeError("Incorrect packet type") - frame = self._create_frame(data, FPType.CMD) - self._send_frame(frame) - - def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: - """Read data from device. - - :return: read data - :raises McuBootDataAbortError: Indicates data transmission abort - :raises McuBootConnectionError: When received invalid CRC - """ - _, frame_type = self._read_frame_header() - _length = to_int(self._read(2)) - crc = to_int(self._read(2)) - if not _length: - self._send_ack() - raise McuBootDataAbortError() - data = self._read(_length) - self._send_ack() - calculated_crc = self._calc_frame_crc(data, frame_type) - if crc != calculated_crc: - raise McuBootConnectionError("Received invalid CRC") - if frame_type == FPType.CMD: - return parse_cmd_response(data) - return data - - def _read(self, length: int, timeout: Optional[int] = None) -> bytes: - """Internal read, done mainly due BUSPAL, where this is overriden.""" - return self.device.read(length, timeout) - - def _send_ack(self) -> None: - """Send ACK command.""" - ack_frame = struct.pack(" None: - """Write frame to the device and wait for ack. - - :param data: Data to be send - """ - self.device.write(frame) - if wait_for_ack: - self._read_frame_header(FPType.ACK) - - def _create_frame(self, data: bytes, frame_type: FPType) -> bytes: - """Encapsulate data into frame.""" - crc = self._calc_frame_crc(data, frame_type.tag) - frame = struct.pack( - f" int: - """Calculate the CRC of a frame. - - :param data: frame data - :param frame_type: frame type - :return: calculated CRC - """ - crc_data = struct.pack( - f" int: - """Calculate CRC from the data. - - :param data: data to calculate CRC from - :return: calculated CRC - """ - crc_function = mkPredefinedCrcFun("xmodem") - return crc_function(data) - - def _read_frame_header( - self, expected_frame_type: Optional[FPType] = None - ) -> Tuple[int, int]: - """Read frame header and frame type. Return them as tuple of integers. - - :param expected_frame_type: Check if the frame_type is exactly as expected - :return: Tuple of integers representing frame header and frame type - :raises McuBootDataAbortError: Target sens Data Abort frame - :raises McuBootConnectionError: Unexpected frame header or frame type (if specified) - :raises McuBootConnectionError: When received invalid ACK - """ - assert isinstance(self.device.timeout, int) - timeout = Timeout(self.device.timeout, "ms") - while not timeout.overflow(): - header = to_int(self._read(1)) - if header not in self.FRAME_START_NOT_READY_LIST: - break - # This is workaround addressing SPI ISP issue on RT5/6xx when sometimes - # ACK frames and START BYTE frames are swapped, see SPSDK-1824 for more details - if header not in [self.FRAME_START_BYTE, FPType.ACK]: - raise McuBootConnectionError( - f"Received invalid frame header '{header:#X}' expected '{self.FRAME_START_BYTE:#X}'" - + "\nTry increasing the timeout, some operations might take longer" - ) - if header == FPType.ACK: - frame_type: int = header - else: - frame_type = to_int(self._read(1)) - if frame_type == FPType.ABORT: - raise McuBootDataAbortError() - if expected_frame_type: - if frame_type == self.FRAME_START_BYTE: - frame_type = header - if frame_type != expected_frame_type: - raise McuBootConnectionError( - f"received invalid ACK '{frame_type:#X}' expected '{expected_frame_type.tag:#X}'" - ) - return header, frame_type - - def _ping(self) -> None: - """Ping the target device, retrieve protocol version. - - :raises McuBootConnectionError: If the target device doesn't respond to ping - :raises McuBootConnectionError: If the start frame is not received - :raises McuBootConnectionError: If the header is invalid - :raises McuBootConnectionError: If the frame type is invalid - :raises McuBootConnectionError: If the ping response is not received - :raises McuBootConnectionError: If crc does not match - """ - with self.ping_timeout(timeout=self.PING_TIMEOUT_MS): - ping = struct.pack(" Generator[None, None, None]: - """Context manager for changing UART's timeout. - - :param timeout: New temporary timeout in milliseconds, defaults to PING_TIMEOUT_MS (500ms) - :return: Generator[None, None, None] - """ - assert isinstance(self.device.timeout, int) - context_timeout = min(timeout, self.device.timeout) - original_timeout = self.device.timeout - self.device.timeout = context_timeout - logger.debug(f"Setting timeout to {context_timeout} ms") - # driver needs to be reconfigured after timeout change, wait for a little while - time.sleep(0.005) - - yield - - self.device.timeout = original_timeout - logger.debug(f"Restoring timeout to {original_timeout} ms") - time.sleep(0.005) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py deleted file mode 100644 index e7bee059..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/scanner.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Helper module used for scanning the existing devices.""" -from typing import List, Optional - -from ..exceptions import SPSDKError -from ..utils.interfaces.scanner_helper import InterfaceParams, parse_plugin_config -from .protocol.base import MbootProtocolBase - - -def get_mboot_interface( - port: Optional[str] = None, - usb: Optional[str] = None, - sdio: Optional[str] = None, - buspal: Optional[str] = None, - lpcusbsio: Optional[str] = None, - plugin: Optional[str] = None, - timeout: int = 5000, -) -> MbootProtocolBase: - """Get appropriate interface. - - 'port', 'usb', 'sdio', 'lpcusbsio' parameters are mutually exclusive; one of them is required. - - :param port: name and speed of the serial port (format: name[,speed]), defaults to None - :param usb: PID,VID of the USB interface, defaults to None - :param sdio: SDIO path of the SDIO interface, defaults to None - :param buspal: buspal interface settings, defaults to None - :param timeout: timeout in milliseconds - :param lpcusbsio: LPCUSBSIO spi or i2c config string - :param plugin: Additional plugin to be used - :return: Selected interface instance - :raises SPSDKError: Only one of the appropriate interfaces must be specified - :raises SPSDKError: When SPSDK-specific error occurs - """ - # check that one and only one interface is defined - interface_params: List[InterfaceParams] = [] - plugin_params = parse_plugin_config(plugin) if plugin else ("Unknown", "") - interface_params.extend( - [ - InterfaceParams(identifier="usb", is_defined=bool(usb), params=usb), - InterfaceParams( - identifier="uart", is_defined=bool(port and not buspal), params=port - ), - InterfaceParams( - identifier="buspal_spi", - is_defined=bool(port and buspal and "spi" in buspal), - params=port, - extra_params=buspal, - ), - InterfaceParams( - identifier="buspal_i2c", - is_defined=bool(port and buspal and "i2c" in buspal), - params=port, - extra_params=buspal, - ), - InterfaceParams( - identifier="usbsio_spi", - is_defined=bool(lpcusbsio and "spi" in lpcusbsio), - params=lpcusbsio, - ), - InterfaceParams( - identifier="usbsio_i2c", - is_defined=bool(lpcusbsio and "i2c" in lpcusbsio), - params=lpcusbsio, - ), - InterfaceParams(identifier="sdio", is_defined=bool(sdio), params=sdio), - InterfaceParams( - identifier=plugin_params[0], - is_defined=bool(plugin), - params=plugin_params[1], - ), - ] - ) - interface_params = [ifce for ifce in interface_params if ifce.is_defined] - if len(interface_params) == 0: - raise SPSDKError( - "One of '--port', '--usb', '--sdio', '--lpcusbsio' or '--plugin' must be specified." - ) - if len(interface_params) > 1: - raise SPSDKError( - "Only one of '--port', '--usb', '--sdio', '--lpcusbsio' or '--plugin must be specified." - ) - interface = MbootProtocolBase.get_interface(interface_params[0].identifier) - assert interface_params[0].params - devices = interface.scan_from_args( - params=interface_params[0].params, - extra_params=interface_params[0].extra_params, - timeout=timeout, - ) - if len(devices) == 0: - raise SPSDKError( - f"Selected '{interface_params[0].identifier}' device not found." - ) - if len(devices) > 1: - raise SPSDKError( - f"Multiple '{interface_params[0].identifier}' devices found: {len(devices)}" - ) - return devices[0] diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py index b0748dc6..fc3e7abf 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/sbfile/sb2/images.py @@ -18,11 +18,7 @@ from ...crypto.hash import EnumHashAlgorithm, get_hash from ...crypto.hmac import hmac from ...crypto.rng import random_bytes -from ...crypto.signature_provider import ( - SignatureProvider, - get_signature_provider, - try_to_verify_public_key, -) +from ...crypto.signature_provider import SignatureProvider, get_signature_provider from ...crypto.symmetric import Counter, aes_key_unwrap, aes_key_wrap from ...exceptions import SPSDKError from ...sbfile.misc import SecBootBlckSize @@ -110,376 +106,6 @@ def timestamp(self) -> datetime: return self._timestamp -######################################################################################################################## -# Secure Boot Image Class (Version 2.0) -######################################################################################################################## -class BootImageV20(BaseClass): - """Boot Image V2.0 class.""" - - # Image specific data - # size of the MAC key - HEADER_MAC_SIZE = 32 - # AES encrypted DEK and MAC, including padding - DEK_MAC_SIZE = 32 + 32 + 16 - - KEY_BLOB_SIZE = 80 - - def __init__( - self, - signed: bool, - kek: bytes, - *sections: BootSectionV2, - product_version: str = "1.0.0", - component_version: str = "1.0.0", - build_number: int = 0, - advanced_params: SBV2xAdvancedParams = SBV2xAdvancedParams(), - ) -> None: - """Initialize Secure Boot Image V2.0. - - :param signed: True if image is signed, False otherwise - :param kek: key for wrapping DEK and MAC keys - :param product_version: The product version (default: 1.0.0) - :param component_version: The component version (default: 1.0.0) - :param build_number: The build number value (default: 0) - :param advanced_params: Advanced parameters for encryption of the SB file, use for tests only - :param sections: Boot sections - :raises SPSDKError: Invalid dek or mac - """ - self._kek = kek - # Set Flags value - self._signed = signed - self.signature_provider: Optional[SignatureProvider] = None - flags = 0x08 if self.signed else 0x04 - # Set private attributes - self._dek: bytes = advanced_params.dek - self._mac: bytes = advanced_params.mac - if ( - len(self._dek) != self.HEADER_MAC_SIZE - and len(self._mac) != self.HEADER_MAC_SIZE - ): # pragma: no cover # condition checked in SBV2xAdvancedParams constructor - raise SPSDKError("Invalid dek or mac") - self._header = ImageHeaderV2( - version="2.0", - product_version=product_version, - component_version=component_version, - build_number=build_number, - flags=flags, - nonce=advanced_params.nonce, - timestamp=advanced_params.timestamp, - ) - self._cert_section: Optional[CertSectionV2] = None - self._boot_sections: List[BootSectionV2] = [] - # Generate nonce - if self._header.nonce is None: - nonce = bytearray(random_bytes(16)) - # clear nonce bit at offsets 31 and 63 - nonce[9] &= 0x7F - nonce[13] &= 0x7F - self._header.nonce = bytes(nonce) - # Sections - for section in sections: - self.add_boot_section(section) - - @property - def header(self) -> ImageHeaderV2: - """Return image header.""" - return self._header - - @property - def dek(self) -> bytes: - """Data encryption key.""" - return self._dek - - @property - def mac(self) -> bytes: - """Message authentication code.""" - return self._mac - - @property - def kek(self) -> bytes: - """Return key for wrapping DEK and MAC keys.""" - return self._kek - - @property - def signed(self) -> bool: - """Check whether sb is signed + encrypted or only encrypted.""" - return self._signed - - @property - def cert_block(self) -> Optional[CertBlockV1]: - """Return certificate block; None if SB file not signed or block not assigned yet.""" - cert_sect = self._cert_section - if cert_sect is None: - return None - - return cert_sect.cert_block - - @cert_block.setter - def cert_block(self, value: Optional[CertBlockV1]) -> None: - """Setter. - - :param value: block to be assigned; None to remove previously assigned block - :raises SPSDKError: When certificate block is used when SB file is not signed - """ - if value is not None: - if not self.signed: - raise SPSDKError( - "Certificate block cannot be used unless SB file is signed" - ) - self._cert_section = CertSectionV2(value) if value else None - - @property - def cert_header_size(self) -> int: - """Return image raw size (not aligned) for certificate header.""" - size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE - for boot_section in self._boot_sections: - size += boot_section.raw_size - return size - - @property - def raw_size_without_signature(self) -> int: - """Return image raw size without signature, used to calculate image blocks.""" - # Header, HMAC and KeyBlob - size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE - # Certificates Section - if self.signed: - size += self.DEK_MAC_SIZE - cert_block = self.cert_block - if not cert_block: - raise SPSDKError("Certification block not present") - size += cert_block.raw_size - # Boot Sections - for boot_section in self._boot_sections: - size += boot_section.raw_size - return size - - @property - def raw_size(self) -> int: - """Return image raw size.""" - size = self.raw_size_without_signature - - if self.signed: - cert_block = self.cert_block - if ( - not cert_block - ): # pragma: no cover # already checked in raw_size_without_signature - raise SPSDKError("Certificate block not present") - size += cert_block.signature_size - - return size - - def __len__(self) -> int: - return len(self._boot_sections) - - def __getitem__(self, key: int) -> BootSectionV2: - return self._boot_sections[key] - - def __setitem__(self, key: int, value: BootSectionV2) -> None: - self._boot_sections[key] = value - - def __iter__(self) -> Iterator[BootSectionV2]: - return self._boot_sections.__iter__() - - def update(self) -> None: - """Update boot image.""" - if self._boot_sections: - self._header.first_boot_section_id = self._boot_sections[0].uid - # calculate first boot tag block - data_size = self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE - if self._cert_section is not None: - data_size += self._cert_section.raw_size - self._header.first_boot_tag_block = SecBootBlckSize.to_num_blocks(data_size) - # ... - self._header.flags = 0x08 if self.signed else 0x04 - self._header.image_blocks = SecBootBlckSize.to_num_blocks( - self.raw_size_without_signature - ) - self._header.header_blocks = SecBootBlckSize.to_num_blocks(self._header.SIZE) - self._header.max_section_mac_count = 0 - if self.signed: - self._header.offset_to_certificate_block = ( - self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE - ) - self._header.offset_to_certificate_block += ( - CmdHeader.SIZE + CertSectionV2.HMAC_SIZE * 2 - ) - self._header.max_section_mac_count = 1 - for boot_sect in self._boot_sections: - boot_sect.is_last = True # this is unified with elftosb - self._header.max_section_mac_count += boot_sect.hmac_count - # Update certificates block header - cert_blk = self.cert_block - if cert_blk is not None: - cert_blk.header.build_number = self._header.build_number - cert_blk.header.image_length = self.cert_header_size - - def __repr__(self) -> str: - return f"SB2.0, {'Signed' if self.signed else 'Plain'} " - - def __str__(self) -> str: - """Return text description of the instance.""" - self.update() - nfo = "\n" - nfo += ":::::::::::::::::::::::::::::::::: IMAGE HEADER ::::::::::::::::::::::::::::::::::::::\n" - nfo += str(self._header) - if self._cert_section is not None: - nfo += "::::::::::::::::::::::::::::::: CERTIFICATES BLOCK ::::::::::::::::::::::::::::::::::::\n" - nfo += str(self._cert_section) - nfo += "::::::::::::::::::::::::::::::::::: BOOT SECTIONS ::::::::::::::::::::::::::::::::::::\n" - for index, section in enumerate(self._boot_sections): - nfo += f"[ SECTION: {index} | UID: 0x{section.uid:08X} ]\n" - nfo += str(section) - return nfo - - def add_boot_section(self, section: BootSectionV2) -> None: - """Add new Boot section into image. - - :param section: Boot section - :raises SPSDKError: Raised when section is not instance of BootSectionV2 class - :raises SPSDKError: Raised when boot section has duplicate UID - """ - if not isinstance(section, BootSectionV2): - raise SPSDKError("Section is not instance of BootSectionV2 class") - duplicate_uid = find_first( - self._boot_sections, lambda bs: bs.uid == section.uid - ) - if duplicate_uid is not None: - raise SPSDKError(f"Boot section with duplicate UID: {str(section.uid)}") - self._boot_sections.append(section) - - def export(self, padding: Optional[bytes] = None) -> bytes: - """Serialize image object. - - :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) - :return: exported bytes - :raises SPSDKError: Raised when there are no boot sections or is not signed or private keys are missing - :raises SPSDKError: Raised when there is invalid dek or mac - :raises SPSDKError: Raised when certificate data is not present - :raises SPSDKError: Raised when there is invalid certificate block - :raises SPSDKError: Raised when there is invalid length of exported data - """ - if len(self.dek) != 32 or len(self.mac) != 32: - raise SPSDKError("Invalid dek or mac") - # validate params - if not self._boot_sections: - raise SPSDKError("No boot section") - if self.signed and (self._cert_section is None): - raise SPSDKError("Certificate section is required for signed images") - # update internals - self.update() - # Add Image Header data - data = self._header.export(padding=padding) - # Add Image Header HMAC data - data += hmac(self.mac, data) - # Add DEK and MAC keys - data += aes_key_wrap(self.kek, self.dek + self.mac) - # Add Padding - data += padding if padding else random_bytes(8) - # Add Certificates data - if not self._header.nonce: - raise SPSDKError("There is no nonce in the header") - counter = Counter(self._header.nonce) - counter.increment(SecBootBlckSize.to_num_blocks(len(data))) - if self._cert_section is not None: - cert_sect_bin = self._cert_section.export( - dek=self.dek, mac=self.mac, counter=counter - ) - counter.increment(SecBootBlckSize.to_num_blocks(len(cert_sect_bin))) - data += cert_sect_bin - # Add Boot Sections data - for sect in self._boot_sections: - data += sect.export(dek=self.dek, mac=self.mac, counter=counter) - # Add Signature data - if self.signed: - if self.signature_provider is None: - raise SPSDKError( - "Signature provider is not assigned, cannot sign the image." - ) - if self.cert_block is None: - raise SPSDKError("Certificate block is not assigned.") - - public_key = self.cert_block.certificates[-1].get_public_key() - try_to_verify_public_key(self.signature_provider, public_key.export()) - data += self.signature_provider.get_signature(data) - - if len(data) != self.raw_size: - raise SPSDKError("Invalid length of exported data") - return data - - # pylint: disable=too-many-locals - @classmethod - def parse(cls, data: bytes, kek: bytes = bytes()) -> Self: - """Parse image from bytes. - - :param data: Raw data of parsed image - :param kek: The Key for unwrapping DEK and MAC keys (required) - :return: parsed image object - :raises SPSDKError: raised when header is in wrong format - :raises SPSDKError: raised when there is invalid header version - :raises SPSDKError: raised when signature is incorrect - :raises SPSDKError: Raised when kek is empty - :raises SPSDKError: raised when header's nonce is not present - """ - if not kek: - raise SPSDKError("kek cannot be empty") - index = 0 - header_raw_data = data[index : index + ImageHeaderV2.SIZE] - index += ImageHeaderV2.SIZE - header_mac_data = data[index : index + cls.HEADER_MAC_SIZE] - index += cls.HEADER_MAC_SIZE - key_blob = data[index : index + cls.KEY_BLOB_SIZE] - index += cls.KEY_BLOB_SIZE - key_blob_unwrap = aes_key_unwrap(kek, key_blob[:-8]) - dek = key_blob_unwrap[:32] - mac = key_blob_unwrap[32:] - header_mac_data_calc = hmac(mac, header_raw_data) - if header_mac_data != header_mac_data_calc: - raise SPSDKError("Invalid header MAC data") - # Parse Header - header = ImageHeaderV2.parse(header_raw_data) - if header.version != "2.0": - raise SPSDKError(f"Invalid Header Version: {header.version} instead 2.0") - image_size = header.image_blocks * 16 - # Initialize counter - if not header.nonce: - raise SPSDKError("Header's nonce not present") - counter = Counter(header.nonce) - counter.increment(SecBootBlckSize.to_num_blocks(index)) - # ... - signed = header.flags == 0x08 - adv_params = SBV2xAdvancedParams( - dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp - ) - obj = cls( - signed, - kek=kek, - product_version=str(header.product_version), - component_version=str(header.component_version), - build_number=header.build_number, - advanced_params=adv_params, - ) - # Parse Certificate section - if header.flags == 0x08: - cert_sect = CertSectionV2.parse( - data, index, dek=dek, mac=mac, counter=counter - ) - obj._cert_section = cert_sect - index += cert_sect.raw_size - # Check Signature - if not cert_sect.cert_block.verify_data( - data[image_size:], data[:image_size] - ): - raise SPSDKError("Parsing Certification section failed") - # Parse Boot Sections - while index < (image_size): - boot_section = BootSectionV2.parse( - data, index, dek=dek, mac=mac, counter=counter - ) - obj.add_boot_section(boot_section) - index += boot_section.raw_size - return obj - - ######################################################################################################################## # Secure Boot Image Class (Version 2.1) ######################################################################################################################## diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py deleted file mode 100644 index 580978fc..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Uboot device.""" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py b/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py deleted file mode 100644 index 9337d045..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/uboot/uboot.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Simple Uboot serial console implementation.""" - -import logging - -from crcmod.predefined import mkPredefinedCrcFun -from hexdump import restore -from serial import Serial - -from ..exceptions import SPSDKError -from ..utils.misc import align, change_endianness, split_data - -logger = logging.getLogger(__name__) - - -class Uboot: - """Class for encapsulation of Uboot CLI interface.""" - - LINE_FEED = "\n" - ENCODING = "ascii" - READ_ALIGNMENT = 16 - DATA_BYTES_SPLIT = 4 - PROMPT = b"u-boot=> " - - def __init__( - self, port: str, timeout: int = 1, baudrate: int = 115200, crc: bool = True - ) -> None: - """Uboot constructor. - - :param port: TTY port - :param timeout: timeout in seconds, defaults to 1 - :param baudrate: baudrate, defaults to 115200 - :param crc: True if crc will be calculated, defaults to True - """ - self.port = port - self.baudrate = baudrate - self.timeout = timeout - self.is_opened = False - self.open() - self.crc = crc - - def calc_crc(self, data: bytes, address: int, count: int) -> None: - """Calculate CRC from the data. - - :param data: data to calculate CRC from - :param address: address from where the data should be calculated - :param count: count of bytes - :raises SPSDKError: Invalid CRC of data - """ - if not self.crc: - return - crc_command = f"crc32 {hex(address)} {hex(count)}" - self.write(crc_command) - hexdump_str = self.LINE_FEED.join(self.read_output().splitlines()[1:-1]) - crc_obtained = "0x" + hexdump_str[-8:] - logger.debug(f"CRC command:\n{crc_command}\n{crc_obtained}") - crc_function = mkPredefinedCrcFun("crc-32") - calculated_crc = hex(crc_function(data)) - logger.debug(f"Calculated CRC {calculated_crc}") - if calculated_crc != crc_obtained: - raise SPSDKError(f"Invalid CRC of data {calculated_crc} != {crc_obtained}") - - def open(self) -> None: - """Open uboot device.""" - self._device = Serial( - port=self.port, timeout=self.timeout, baudrate=self.baudrate - ) - self.is_opened = True - - def close(self) -> None: - """Close uboot device.""" - self._device.close() - self.is_opened = False - - def read(self, length: int) -> str: - """Read specified number of charactrs from uboot CLI. - - :param length: count of read characters - :return: encoded string - """ - output = self._device.read(length) - return output.decode(encoding=self.ENCODING) - - def read_output(self) -> str: - """Read CLI output until prompt. - - :return: ASCII encoded output - """ - return self._device.read_until(expected=self.PROMPT).decode(self.ENCODING) - - def write(self, data: str) -> None: - """Write ASCII decoded data to CLI. Append LINE FEED if not present. - - :param data: ASCII decoded data - """ - if self.LINE_FEED not in data: - data += self.LINE_FEED - data_bytes = bytes(data, encoding=self.ENCODING) - self._device.write(data_bytes) - - def read_memory(self, address: int, count: int) -> bytes: - """Read memory using the md command. Optionally calculate CRC. - - :param address: Address in memory - :param count: Count of bytes - :return: data as bytes - """ - count = align(count, self.READ_ALIGNMENT) - md_command = f"md.b {hex(address)} {hex(count)}" - self.write(md_command) - hexdump_str = self.LINE_FEED.join(self.read_output().splitlines()[1:-1]) - logger.debug(f"read_memory:\n{md_command}\n{hexdump_str}") - data = restore(hexdump_str) - self.calc_crc(data, address, count) - - return data - - def write_memory(self, address: int, data: bytes) -> None: - """Write memory and optionally calculate CRC. - - :param address: Address in memory - :param data: data as bytes - """ - start_address = address - for splitted_data in split_data(data, self.DATA_BYTES_SPLIT): - mw_command = f"mw.l {hex(address)} {change_endianness(splitted_data).hex()}" - logger.debug(f"write_memory: {mw_command}") - self.write(mw_command) - address += len(splitted_data) - self.read_output() - - self.calc_crc(data, start_address, len(data)) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py deleted file mode 100644 index e2e9789e..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/iee.py +++ /dev/null @@ -1,838 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2022-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""The module provides support for IEE for RTxxxx devices.""" - -import logging -from copy import deepcopy -from struct import pack -from typing import Any, Dict, List, Optional, Union - -from crcmod.predefined import mkPredefinedCrcFun - -from ... import version as spsdk_version -from ...apps.utils.utils import filepath_from_config -from ...crypto.rng import random_bytes -from ...crypto.symmetric import Counter, aes_ctr_encrypt, aes_xts_encrypt -from ...exceptions import SPSDKError, SPSDKValueError -from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file -from ...utils.images import BinaryImage -from ...utils.misc import ( - Endianness, - align_block, - load_hex_string, - reverse_bytes_in_longs, - split_data, - value_to_bytes, - value_to_int, -) -from ...utils.registers import Registers -from ...utils.schema_validator import CommentedConfig -from ...utils.spsdk_enum import SpsdkEnum - -logger = logging.getLogger(__name__) - - -class IeeKeyBlobLockAttributes(SpsdkEnum): - """IEE keyblob lock attributes.""" - - LOCK = (0x95, "LOCK") # IEE region lock. - UNLOCK = (0x59, "UNLOCK") # IEE region unlock. - - -class IeeKeyBlobKeyAttributes(SpsdkEnum): - """IEE keyblob key attributes.""" - - CTR128XTS256 = (0x5A, "CTR128XTS256") # AES 128 bits (CTR), 256 bits (XTS) - CTR256XTS512 = (0xA5, "CTR256XTS512") # AES 256 bits (CTR), 512 bits (XTS) - - -class IeeKeyBlobModeAttributes(SpsdkEnum): - """IEE Keyblob mode attributes.""" - - Bypass = (0x6A, "Bypass") # AES encryption/decryption bypass - AesXTS = (0xA6, "AesXTS") # AES XTS mode - AesCTRWAddress = (0x66, "AesCTRWAddress") # AES CTR w address binding mode - AesCTRWOAddress = (0xAA, "AesCTRWOAddress") # AES CTR w/o address binding mode - AesCTRkeystream = (0x19, "AesCTRkeystream") # AES CTR keystream only - - -class IeeKeyBlobWritePmsnAttributes(SpsdkEnum): - """IEE keblob write permission attributes.""" - - ENABLE = (0x99, "ENABLE") # Enable write permission in APC IEE - DISABLE = (0x11, "DISABLE") # Disable write permission in APC IEE - - -class IeeKeyBlobAttribute: - """IEE Keyblob Attribute. - - | typedef struct _iee_keyblob_attribute - | { - | uint8_t lock; # IEE Region Lock control flag. - | uint8_t keySize; # IEE AES key size. - | uint8_t aesMode; # IEE AES mode. - | uint8_t reserved; # Reserved. - | } iee_keyblob_attribute_t; - """ - - _FORMAT = " None: - """IEE keyblob constructor. - - :param lock: IeeKeyBlobLockAttributes - :param key_attribute: IeeKeyBlobKeyAttributes - :param aes_mode: IeeKeyBlobModeAttributes - """ - self.lock = lock - self.key_attribute = key_attribute - self.aes_mode = aes_mode - - @property - def ctr_mode(self) -> bool: - """Return true if AES mode is CTR. - - :return: True if AES-CTR, false otherwise - """ - if self.aes_mode in [ - IeeKeyBlobModeAttributes.AesCTRWAddress, - IeeKeyBlobModeAttributes.AesCTRWOAddress, - IeeKeyBlobModeAttributes.AesCTRkeystream, - ]: - return True - return False - - @property - def key1_size(self) -> int: - """Return IEE key size based on selected mode. - - :return: Key size in bytes - """ - if self.key_attribute == IeeKeyBlobKeyAttributes.CTR128XTS256: - return 16 - return 32 - - @property - def key2_size(self) -> int: - """Return IEE key size based on selected mode. - - :return: Key size in bytes - """ - if self.key_attribute == IeeKeyBlobKeyAttributes.CTR128XTS256: - return 16 - if self.ctr_mode: - return 16 - return 32 - - def export(self) -> bytes: - """Export binary representation of KeyBlobAttribute. - - :return: serialized binary data - """ - return pack( - self._FORMAT, self.lock.tag, self.key_attribute.tag, self.aes_mode.tag, 0 - ) - - -class IeeKeyBlob: - """IEE KeyBlob. - - | typedef struct _iee_keyblob_ - | { - | uint32_t header; # IEE Key Blob header tag. - | uint32_t version; # IEE Key Blob version, upward compatible. - | iee_keyblob_attribute_t attribute; # IEE configuration attribute. - | uint32_t pageOffset; # IEE page offset. - | uint32_t key1[IEE_MAX_AES_KEY_SIZE_IN_BYTE / - | sizeof(uint32_t)]; # Encryption key1 for XTS-AES mode, encryption key for AES-CTR mode. - | uint32_t key2[IEE_MAX_AES_KEY_SIZE_IN_BYTE / - | sizeof(uint32_t)]; # Encryption key2 for XTS-AES mode, initial counter for AES-CTR mode. - | uint32_t startAddr; # Physical address of encryption region. - | uint32_t endAddr; # Physical address of encryption region. - | uint32_t reserved; # Reserved word. - | uint32_t crc32; # Entire IEE Key Blob CRC32 value. Must be the last struct member. - | } iee_keyblob_t - """ - - _FORMAT = "LL4BL8L8LLLLL96B" - - HEADER_TAG = 0x49454542 - # Tag used in keyblob header - # (('I' << 24) | ('E' << 16) | ('E' << 8) | ('B' << 0)) - KEYBLOB_VERSION = 0x56010000 - # Identifier of IEE keyblob version - # (('V' << 24) | (1 << 16) | (0 << 8) | (0 << 0)) - KEYBLOB_OFFSET = 0x1000 - - _IEE_ENCR_BLOCK_SIZE_XTS = 0x1000 - - _ENCRYPTION_BLOCK_SIZE = 0x10 - - _START_ADDR_MASK = 0x400 - 1 - # Region addresses are modulo 1024 - - _END_ADDR_MASK = 0x3F8 - - def __init__( - self, - attributes: IeeKeyBlobAttribute, - start_addr: int, - end_addr: int, - key1: Optional[bytes] = None, - key2: Optional[bytes] = None, - page_offset: int = 0, - crc: Optional[bytes] = None, - ): - """Constructor. - - :param attributes: IEE keyblob attributes - :param start_addr: start address of the region - :param end_addr: end address of the region - :param key1: Encryption key1 for XTS-AES mode, encryption key for AES-CTR mode. - :param key2: Encryption key2 for XTS-AES mode, initial_counter for AES-CTR mode. - :param crc: optional value for unused CRC fill (for testing only); None to use calculated value - :raises SPSDKError: Start or end address are not aligned - :raises SPSDKError: When there is invalid key - :raises SPSDKError: When there is invalid start/end address - """ - self.attributes = attributes - - if key1 is None: - key1 = random_bytes(self.attributes.key1_size) - if key2 is None: - key2 = random_bytes(self.attributes.key2_size) - - key1 = value_to_bytes(key1, byte_cnt=self.attributes.key1_size) - key2 = value_to_bytes(key2, byte_cnt=self.attributes.key2_size) - - if start_addr < 0 or start_addr > end_addr or end_addr > 0xFFFFFFFF: - raise SPSDKError("Invalid start/end address") - - if (start_addr & self._START_ADDR_MASK) != 0: - raise SPSDKError( - f"Start address must be aligned to {hex(self._START_ADDR_MASK + 1)} boundary" - ) - - self.start_addr = start_addr - self.end_addr = end_addr - - self.key1 = key1 - self.key2 = key2 - self.page_offset = page_offset - - self.crc_fill = crc - - def __str__(self) -> str: - """Text info about the instance.""" - msg = "" - msg += f"KEY 1: {self.key1.hex()}\n" - msg += f"KEY 2: {self.key2.hex()}\n" - msg += f"Start Addr: {hex(self.start_addr)}\n" - msg += f"End Addr: {hex(self.end_addr)}\n" - return msg - - def plain_data(self) -> bytes: - """Plain data for selected key range. - - :return: key blob exported into binary form (serialization) - """ - result = bytes() - result += pack(" bool: - """Whether key blob contains specified address. - - :param addr: to be tested - :return: True if yes, False otherwise - """ - return self.start_addr <= addr <= self.end_addr - - def matches_range(self, image_start: int, image_end: int) -> bool: - """Whether key blob matches address range of the image to be encrypted. - - :param image_start: start address of the image - :param image_end: last address of the image - :return: True if yes, False otherwise - """ - return self.contains_addr(image_start) and self.contains_addr(image_end) - - def encrypt_image_xts(self, base_address: int, data: bytes) -> bytes: - """Encrypt specified data using AES-XTS. - - :param base_address: of the data in target memory; must be >= self.start_addr - :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr - :return: encrypted data - """ - encrypted_data = bytes() - current_start = base_address - key1 = reverse_bytes_in_longs(self.key1) - key2 = reverse_bytes_in_longs(self.key2) - - for block in split_data(bytearray(data), self._IEE_ENCR_BLOCK_SIZE_XTS): - tweak = self.calculate_tweak(current_start) - - encrypted_block = aes_xts_encrypt( - key1 + key2, - block, - tweak, - ) - encrypted_data += encrypted_block - current_start += len(block) - - return encrypted_data - - def encrypt_image_ctr(self, base_address: int, data: bytes) -> bytes: - """Encrypt specified data using AES-CTR. - - :param base_address: of the data in target memory; must be >= self.start_addr - :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr - :return: encrypted data - """ - encrypted_data = bytes() - key = reverse_bytes_in_longs(self.key1) - nonce = reverse_bytes_in_longs(self.key2) - - counter = Counter( - nonce, ctr_value=base_address >> 4, ctr_byteorder_encoding=Endianness.BIG - ) - - for block in split_data(bytearray(data), self._ENCRYPTION_BLOCK_SIZE): - encrypted_block = aes_ctr_encrypt( - key, - block, - counter.value, - ) - encrypted_data += encrypted_block - counter.increment(self._ENCRYPTION_BLOCK_SIZE >> 4) - - return encrypted_data - - def encrypt_image(self, base_address: int, data: bytes) -> bytes: - """Encrypt specified data. - - :param base_address: of the data in target memory; must be >= self.start_addr - :param data: to be encrypted (e.g. plain image); base_address + len(data) must be <= self.end_addr - :return: encrypted data - :raises SPSDKError: If start address is not valid - :raises NotImplementedError: AES-CTR is not implemented yet - """ - if base_address % 16 != 0: - raise SPSDKError( - "Invalid start address" - ) # Start address has to be 16 byte aligned - data = align_block(data, self._ENCRYPTION_BLOCK_SIZE) # align data length - data_len = len(data) - - # check start and end addresses - if not self.matches_range(base_address, base_address + data_len - 1): - logger.warning( - f"Image address range is not within key blob: {hex(self.start_addr)}-{hex(self.end_addr)}." - ) - - if self.attributes.ctr_mode: - return self.encrypt_image_ctr(base_address, data) - return self.encrypt_image_xts(base_address, data) - - @staticmethod - def calculate_tweak(address: int) -> bytes: - """Calculate tweak value for AES-XTS encryption based on the address value. - - :param address: start address of encryption - :return: 16 byte tweak values - """ - sector = address >> 12 - tweak = bytearray(16) - for n in range(16): - tweak[n] = sector & 0xFF - sector = sector >> 8 - return bytes(tweak) - - -class Iee: - """IEE: Inline Encryption Engine.""" - - IEE_DATA_UNIT = 0x1000 - IEE_KEY_BLOBS_SIZE = 384 - - def __init__(self) -> None: - """Constructor.""" - self._key_blobs: List[IeeKeyBlob] = [] - - def __getitem__(self, index: int) -> IeeKeyBlob: - return self._key_blobs[index] - - def __setitem__(self, index: int, value: IeeKeyBlob) -> None: - self._key_blobs.remove(self._key_blobs[index]) - self._key_blobs.insert(index, value) - - def add_key_blob(self, key_blob: IeeKeyBlob) -> None: - """Add key for specified address range. - - :param key_blob: to be added - """ - self._key_blobs.append(key_blob) - - def encrypt_image(self, image: bytes, base_addr: int) -> bytes: - """Encrypt image with all available keyblobs. - - :param image: plain image to be encrypted - :param base_addr: where the image will be located in target processor - :return: encrypted image - """ - encrypted_data = bytearray(image) - addr = base_addr - for block in split_data(image, self.IEE_DATA_UNIT): - for key_blob in self._key_blobs: - if key_blob.matches_range(addr, addr + len(block)): - logger.debug( - f"Encrypting {hex(addr)}:{hex(len(block) + addr)}" - f" with keyblob: \n {str(key_blob)}" - ) - encrypted_data[ - addr - base_addr : len(block) + addr - base_addr - ] = key_blob.encrypt_image(addr, block) - addr += len(block) - - return bytes(encrypted_data) - - def get_key_blobs(self) -> bytes: - """Get key blobs. - - :return: Binary key blobs joined together - """ - result = bytes() - for key_blob in self._key_blobs: - result += key_blob.plain_data() - - # return result - return align_block(result, self.IEE_KEY_BLOBS_SIZE) - - def encrypt_key_blobs( - self, - ibkek1: Union[bytes, str], - ibkek2: Union[bytes, str], - keyblob_address: int, - ) -> bytes: - """Encrypt keyblobs and export them as binary. - - :param ibkek1: key encryption key AES-XTS 256 bit - :param ibkek2: key encryption key AES-XTS 256 bit - :param keyblob_address: keyblob base address - :return: encrypted keyblobs - """ - plain_key_blobs = self.get_key_blobs() - - ibkek1 = reverse_bytes_in_longs(value_to_bytes(ibkek1, byte_cnt=32)) - logger.debug(f"IBKEK1: {' '.join(f'{b:02x}' for b in ibkek1)}") - ibkek2 = reverse_bytes_in_longs(value_to_bytes(ibkek2, byte_cnt=32)) - logger.debug(f"IBKEK2 {' '.join(f'{b:02x}' for b in ibkek2)}") - - tweak = IeeKeyBlob.calculate_tweak(keyblob_address) - return aes_xts_encrypt( - ibkek1 + ibkek2, - plain_key_blobs, - tweak, - ) - - -class IeeNxp(Iee): - """IEE: Inline Encryption Engine.""" - - def __init__( - self, - family: str, - keyblob_address: int, - ibkek1: Union[bytes, str], - ibkek2: Union[bytes, str], - key_blobs: Optional[List[IeeKeyBlob]] = None, - binaries: Optional[BinaryImage] = None, - ) -> None: - """Constructor. - - :param family: Device family - :param ibkek1: 256 bit key to encrypt IEE keyblob - :param ibkek2: 256 bit key to encrypt IEE keyblob - :param key_blobs: Optional Key blobs to add to IEE, defaults to None - :raises SPSDKValueError: Unsupported family - """ - super().__init__() - - if family not in self.get_supported_families(): - raise SPSDKValueError(f"Unsupported family{family} by IEE") - - self.family = family - self.ibkek1 = bytes.fromhex(ibkek1) if isinstance(ibkek1, str) else ibkek1 - self.ibkek2 = bytes.fromhex(ibkek2) if isinstance(ibkek2, str) else ibkek2 - self.keyblob_address = keyblob_address - self.binaries = binaries - - self.db = get_db(family, "latest") - self.blobs_min_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_min_cnt") - self.blobs_max_cnt = self.db.get_int(DatabaseManager.IEE, "key_blob_max_cnt") - self.generate_keyblob = self.db.get_bool( - DatabaseManager.IEE, "generate_keyblob" - ) - - if key_blobs: - for key_blob in key_blobs: - self.add_key_blob(key_blob) - - def export_key_blobs(self) -> bytes: - """Export encrypted keyblobs in binary. - - :return: Encrypted keyblobs - """ - return self.encrypt_key_blobs(self.ibkek1, self.ibkek2, self.keyblob_address) - - def export_image(self) -> Optional[BinaryImage]: - """Export encrypted image. - - :return: Encrypted image - """ - if self.binaries is None: - return None - self.binaries.validate() - - binaries: BinaryImage = deepcopy(self.binaries) - - for binary in binaries.sub_images: - if binary.binary: - binary.binary = self.encrypt_image( - binary.binary, binary.absolute_address + self.keyblob_address - ) - for segment in binary.sub_images: - if segment.binary: - segment.binary = self.encrypt_image( - segment.binary, - segment.absolute_address + self.keyblob_address, - ) - - binaries.validate() - return binaries - - def get_blhost_script_otp_kek(self) -> str: - """Create BLHOST script to load fuses needed to run IEE with OTP fuses. - - :return: BLHOST script that loads the keys into fuses. - """ - if not self.db.get_bool(DatabaseManager.IEE, "has_kek_fuses", default=False): - logger.debug(f"The {self.family} has no IEE KEK fuses") - return "" - - xml_fuses = self.db.get_file_path( - DatabaseManager.IEE, "reg_fuses", default=None - ) - if not xml_fuses: - logger.debug(f"The {self.family} has no IEE fuses definition") - return "" - - fuses = Registers(self.family, base_endianness=Endianness.LITTLE) - grouped_regs = self.db.get_list( - DatabaseManager.IEE, "grouped_registers", default=None - ) - - fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) - fuses.find_reg("USER_KEY1").set_value(self.ibkek1) - fuses.find_reg("USER_KEY2").set_value(self.ibkek2) - - load_iee = fuses.find_reg("LOAD_IEE_KEY") - load_iee.find_bitfield("LOAD_IEE_KEY_BITFIELD").set_value(1) - - encrypt_engine = fuses.find_reg("ENCRYPT_XIP_ENGINE") - encrypt_engine.find_bitfield("ENCRYPT_XIP_ENGINE_BITFIELD").set_value(1) - - boot_cfg = fuses.find_reg("BOOT_CFG") - boot_cfg.find_bitfield("ENCRYPT_XIP_EN_BITFIELD").set_value(1) - - ibkek_lock = fuses.find_reg("USER_KEY_RLOCK") - ibkek_lock.find_bitfield("USER_KEY1_RLOCK").set_value(1) - ibkek_lock.find_bitfield("USER_KEY2_RLOCK").set_value(1) - - ret = ( - "# BLHOST IEE fuses programming script\n" - f"# Generated by SPSDK {spsdk_version}\n" - f"# Chip: {self.family} \n\n" - ) - - ret += f"# OTP IBKEK1: {self.ibkek1.hex()}\n\n" - for reg in fuses.find_reg("USER_KEY1").sub_regs: - ret += f"# {reg.name} fuse.\n" - ret += f"efuse-program-once {hex(reg.offset)} 0x{reg.get_hex_value(raw=True)} --no-verify\n" - - ret += f"\n\n# OTP IBKEK2: {self.ibkek2.hex()}\n\n" - for reg in fuses.find_reg("USER_KEY2").sub_regs: - ret += f"# {reg.name} fuse.\n" - ret += f"efuse-program-once {hex(reg.offset)} 0x{reg.get_hex_value(raw=True)} --no-verify\n" - - ret += f"\n\n# {load_iee.name} fuse.\n" - for bitfield in load_iee.get_bitfields(): - ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" - ret += f"efuse-program-once {hex(load_iee.offset)} 0x{load_iee.get_hex_value(raw=True)} --no-verify\n" - - ret += f"\n\n# {encrypt_engine.name} fuse.\n" - for bitfield in encrypt_engine.get_bitfields(): - ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" - ret += ( - f"efuse-program-once {hex(encrypt_engine.offset)} " - f"0x{encrypt_engine.get_hex_value(raw=True)} --no-verify\n" - ) - - ret += f"\n\n# {ibkek_lock.name} fuse.\n" - for bitfield in ibkek_lock.get_bitfields(): - ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" - ret += f"efuse-program-once {hex(ibkek_lock.offset)} 0x{ibkek_lock.get_hex_value(raw=True)} --no-verify\n" - - ret += f"\n\n# {boot_cfg.name} fuse.\n" - ret += ( - "WARNING!! Check SRM and set all desired bitfields for boot configuration" - ) - for bitfield in boot_cfg.get_bitfields(): - ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" - ret += ( - f"# efuse-program-once {hex(boot_cfg.offset)} " - f"0x{boot_cfg.get_hex_value(raw=True)} --no-verify\n" - ) - - return ret - - def binary_image( - self, - plain_data: bool = False, - data_alignment: int = 16, - keyblob_name: str = "iee_keyblob.bin", - image_name: str = "encrypted.bin", - ) -> BinaryImage: - """Get the IEE Binary Image representation. - - :param plain_data: Binary representation in plain format, defaults to False - :param data_alignment: Alignment of data part key blobs. - :param keyblob_name: Filename of the IEE keyblob - :param image_name: Filename of the IEE image - :return: IEE in BinaryImage. - """ - iee = BinaryImage(image_name, offset=self.keyblob_address) - if self.generate_keyblob: - # Add mandatory IEE keyblob - iee_keyblobs = ( - self.get_key_blobs() if plain_data else self.export_key_blobs() - ) - iee.add_image( - BinaryImage( - keyblob_name, - offset=0, - description=f"IEE keyblobs {self.family}", - binary=iee_keyblobs, - ) - ) - binaries = self.export_image() - - if binaries: - binaries.alignment = data_alignment - binaries.validate() - iee.add_image(binaries) - - return iee - - @staticmethod - def get_supported_families() -> List[str]: - """Get all supported families for AHAB container. - - :return: List of supported families. - """ - return get_families(DatabaseManager.IEE) - - @staticmethod - def get_validation_schemas(family: str) -> List[Dict[str, Any]]: - """Get list of validation schemas. - - :param family: Family for which the template should be generated. - :return: Validation list of schemas. - """ - if family not in IeeNxp.get_supported_families(): - return [] - - database = get_db(family, "latest") - schemas = get_schema_file(DatabaseManager.IEE) - family_sch = schemas["iee_family"] - family_sch["properties"]["family"]["enum"] = IeeNxp.get_supported_families() - family_sch["properties"]["family"]["template_value"] = family - ret = [family_sch, schemas["iee_output"], schemas["iee"]] - additional_schemes = database.get_list( - DatabaseManager.IEE, "additional_template", default=[] - ) - ret.extend([schemas[x] for x in additional_schemes]) - return ret - - @staticmethod - def get_validation_schemas_family() -> List[Dict[str, Any]]: - """Get list of validation schemas for family key. - - :return: Validation list of schemas. - """ - schemas = get_schema_file(DatabaseManager.IEE) - family_sch = schemas["iee_family"] - family_sch["properties"]["family"]["enum"] = IeeNxp.get_supported_families() - return [family_sch] - - @staticmethod - def generate_config_template(family: str) -> Dict[str, Any]: - """Generate IEE configuration template. - - :param family: Family for which the template should be generated. - :return: Dictionary of individual templates (key is name of template, value is template itself). - """ - val_schemas = IeeNxp.get_validation_schemas(family) - database = get_db(family, "latest") - - if val_schemas: - template_note = database.get_str( - DatabaseManager.IEE, "additional_template_text", default="" - ) - title = ( - f"IEE: Inline Encryption Engine Configuration template for {family}." - ) - - yaml_data = CommentedConfig( - title, val_schemas, note=template_note - ).get_template() - - return {f"{family}_iee": yaml_data} - - return {} - - @staticmethod - def load_from_config( - config: Dict[str, Any], - config_dir: str, - search_paths: Optional[List[str]] = None, - ) -> "IeeNxp": - """Converts the configuration option into an IEE image object. - - "config" content array of containers configurations. - - :param config: array of IEE configuration dictionaries. - :param config_dir: directory where the config is located - :param search_paths: List of paths where to search for the file, defaults to None - :return: initialized IEE object. - """ - iee_config: List[Dict[str, Any]] = config.get( - "key_blobs", [config.get("key_blob")] - ) - family = config["family"] - ibkek1 = load_hex_string( - config.get( - "ibkek1", - "0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", - ), - 32, - ) - ibkek2 = load_hex_string( - config.get( - "ibkek2", - "0x202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", - ), - 32, - ) - - logger.debug(f"Loaded IBKEK1: {ibkek1.hex()}") - logger.debug(f"Loaded IBKEK2: {ibkek2.hex()}") - - keyblob_address = value_to_int(config["keyblob_address"]) - start_address = min( - [value_to_int(addr.get("start_address", 0xFFFFFFFF)) for addr in iee_config] - ) - - data_blobs: Optional[List[Dict]] = config.get("data_blobs") - binaries = None - if data_blobs: - # start address to calculate offset from keyblob, min from keyblob or data blob address - # pylint: disable-next=nested-min-max - start_address = min( - min( - [ - value_to_int(addr.get("address", 0xFFFFFFFF)) - for addr in data_blobs - ] - ), - start_address, - ) - binaries = BinaryImage( - filepath_from_config( - config, - "encrypted_name", - "encrypted_blobs", - config_dir, - config["output_folder"], - ), - offset=start_address - keyblob_address, - alignment=IeeKeyBlob._ENCRYPTION_BLOCK_SIZE, - ) - for data_blob in data_blobs: - address = value_to_int( - data_blob.get("address", 0), keyblob_address + binaries.offset - ) - - binary = BinaryImage.load_binary_image( - path=data_blob["data"], - search_paths=search_paths, - offset=address - keyblob_address - binaries.offset, - alignment=IeeKeyBlob._ENCRYPTION_BLOCK_SIZE, - size=0, - ) - - binaries.add_image(binary) - - iee = IeeNxp(family, keyblob_address, ibkek1, ibkek2, binaries=binaries) - - for key_blob_cfg in iee_config: - aes_mode = key_blob_cfg["aes_mode"] - region_lock = "LOCK" if key_blob_cfg.get("region_lock") else "UNLOCK" - key_size = key_blob_cfg["key_size"] - - attributes = IeeKeyBlobAttribute( - IeeKeyBlobLockAttributes.from_label(region_lock), - IeeKeyBlobKeyAttributes.from_label(key_size), - IeeKeyBlobModeAttributes.from_label(aes_mode), - ) - - key1 = load_hex_string(key_blob_cfg["key1"], attributes.key1_size) - key2 = load_hex_string(key_blob_cfg["key2"], attributes.key2_size) - - start_addr = value_to_int(key_blob_cfg.get("start_address", start_address)) - end_addr = value_to_int(key_blob_cfg.get("end_address", 0xFFFFFFFF)) - page_offset = value_to_int(key_blob_cfg.get("page_offset", 0)) - - iee.add_key_blob( - IeeKeyBlob( - attributes=attributes, - start_addr=start_addr, - end_addr=end_addr, - key1=key1, - key2=key2, - page_offset=page_offset, - ) - ) - - return iee diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py index cb49f703..dc7ae1d7 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py @@ -7,37 +7,8 @@ """The module provides support for On-The-Fly encoding for RTxxx devices.""" -import logging -import os -from copy import deepcopy -from struct import pack from typing import Any, Dict, List, Optional, Union -from crcmod.predefined import mkPredefinedCrcFun - -from ... import version as spsdk_version -from ...apps.utils.utils import filepath_from_config -from ...crypto.rng import random_bytes -from ...crypto.symmetric import Counter, aes_ctr_encrypt, aes_key_wrap -from ...exceptions import SPSDKError, SPSDKValueError -from ...utils.database import DatabaseManager, get_db, get_families, get_schema_file -from ...utils.exceptions import SPSDKRegsErrorBitfieldNotFound -from ...utils.images import BinaryImage -from ...utils.misc import ( - Endianness, - align_block, - load_binary, - load_hex_string, - reverse_bits_in_bytes, - split_data, - value_to_bytes, - value_to_int, -) -from ...utils.registers import Registers -from ...utils.schema_validator import CommentedConfig - -logger = logging.getLogger(__name__) - class KeyBlob: """OTFAD KeyBlob: The class specifies AES key and counter initial value for specified address range. @@ -355,632 +326,3 @@ def is_encrypted(self) -> bool: (self.key_flags & (self.KEY_FLAG_ADE | self.KEY_FLAG_VLD)) == (self.KEY_FLAG_ADE | self.KEY_FLAG_VLD) ) - - -class Otfad: - """OTFAD: On-the-Fly AES Decryption Module.""" - - OTFAD_DATA_UNIT = 0x400 - - def __init__(self) -> None: - """Constructor.""" - self._key_blobs: List[KeyBlob] = [] - - def __getitem__(self, index: int) -> KeyBlob: - return self._key_blobs[index] - - def __setitem__(self, index: int, value: KeyBlob) -> None: - self._key_blobs.remove(self._key_blobs[index]) - self._key_blobs.insert(index, value) - - def __len__(self) -> int: - """Count of keyblobs.""" - return len(self._key_blobs) - - def add_key_blob(self, key_blob: KeyBlob) -> None: - """Add key for specified address range. - - :param key_blob: to be added - """ - self._key_blobs.append(key_blob) - - def encrypt_image(self, image: bytes, base_addr: int, byte_swap: bool) -> bytes: - """Encrypt image with all available keyblobs. - - :param image: plain image to be encrypted - :param base_addr: where the image will be located in target processor - :param byte_swap: this probably depends on the flash device, how bytes are organized there - :return: encrypted image - """ - encrypted_data = bytearray(image) - addr = base_addr - for block in split_data(image, self.OTFAD_DATA_UNIT): - for key_blob in self._key_blobs: - if key_blob.matches_range(addr, addr + len(block)): - logger.debug( - f"Encrypting {hex(addr)}:{hex(len(block) + addr)}" - f" with keyblob: \n {str(key_blob)}" - ) - encrypted_data[ - addr - base_addr : len(block) + addr - base_addr - ] = key_blob.encrypt_image( - addr, block, byte_swap, counter_value=addr - ) - addr += len(block) - - return bytes(encrypted_data) - - def get_key_blobs(self) -> bytes: - """Get key blobs. - - :return: Binary key blobs joined together - """ - result = bytes() - for key_blob in self._key_blobs: - result += key_blob.plain_data() - return align_block( - result, 256 - ) # this is for compatibility with elftosb, probably need FLASH sector size - - def encrypt_key_blobs( - self, - kek: Union[bytes, str], - key_scramble_mask: Optional[int] = None, - key_scramble_align: Optional[int] = None, - byte_swap_cnt: int = 0, - ) -> bytes: - """Encrypt key blobs with specified key. - - :param kek: key to encode key blobs - :param key_scramble_mask: 32-bit scramble key, if KEK scrambling is desired. - :param key_scramble_align: 8-bit scramble align, if KEK scrambling is desired. - :param byte_swap_cnt: Encrypted keyblob reverse byte count, 0 means NO reversing is enabled - :raises SPSDKValueError: Invalid input value. - :return: encrypted binary key blobs joined together - """ - if isinstance(kek, str): - kek = bytes.fromhex(kek) - scramble_enabled = ( - key_scramble_mask is not None and key_scramble_align is not None - ) - if scramble_enabled: - assert key_scramble_mask and key_scramble_align - if key_scramble_mask >= 1 << 32: - raise SPSDKValueError("OTFAD Key scramble mask has invalid length") - if key_scramble_align >= 1 << 8: - raise SPSDKValueError("OTFAD Key scramble align has invalid length") - - logger.debug("The scrambling of keys is enabled.") - key_scramble_mask_inv = reverse_bits_in_bytes( - key_scramble_mask.to_bytes(4, byteorder=Endianness.BIG.value) - ) - logger.debug(f"The inverted scramble key is: {key_scramble_mask_inv.hex()}") - result = bytes() - scrambled = bytes() - for i, key_blob in enumerate(self._key_blobs): - if scramble_enabled: - assert key_scramble_mask and key_scramble_align - scrambled = bytearray(kek) - long_ix = (key_scramble_align >> (i * 2)) & 0x03 - for j in range(4): - scrambled[(long_ix * 4) + j] ^= key_scramble_mask_inv[j] - - logger.debug( - f"Used KEK for keyblob{i} encryption is: {scrambled.hex() if scramble_enabled else kek.hex()}" - ) - - result += key_blob.export( - scrambled if scramble_enabled else kek, byte_swap_cnt=byte_swap_cnt - ) - return align_block( - result, 256 - ) # this is for compatibility with elftosb, probably need FLASH sector size - - def __str__(self) -> str: - """Text info about the instance.""" - msg = "Key-Blob\n" - for index, key_blob in enumerate(self._key_blobs): - msg += f"Key-Blob {str(index)}:\n" - msg += str(key_blob) - return msg - - -class OtfadNxp(Otfad): - """OTFAD: On-the-Fly AES Decryption Module with reflecting of NXP parts.""" - - def __init__( - self, - family: str, - kek: Union[bytes, str], - table_address: int = 0, - key_blobs: Optional[List[KeyBlob]] = None, - key_scramble_mask: Optional[int] = None, - key_scramble_align: Optional[int] = None, - binaries: Optional[BinaryImage] = None, - ) -> None: - """Constructor. - - :param family: Device family - :param kek: KEK to encrypt OTFAD table - :param table_address: Absolute address of OTFAD table. - :param key_blobs: Optional Key blobs to add to OTFAD, defaults to None - :param key_scramble_mask: If defined, the key scrambling algorithm will be applied. - ('key_scramble_align' must be defined also) - :param key_scramble_align: If defined, the key scrambling algorithm will be applied. - ('key_scramble_mask' must be defined also) - :raises SPSDKValueError: Unsupported family - """ - super().__init__() - - if family not in self.get_supported_families(): - raise SPSDKValueError(f"Unsupported family{family} by OTFAD") - - if (key_scramble_align is None and key_scramble_mask) or ( - key_scramble_align and key_scramble_mask is None - ): - raise SPSDKValueError("Key Scrambling is not fully defined") - - self.family = family - self.kek = bytes.fromhex(kek) if isinstance(kek, str) else kek - self.key_scramble_mask = key_scramble_mask - self.key_scramble_align = key_scramble_align - self.table_address = table_address - self.db = get_db(family, "latest") - self.blobs_min_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_min_cnt") - self.blobs_max_cnt = self.db.get_int(DatabaseManager.OTFAD, "key_blob_max_cnt") - self.byte_swap = self.db.get_bool(DatabaseManager.OTFAD, "byte_swap") - self.key_blob_rec_size = self.db.get_int( - DatabaseManager.OTFAD, "key_blob_rec_size" - ) - self.keyblob_byte_swap_cnt = self.db.get_int( - DatabaseManager.OTFAD, "keyblob_byte_swap_cnt" - ) - assert self.keyblob_byte_swap_cnt in [0, 2, 4, 8, 16] - self.binaries = binaries - - if key_blobs: - for key_blob in key_blobs: - self.add_key_blob(key_blob) - - # Just fill up the minimum count of key blobs - while len(self._key_blobs) < self.blobs_min_cnt: - self.add_key_blob( - KeyBlob( - start_addr=0, - end_addr=0, - key=bytes([0] * KeyBlob.KEY_SIZE), - counter_iv=bytes([0] * KeyBlob.CTR_SIZE), - key_flags=0, - zero_fill=bytes([0] * 4), - ) - ) - - @staticmethod - def get_blhost_script_otp_keys( - family: str, otp_master_key: bytes, otfad_key_seed: bytes - ) -> str: - """Create BLHOST script to load fuses needed to run OTFAD with OTP fuses. - - :param family: Device family. - :param otp_master_key: OTP Master Key. - :param otfad_key_seed: OTFAD Key Seed. - :return: BLHOST script that loads the keys into fuses. - """ - database = get_db(family, "latest") - xml_fuses = database.get_file_path( - DatabaseManager.OTFAD, "reg_fuses", default=None - ) - if not xml_fuses: - logger.debug(f"The {family} has no OTFAD fuses definition") - return "" - - fuses = Registers(family, base_endianness=Endianness.LITTLE) - grouped_regs = database.get_list( - DatabaseManager.OTFAD, "grouped_registers", default=None - ) - fuses.load_registers_from_xml(xml_fuses, grouped_regs=grouped_regs) - reg_omk = fuses.find_reg("OTP_MASTER_KEY") - reg_oks = fuses.find_reg("OTFAD_KEK_SEED") - reg_omk.set_value(otp_master_key) - reg_oks.set_value(otfad_key_seed) - ret = ( - "# BLHOST OTFAD keys fuse programming script\n" - f"# Generated by SPSDK {spsdk_version}\n" - f"# Chip: {family}\n\n" - ) - - ret += f"# OTP MASTER KEY(Big Endian): 0x{reg_omk.get_bytes_value(raw=False).hex()}\n\n" - for reg in reg_omk.sub_regs: - ret += f"# {reg.name} fuse.\n" - ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" - - ret += f"\n# OTFAD KEK SEED (Big Endian): 0x{reg_oks.get_bytes_value(raw=True).hex()}\n\n" - for reg in reg_oks.sub_regs: - ret += f"# {reg.name} fuse.\n" - ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" - - return ret - - @staticmethod - def _replace_idx_value(value: str, index: int) -> str: - """Replace index value if provided in the database. - - :param value: value to be replaced f-string containing index - :param index: Index of record to be replaced - :return: value with replaced index - """ - return value.replace("{index}", str(index)) - - def get_blhost_script_otp_kek(self, index: int = 1) -> str: - """Create BLHOST script to load fuses needed to run OTFAD with OTP fuses just for OTFAD key. - - :param index: Index of OTFAD peripheral [1, 2, ..., n]. - :return: BLHOST script that loads the keys into fuses. - """ - if not self.db.get_bool(DatabaseManager.OTFAD, "has_kek_fuses", default=False): - logger.debug(f"The {self.family} has no OTFAD KEK fuses") - return "" - - peripheral_list = self.db.get_list(DatabaseManager.OTFAD, "peripheral_list") - if str(index) not in peripheral_list: - logger.debug(f"The {self.family} has no OTFAD{index} peripheral") - return "" - - filter_out_list = [f"OTFAD{i}" for i in peripheral_list if str(index) != i] - xml_fuses = self.db.get_file_path( - DatabaseManager.OTFAD, "reg_fuses", default=None - ) - if not xml_fuses: - logger.debug(f"The {self.family} has no OTFAD fuses definition") - return "" - - fuses = Registers(self.family, base_endianness=Endianness.LITTLE) - - grouped_regs = self.db.get_list( - DatabaseManager.OTFAD, "grouped_registers", default=None - ) - - fuses.load_registers_from_xml(xml_fuses, filter_out_list, grouped_regs) - - scramble_enabled = ( - self.key_scramble_mask is not None and self.key_scramble_align is not None - ) - - otfad_key_fuse = self._replace_idx_value( - self.db.get_str(DatabaseManager.OTFAD, "otfad_key_fuse"), index - ) - otfad_cfg_fuse = self._replace_idx_value( - self.db.get_str(DatabaseManager.OTFAD, "otfad_cfg_fuse"), index - ) - - fuses.find_reg(otfad_key_fuse).set_value(self.kek) - otfad_cfg = fuses.find_reg(otfad_cfg_fuse) - - try: - otfad_cfg.find_bitfield( - self.db.get_str(DatabaseManager.OTFAD, "otfad_enable_bitfield") - ).set_value(1) - except SPSDKRegsErrorBitfieldNotFound: - logger.debug(f"Bitfield for OTFAD ENABLE not found for {self.family}") - - if scramble_enabled: - scramble_key = self._replace_idx_value( - self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_key"), index - ) - scramble_align = self._replace_idx_value( - self.db.get_str(DatabaseManager.OTFAD, "otfad_scramble_align_bitfield"), - index, - ) - scramble_align_standalone = self.db.get_bool( - DatabaseManager.OTFAD, "otfad_scramble_align_fuse_standalone" - ) - otfad_cfg.find_bitfield( - self._replace_idx_value( - self.db.get_str( - DatabaseManager.OTFAD, "otfad_scramble_enable_bitfield" - ), - index, - ) - ).set_value(1) - if scramble_align_standalone: - fuses.find_reg(scramble_align).set_value(self.key_scramble_align) - else: - otfad_cfg.find_bitfield(scramble_align).set_value( - self.key_scramble_align - ) - fuses.find_reg(scramble_key).set_value(self.key_scramble_mask) - - ret = ( - f"# BLHOST OTFAD{index} KEK fuses programming script\n" - f"# Generated by SPSDK {spsdk_version}\n" - f"# Chip: {self.family}, peripheral: OTFAD{index} !\n\n" - ) - - ret += f"# OTP KEK (Big Endian): {self.kek.hex()}\n\n" - for reg in fuses.find_reg(otfad_key_fuse).sub_regs: - ret += f"# {reg.name} fuse.\n" - ret += f"efuse-program-once {reg.offset} 0x{reg.get_bytes_value(raw=True).hex()} --no-verify\n" - - ret += f"\n\n# {otfad_cfg.name} fuse.\n" - for bitfield in otfad_cfg.get_bitfields(): - ret += f"# {bitfield.name}: {bitfield.get_enum_value()}\n" - ret += f"efuse-program-once {otfad_cfg.offset} 0x{otfad_cfg.get_bytes_value(raw=True).hex()} --no-verify\n" - - if scramble_enabled: - scramble = fuses.find_reg(scramble_key) - ret += f"\n# {scramble.name} fuse.\n" - ret += f"efuse-program-once {scramble.offset} 0x{scramble.get_bytes_value(raw=True).hex()} --no-verify\n" - if scramble_align_standalone: - scramble_align_reg = fuses.find_reg(scramble_align) - ret += f"\n# {scramble_align_reg.name} fuse.\n" - ret += ( - f"efuse-program-once {scramble_align_reg.offset}" - f" 0x{scramble_align_reg.get_bytes_value(raw=True).hex()} --no-verify\n" - ) - - return ret - - def export_image( - self, - plain_data: bool = False, - swap_bytes: bool = False, - join_sub_images: bool = True, - table_address: int = 0, - ) -> Optional[BinaryImage]: - """Get the OTFAD Key Blob Binary Image representation. - - :param plain_data: Binary representation in plain data format, defaults to False - :param swap_bytes: For some platforms the swap bytes is needed in encrypted format, defaults to False. - :param join_sub_images: If it's True, all the binary sub-images are joined into one, defaults to True. - :param table_address: Absolute address of OTFAD table. - :return: OTFAD key blob data in BinaryImage. - """ - if self.binaries is None: - return None - binaries: BinaryImage = deepcopy(self.binaries) - for binary in binaries.sub_images: - if binary.binary: - binary.binary = align_block( - binary.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE - ) - for segment in binary.sub_images: - if segment.binary: - segment.binary = align_block( - segment.binary, KeyBlob._ENCRYPTION_BLOCK_SIZE - ) - - binaries.validate() - - if not plain_data: - for binary in binaries.sub_images: - if binary.binary: - binary.binary = self.encrypt_image( - binary.binary, - table_address + binary.absolute_address, - swap_bytes, - ) - for segment in binary.sub_images: - if segment.binary: - segment.binary = self.encrypt_image( - segment.binary, - segment.absolute_address + table_address, - swap_bytes, - ) - - if join_sub_images: - binaries.join_images() - binaries.validate() - - return binaries - - def binary_image( - self, - plain_data: bool = False, - data_alignment: int = 16, - otfad_table_name: str = "OTFAD_Table", - ) -> BinaryImage: - """Get the OTFAD Binary Image representation. - - :param plain_data: Binary representation in plain format, defaults to False - :param data_alignment: Alignment of data part key blobs. - :param otfad_table_name: name of the output file that contains OTFAD table - :return: OTFAD in BinaryImage. - """ - otfad = BinaryImage("OTFAD", offset=self.table_address) - # Add mandatory OTFAD table - otfad_table = ( - self.get_key_blobs() - if plain_data - else self.encrypt_key_blobs( - self.kek, - self.key_scramble_mask, - self.key_scramble_align, - self.keyblob_byte_swap_cnt, - ) - ) - otfad.add_image( - BinaryImage( - otfad_table_name, - size=self.key_blob_rec_size * self.blobs_max_cnt, - offset=0, - description=f"OTFAD description table for {self.family}", - binary=otfad_table, - alignment=256, - ) - ) - binaries = self.export_image(table_address=self.table_address) - - if binaries: - binaries.alignment = data_alignment - binaries.validate() - otfad.add_image(binaries) - return otfad - - @staticmethod - def get_supported_families() -> List[str]: - """Get all supported families for AHAB container. - - :return: List of supported families. - """ - return get_families(DatabaseManager.OTFAD) - - @staticmethod - def get_validation_schemas(family: str) -> List[Dict[str, Any]]: - """Get list of validation schemas. - - :param family: Family for which the template should be generated. - :return: Validation list of schemas. - """ - if family not in OtfadNxp.get_supported_families(): - return [] - - database = get_db(family, "latest") - schemas = get_schema_file(DatabaseManager.OTFAD) - family_sch = schemas["otfad_family"] - family_sch["properties"]["family"]["enum"] = OtfadNxp.get_supported_families() - family_sch["properties"]["family"]["template_value"] = family - ret = [family_sch, schemas["otfad_output"], schemas["otfad"]] - additional_schemes = database.get_list( - DatabaseManager.OTFAD, "additional_template", default=[] - ) - ret.extend([schemas[x] for x in additional_schemes]) - return ret - - @staticmethod - def get_validation_schemas_family() -> List[Dict[str, Any]]: - """Get list of validation schemas for family key. - - :return: Validation list of schemas. - """ - schemas = get_schema_file(DatabaseManager.OTFAD) - family_sch = schemas["otfad_family"] - family_sch["properties"]["family"]["enum"] = OtfadNxp.get_supported_families() - return [family_sch] - - @staticmethod - def generate_config_template(family: str) -> Dict[str, Any]: - """Generate OTFAD configuration template. - - :param family: Family for which the template should be generated. - :return: Dictionary of individual templates (key is name of template, value is template itself). - """ - val_schemas = OtfadNxp.get_validation_schemas(family) - database = get_db(family, "latest") - - if val_schemas: - template_note = database.get_str( - DatabaseManager.OTFAD, "additional_template_text", default="" - ) - title = f"On-The-Fly AES decryption Configuration template for {family}." - - yaml_data = CommentedConfig( - title, val_schemas, note=template_note - ).get_template() - - return {f"{family}_otfad": yaml_data} - - return {} - - @staticmethod - def load_from_config( - config: Dict[str, Any], - config_dir: str, - search_paths: Optional[List[str]] = None, - ) -> "OtfadNxp": - """Converts the configuration option into an OTFAD image object. - - "config" content array of containers configurations. - - :param config: array of OTFAD configuration dictionaries. - :param config_dir: directory where the config is located - :param search_paths: List of paths where to search for the file, defaults to None - :return: initialized OTFAD object. - """ - otfad_config: List[Dict[str, Any]] = config["key_blobs"] - family = config["family"] - database = get_db(family, "latest") - kek = load_hex_string( - config["kek"], expected_size=16, search_paths=search_paths - ) - logger.debug(f"Loaded KEK: {kek.hex()}") - table_address = value_to_int(config["otfad_table_address"]) - start_address = min( - [value_to_int(addr["start_address"]) for addr in otfad_config] - ) - - key_scramble_mask = None - key_scramble_align = None - if database.get_bool( - DatabaseManager.OTFAD, "supports_key_scrambling", default=False - ): - if "key_scramble" in config.keys(): - key_scramble = config["key_scramble"] - key_scramble_mask = value_to_int(key_scramble["key_scramble_mask"]) - key_scramble_align = value_to_int(key_scramble["key_scramble_align"]) - - data_blobs: Optional[List[Dict]] = config.get("data_blobs") - binaries = None - if data_blobs: - # pylint: disable-next=nested-min-max - start_address = min( - min([value_to_int(addr["address"]) for addr in data_blobs]), - start_address, - ) - binaries = BinaryImage( - filepath_from_config( - config, - "encrypted_name", - "encrypted_blobs", - config_dir, - config["output_folder"], - ), - offset=start_address - table_address, - ) - for data_blob in data_blobs: - data = load_binary(data_blob["data"], search_paths=search_paths) - address = value_to_int(data_blob["address"]) - - binary = BinaryImage( - os.path.basename(data_blob["data"]), - offset=address - table_address - binaries.offset, - binary=data, - ) - binaries.add_image(binary) - else: - logger.warning("The OTFAD configuration has NOT any data blobs records!") - - otfad = OtfadNxp( - family=family, - kek=kek, - table_address=table_address, - key_scramble_align=key_scramble_align, - key_scramble_mask=key_scramble_mask, - binaries=binaries, - ) - - for i, key_blob_cfg in enumerate(otfad_config): - aes_key = value_to_bytes(key_blob_cfg["aes_key"], byte_cnt=KeyBlob.KEY_SIZE) - aes_ctr = value_to_bytes(key_blob_cfg["aes_ctr"], byte_cnt=KeyBlob.CTR_SIZE) - start_addr = value_to_int(key_blob_cfg["start_address"]) - end_addr = value_to_int(key_blob_cfg["end_address"]) - aes_decryption_enable = key_blob_cfg.get("aes_decryption_enable", True) - valid = key_blob_cfg.get("valid", True) - read_only = key_blob_cfg.get("read_only", True) - flags = 0 - if aes_decryption_enable: - flags |= KeyBlob.KEY_FLAG_ADE - if valid: - flags |= KeyBlob.KEY_FLAG_VLD - if read_only: - flags |= KeyBlob.KEY_FLAG_READ_ONLY - - otfad[i] = KeyBlob( - start_addr=start_addr, - end_addr=end_addr, - key=aes_key, - counter_iv=aes_ctr, - key_flags=flags, - zero_fill=bytes([0] * 4), - ) - - return otfad diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py deleted file mode 100644 index 80858300..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/rot.py +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""The module provides support for RoT hash calculation .""" - - -from abc import abstractmethod -from typing import List, Optional, Sequence, Type, Union - -from ...crypto.certificate import Certificate -from ...crypto.keys import PrivateKey, PublicKey -from ...exceptions import SPSDKError -from ...image.ahab.ahab_container import SRKRecord -from ...image.ahab.ahab_container import SRKTable as AhabSrkTable -from ...image.secret import SrkItem -from ...image.secret import SrkTable as HabSrkTable -from ...utils.crypto.rkht import RKHT, RKHTv1, RKHTv21 -from ...utils.database import DatabaseManager, get_db, get_families -from ...utils.misc import load_binary - - -class Rot: - """Root of Trust object providing an abstraction over the RoT hash calculation for multiple device families.""" - - def __init__( - self, - family: str, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """Root of Trust initialization.""" - self.rot_obj = self.get_rot_class(family)( - keys_or_certs=keys_or_certs, password=password, search_paths=search_paths - ) - - def calculate_hash(self) -> bytes: - """Calculate RoT hash.""" - return self.rot_obj.calculate_hash() - - def export(self) -> bytes: - """Export RoT.""" - return self.rot_obj.export() - - @classmethod - def get_supported_families(cls) -> List[str]: - """Get all supported families.""" - return get_families(DatabaseManager.CERT_BLOCK) - - @classmethod - def get_rot_class(cls, family: str) -> Type["RotBase"]: - """Get RoT class.""" - db = get_db(family, "latest") - rot_type = db.get_str(DatabaseManager.CERT_BLOCK, "rot_type") - for subclass in RotBase.__subclasses__(): - if subclass.rot_type == rot_type: - return subclass - raise SPSDKError(f"A ROT type {rot_type} does not exist.") - - -class RotBase: - """Root of Trust base class.""" - - rot_type: Optional[str] = None - - def __init__( - self, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """Rot initialization.""" - self.keys_or_certs = keys_or_certs - self.password = password - self.search_paths = search_paths - - @abstractmethod - def calculate_hash( - self, - ) -> bytes: - """Calculate ROT hash.""" - - @abstractmethod - def export(self) -> bytes: - """Calculate ROT table.""" - - -class RotCertBlockv1(RotBase): - """Root of Trust for certificate block v1 class.""" - - rot_type = "cert_block_1" - - def __init__( - self, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """Rot cert block v1 initialization.""" - super().__init__(keys_or_certs, password, search_paths) - self.rkht = RKHTv1.from_keys( - self.keys_or_certs, self.password, self.search_paths - ) - - def calculate_hash( - self, - ) -> bytes: - """Calculate RoT hash.""" - return self.rkht.rkth() - - def export(self) -> bytes: - """Export RoT.""" - return self.rkht.export() - - -class RotCertBlockv21(RotBase): - """Root of Trust for certificate block v21 class.""" - - rot_type = "cert_block_21" - - def __init__( - self, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """Rot cert block v21 initialization.""" - super().__init__(keys_or_certs, password, search_paths) - self.rkht = RKHTv21.from_keys( - self.keys_or_certs, self.password, self.search_paths - ) - - def calculate_hash( - self, - ) -> bytes: - """Calculate ROT hash.""" - return self.rkht.rkth() - - def export(self) -> bytes: - """Export RoT.""" - return self.rkht.export() - - -class RotSrkTableAhab(RotBase): - """Root of Trust for AHAB SrkTable class.""" - - rot_type = "srk_table_ahab" - - def __init__( - self, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """AHAB SRK table initialization.""" - super().__init__(keys_or_certs, password, search_paths) - self.srk = AhabSrkTable( - [ - SRKRecord(RKHT.convert_key(key, password, search_paths)) - for key in keys_or_certs - ] - ) - self.srk.update_fields() - - def calculate_hash(self) -> bytes: - """Calculate ROT hash.""" - return self.srk.compute_srk_hash() - - def export(self) -> bytes: - """Export RoT.""" - return self.srk.export() - - -class RotSrkTableHab(RotBase): - """Root of Trust for HAB SrkTable class.""" - - rot_type = "srk_table_hab" - - def __init__( - self, - keys_or_certs: Sequence[ - Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] - ], - password: Optional[str] = None, - search_paths: Optional[List[str]] = None, - ) -> None: - """HAB SRK table initialization.""" - super().__init__(keys_or_certs, password, search_paths) - self.srk = HabSrkTable() - for certificate in keys_or_certs: - if isinstance(certificate, (str, bytes, bytearray)): - try: - certificate = self._load_certificate(certificate, search_paths) - except SPSDKError as exc: - raise SPSDKError( - "Unable to load certificate. Certificate must be provided for HAB RoT calculation." - ) from exc - if not isinstance(certificate, Certificate): - raise SPSDKError( - "Certificate must be provided for HAB RoT calculation." - ) - item = SrkItem.from_certificate(certificate) - self.srk.append(item) - - def calculate_hash(self) -> bytes: - """Calculate ROT hash.""" - return self.srk.export_fuses() - - def export(self) -> bytes: - """Export RoT.""" - return self.srk.export() - - @classmethod - def _load_certificate( - cls, - certificate: Union[str, bytes, bytearray], - search_paths: Optional[List[str]] = None, - ) -> Certificate: - """Load certificate if certificate provided, or extract public key if private/public key is provided.""" - if isinstance(certificate, str): - certificate = load_binary(certificate, search_paths) - try: - return Certificate.parse(certificate) - except SPSDKError as exc: - raise SPSDKError("Unable to load certificate.") from exc diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py deleted file mode 100644 index 280b656c..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/images.py +++ /dev/null @@ -1,616 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2022-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Module to keep additional utilities for binary images.""" - -import logging -import math -import os -import re -import textwrap -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -import colorama - -from ..exceptions import SPSDKError, SPSDKOverlapError, SPSDKValueError -from ..utils.database import DatabaseManager -from ..utils.misc import ( - BinaryPattern, - align, - align_block, - find_file, - format_value, - size_fmt, - write_file, -) -from ..utils.schema_validator import CommentedConfig - -if TYPE_CHECKING: - # bincopy will be loaded lazily as needed, this is just to satisfy type-hint checkers - import bincopy - -logger = logging.getLogger(__name__) - - -class ColorPicker: - """Simple class to get each time when ask different color from list.""" - - COLORS = [ - colorama.Fore.LIGHTBLACK_EX, - colorama.Fore.BLUE, - colorama.Fore.GREEN, - colorama.Fore.CYAN, - colorama.Fore.YELLOW, - colorama.Fore.MAGENTA, - colorama.Fore.WHITE, - colorama.Fore.LIGHTBLUE_EX, - colorama.Fore.LIGHTCYAN_EX, - colorama.Fore.LIGHTGREEN_EX, - colorama.Fore.LIGHTMAGENTA_EX, - colorama.Fore.LIGHTWHITE_EX, - colorama.Fore.LIGHTYELLOW_EX, - ] - - def __init__(self) -> None: - """Constructor of ColorPicker.""" - self.index = len(self.COLORS) - - def get_color(self, unwanted_color: Optional[str] = None) -> str: - """Get new color from list. - - :param unwanted_color: Color that should be omitted. - :return: Color - """ - self.index += 1 - if self.index >= len(ColorPicker.COLORS): - self.index = 0 - if unwanted_color and ColorPicker.COLORS[self.index] == unwanted_color: - return self.get_color(unwanted_color) - return ColorPicker.COLORS[self.index] - - -class BinaryImage: - """Binary Image class.""" - - MINIMAL_DRAW_WIDTH = 30 - - def __init__( - self, - name: str, - size: int = 0, - offset: int = 0, - description: Optional[str] = None, - binary: Optional[bytes] = None, - pattern: Optional[BinaryPattern] = None, - alignment: int = 1, - parent: Optional["BinaryImage"] = None, - ) -> None: - """Binary Image class constructor. - - :param name: Name of Image. - :param size: Image size. - :param offset: Image offset in parent image, defaults to 0 - :param description: Text description of image, defaults to None - :param binary: Optional binary content. - :param pattern: Optional binary pattern. - :param alignment: Optional alignment of result image - :param parent: Handle to parent object, defaults to None - """ - self.name = name - self.description = description - self.offset = offset - self._size = align(size, alignment) - self.binary = binary - self.pattern = pattern - self.alignment = alignment - self.parent = parent - - if parent: - assert isinstance(parent, BinaryImage) - self.sub_images: List["BinaryImage"] = [] - - @property - def size(self) -> int: - """Size property.""" - return len(self) - - @size.setter - def size(self, value: int) -> None: - """Size property setter.""" - self._size = align(value, self.alignment) - - def add_image(self, image: "BinaryImage") -> None: - """Add new sub image information. - - :param image: Image object. - """ - image.parent = self - for i, child in enumerate(self.sub_images): - if image.offset < child.offset: - self.sub_images.insert(i, image) - return - self.sub_images.append(image) - - def join_images(self) -> None: - """Join all sub images into main binary block.""" - binary = self.export() - self.sub_images.clear() - self.binary = binary - - @property - def image_name(self) -> str: - """Image name including all parents. - - :return: Full Image name - """ - if self.parent: - return self.parent.image_name + "=>" + self.name - return self.name - - @property - def absolute_address(self) -> int: - """Image absolute address relative to base parent. - - :return: Absolute address relative to base parent - """ - if self.parent: - return self.parent.absolute_address + self.offset - return self.offset - - def aligned_start(self, alignment: int = 4) -> int: - """Returns aligned start address. - - :param alignment: The alignment value, defaults to 4. - :return: Floor alignment address. - """ - return math.floor(self.absolute_address / alignment) * alignment - - def aligned_length(self, alignment: int = 4) -> int: - """Returns aligned length for erasing purposes. - - :param alignment: The alignment value, defaults to 4. - :return: Ceil alignment length. - """ - end_address = self.absolute_address + len(self) - aligned_end = math.ceil(end_address / alignment) * alignment - aligned_len = aligned_end - self.aligned_start(alignment) - return aligned_len - - def __str__(self) -> str: - """Provides information about image. - - :return: String information about Image. - """ - size = len(self) - ret = "" - ret += f"Name: {self.image_name}\n" - ret += f"Starts: {hex(self.absolute_address)}\n" - ret += f"Ends: {hex(self.absolute_address+ size-1)}\n" - ret += f"Size: {self._get_size_line(size)}\n" - ret += f"Alignment: {size_fmt(self.alignment, use_kibibyte=False)}\n" - if self.pattern: - ret += f"Pattern:{self.pattern.pattern}\n" - if self.description: - ret += self.description + "\n" - return ret - - def validate(self) -> None: - """Validate if the images doesn't overlaps each other.""" - if self.offset < 0: - raise SPSDKValueError( - f"Image offset of {self.image_name} cannot be in negative numbers." - ) - if len(self) < 0: - raise SPSDKValueError( - f"Image size of {self.image_name} cannot be in negative numbers." - ) - for image in self.sub_images: - image.validate() - begin = image.offset - end = begin + len(image) - 1 - # Check if it fits inside the parent image - if end >= len(self): - raise SPSDKOverlapError( - f"The image {image.name} doesn't fit into {self.name} parent image." - ) - # Check if it doesn't overlap any other sibling image - for sibling in self.sub_images: - if sibling != image: - sibling_begin = sibling.offset - sibling_end = sibling_begin + len(sibling) - 1 - if end < sibling_begin or begin > sibling_end: - continue - - raise SPSDKOverlapError( - f"The image overlap error:\n" - f"{str(image)}\n" - "overlaps the:\n" - f"{str(sibling)}\n" - ) - - def _get_size_line(self, size: int) -> str: - """Get string of size line. - - :param size: Size in bytes - :return: Formatted size line. - """ - if size >= 1024: - real_size = ",".join(re.findall(".{1,3}", (str(len(self)))[::-1]))[::-1] - return f"Size: {size_fmt(len(self), False)}; {real_size} B" - - return f"Size: {size_fmt(len(self), False)}" - - def get_min_draw_width(self, include_sub_images: bool = True) -> int: - """Get minimal width of table for draw function. - - :param include_sub_images: Include also sub images into, defaults to True - :return: Minimal width in characters. - """ - widths = [ - self.MINIMAL_DRAW_WIDTH, - len(f"+==-0x0000_0000= {self.name} =+"), - len(f"|{self._get_size_line(self.size)}|"), - ] - if include_sub_images: - for child in self.sub_images: - widths.append( - child.get_min_draw_width() + 2 - ) # +2 means add vertical borders - return max(widths) - - def draw( - self, - include_sub_images: bool = True, - width: int = 0, - color: str = "", - no_color: bool = False, - ) -> str: - # fmt: off - """Draw the image into the ASCII graphics. - - :param include_sub_images: Include also sub images into, defaults to True - :param width: Fixed width of table, 0 means autosize. - :param color: Color of this block, None means automatic color. - :param no_color: Disable adding colors into output. - :raises SPSDKValueError: In case of invalid width. - :return: ASCII art representation of image. - """ - # +==0x0000_0000==Title1===============+ - # | Size: 2048B | - # | Description1 | - # | Description1 2nd line | - # |+==0x0000_0000==Title11============+| - # || Size: 512B || - # || Description11 || - # || Description11 2nd line || - # |+==0x0000_01FF=====================+| - # | | - # |+==0x0000_0210==Title12============+| - # || Size: 512B || - # || Description12 || - # || Description12 2nd line || - # |+==0x0000_041F=====================+| - # +==0x0000_07FF=======================+ - # fmt: on - def _get_centered_line(text: str) -> str: - text_len = len(text) - spaces = width - text_len - 2 - assert spaces >= 0, "Binary Image Draw: Center line is longer than width" - padding_l = int(spaces / 2) - padding_r = int(spaces - padding_l) - return color + f"|{' '*padding_l}{text}{' '*padding_r}|\n" - - def wrap_block(inner: str) -> str: - wrapped_block = "" - lines = inner.splitlines(keepends=False) - for line in lines: - wrapped_block += color + "|" + line + color + "|\n" - return wrapped_block - - if no_color: - color = "" - else: - color_picker = ColorPicker() - try: - self.validate() - color = color or color_picker.get_color() - except SPSDKError: - color = colorama.Fore.RED - - block = "" if self.parent else "\n" - min_width = self.get_min_draw_width(include_sub_images) - if not width and self.parent is None: - width = min_width - - if width < min_width: - raise SPSDKValueError( - f"Binary Image Draw: Width is to short ({width} < minimal width: {min_width})" - ) - - # - Title line - header = f"+=={format_value(self.absolute_address, 32)}= {self.name} =" - block += color + f"{header}{'='*(width-len(header)-1)}+\n" - # - Size - block += _get_centered_line(self._get_size_line(len(self))) - # - Description - if self.description: - for line in textwrap.wrap( - self.description, width=width - 2, fix_sentence_endings=True - ): - block += _get_centered_line(line) - # - Pattern - if self.pattern: - block += _get_centered_line(f"Pattern: {self.pattern.pattern}") - # - Inner blocks - if include_sub_images: - next_free_space = 0 - for child in self.sub_images: - # If the images doesn't comes one by one place empty line - if child.offset != next_free_space: - block += _get_centered_line( - f"Gap: {size_fmt(child.offset-next_free_space, False)}" - ) - next_free_space = child.offset + len(child) - inner_block = child.draw( - include_sub_images=include_sub_images, - width=width - 2, - color="" if no_color else color_picker.get_color(color), - no_color=no_color, - ) - block += wrap_block(inner_block) - - # - Closing line - footer = f"+=={format_value(self.absolute_address + len(self) - 1, 32)}==" - block += color + f"{footer}{'='*(width-len(footer)-1)}+\n" - - if self.parent is None: - block += "\n" + "" if no_color else colorama.Fore.RESET - return block - - def update_offsets(self) -> None: - """Update offsets from the sub images into main offset value begin offsets.""" - offsets = [] - for image in self.sub_images: - offsets.append(image.offset) - - min_offset = min(offsets) - for image in self.sub_images: - image.offset -= min_offset - self.offset += min_offset - - def __len__(self) -> int: - """Get length of image. - - If internal member size is not set(is zero) the size is computed from sub images. - :return: Size of image. - """ - if self._size: - return self._size - max_size = len(self.binary) if self.binary else 0 - for image in self.sub_images: - size = image.offset + len(image) - max_size = max(size, max_size) - return align(max_size, self.alignment) - - def export(self) -> bytes: - """Export represented binary image. - - :return: Byte array of binary image. - """ - if self.binary and len(self) == len(self.binary) and len(self.sub_images) == 0: - return self.binary - - if self.pattern: - ret = bytearray(self.pattern.get_block(len(self))) - else: - ret = bytearray(len(self)) - - if self.binary: - binary_view = memoryview(self.binary) - ret[: len(self.binary)] = binary_view - - for image in self.sub_images: - image_data = image.export() - ret_slice = memoryview(ret)[image.offset : image.offset + len(image_data)] - image_data_view = memoryview(image_data) - ret_slice[:] = image_data_view - - return align_block(ret, self.alignment, self.pattern) - - @staticmethod - def get_validation_schemas() -> List[Dict[str, Any]]: - """Get validation schemas list to check a supported configuration. - - :return: Validation schemas. - """ - return [DatabaseManager().db.get_schema_file("binary")] - - @staticmethod - def load_from_config( - config: Dict[str, Any], search_paths: Optional[List[str]] = None - ) -> "BinaryImage": - """Converts the configuration option into an Binary Image object. - - :param config: Description of binary image. - :param search_paths: List of paths where to search for the file, defaults to None - :return: Initialized Binary Image. - """ - name = config.get("name", "Base Image") - size = config.get("size", 0) - pattern = BinaryPattern(config.get("pattern", "zeros")) - alignment = config.get("alignment", 1) - ret = BinaryImage(name=name, size=size, pattern=pattern, alignment=alignment) - regions = config.get("regions") - if regions: - for i, region in enumerate(regions): - binary_file: Dict = region.get("binary_file") - if binary_file: - offset = binary_file.get( - "offset", ret.aligned_length(ret.alignment) - ) - name = binary_file.get("name", binary_file["path"]) - ret.add_image( - BinaryImage.load_binary_image( - binary_file["path"], - name=name, - offset=offset, - pattern=pattern, - search_paths=search_paths, - ) - ) - binary_block: Dict = region.get("binary_block") - if binary_block: - size = binary_block["size"] - offset = binary_block.get( - "offset", ret.aligned_length(ret.alignment) - ) - name = binary_block.get("name", f"Binary block(#{i})") - pattern = BinaryPattern(binary_block["pattern"]) - ret.add_image(BinaryImage(name, size, offset, pattern=pattern)) - return ret - - def save_binary_image( - self, - path: str, - file_format: str = "BIN", - ) -> None: - # pylint: disable=missing-param-doc - """Save binary data file. - - :param path: Path to the file. - :param file_format: Format of saved file ('BIN', 'HEX', 'S19'), defaults to 'BIN'. - :raises SPSDKValueError: The file format is invalid. - """ - file_format = file_format.upper() - if file_format.upper() not in ("BIN", "HEX", "S19"): - raise SPSDKValueError(f"Invalid input file format: {file_format}") - - if file_format == "BIN": - write_file(self.export(), path, mode="wb") - return - - def add_into_binary(bin_image: BinaryImage) -> None: - if bin_image.pattern: - bin_file.add_binary( - bin_image.pattern.get_block(len(bin_image)), - address=bin_image.absolute_address, - overwrite=True, - ) - - if bin_image.binary: - bin_file.add_binary( - bin_image.binary, address=bin_image.absolute_address, overwrite=True - ) - - for sub_image in bin_image.sub_images: - add_into_binary(sub_image) - - # import bincopy only if needed to save startup time - import bincopy # pylint: disable=import-outside-toplevel - - bin_file = bincopy.BinFile() - add_into_binary(self) - - if file_format == "HEX": - write_file(bin_file.as_ihex(), path) - return - - # And final supported format is....... Yes, S record from MOTOROLA - write_file(bin_file.as_srec(), path) - - @staticmethod - def generate_config_template() -> str: - """Generate configuration template. - - :return: Template to create binary merge.. - """ - return CommentedConfig( - "Binary Image Configuration template.", BinaryImage.get_validation_schemas() - ).get_template() - - @staticmethod - def load_binary_image( - path: str, - name: Optional[str] = None, - size: int = 0, - offset: int = 0, - description: Optional[str] = None, - pattern: Optional[BinaryPattern] = None, - search_paths: Optional[List[str]] = None, - alignment: int = 1, - load_bin: bool = True, - ) -> "BinaryImage": - # pylint: disable=missing-param-doc - r"""Load binary data file. - - Supported formats are ELF, HEX, SREC and plain binary - - :param path: Path to the file. - :param name: Name of Image, defaults to file name. - :param size: Image size, defaults to 0. - :param offset: Image offset in parent image, defaults to 0 - :param description: Text description of image, defaults to None - :param pattern: Optional binary pattern. - :param search_paths: List of paths where to search for the file, defaults to None - :param alignment: Optional alignment of result image - :param load_bin: Load as binary in case of every other format load fails - :raises SPSDKError: The binary file cannot be loaded. - :return: Binary data represented in BinaryImage class. - """ - path = find_file(path, search_paths=search_paths) - try: - with open(path, "rb") as f: - data = f.read(4) - except Exception as e: - raise SPSDKError(f"Error loading file: {str(e)}") from e - - # import bincopy only if needed to save startup time - import bincopy # pylint: disable=import-outside-toplevel - - bin_file = bincopy.BinFile() - try: - if data == b"\x7fELF": - bin_file.add_elf_file(path) - else: - try: - bin_file.add_file(path) - except (UnicodeDecodeError, bincopy.UnsupportedFileFormatError) as e: - if load_bin: - bin_file.add_binary_file(path) - else: - raise SPSDKError("Cannot load file as ELF, HEX or SREC") from e - except Exception as e: - raise SPSDKError(f"Error loading file: {str(e)}") from e - - img_name = name or os.path.basename(path) - img_size = size or 0 - img_descr = description or f"The image loaded from: {path} ." - bin_image = BinaryImage( - name=img_name, - size=img_size, - offset=offset, - description=img_descr, - pattern=pattern, - alignment=alignment, - ) - if len(bin_file.segments) == 0: - raise SPSDKError(f"Load of {path} failed, can't be decoded.") - - for i, segment in enumerate(bin_file.segments): - bin_image.add_image( - BinaryImage( - name=f"Segment {i}", - size=len(segment.data), - offset=segment.address, - pattern=pattern, - binary=segment.data, - parent=bin_image, - alignment=alignment, - ) - ) - # Optimize offsets in image - bin_image.update_offsets() - return bin_image diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py deleted file mode 100644 index a5112abb..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/sdio_device.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Low level sdio device.""" -import os -import time -from io import FileIO -from typing import List, Optional - -from typing_extensions import Self - -from ....exceptions import SPSDKConnectionError, SPSDKError -from ....utils.exceptions import SPSDKTimeoutError -from ....utils.interfaces.device.base import DeviceBase, logger -from ....utils.misc import Timeout - - -class SdioDevice(DeviceBase): - """SDIO device class.""" - - DEFAULT_TIMEOUT = 2000 - - def __init__( - self, - path: Optional[str] = None, - timeout: int = DEFAULT_TIMEOUT, - ) -> None: - """Initialize the SDIO interface object. - - :raises McuBootConnectionError: when the path is empty - """ - self._opened = False - # Temporarily use hard code until there is a way to retrive VID/PID - self.vid = 0x0471 - self.pid = 0x0209 - self._timeout = timeout - if path is None: - raise SPSDKConnectionError("No SDIO device path") - self.path = path - self.is_blocking = False - self.device: Optional[FileIO] = None - - @property - def timeout(self) -> int: - """Timeout property.""" - return self._timeout - - @timeout.setter - def timeout(self, value: int) -> None: - """Timeout property setter.""" - self._timeout = value - - @property - def is_opened(self) -> bool: - """Indicates whether device is open. - - :return: True if device is open, False othervise. - """ - return self.device is not None and self._opened - - def open(self) -> None: - """Open the interface with non-blocking mode. - - :raises McuBootError: if non-blocking mode is not available - :raises SPSDKError: if trying to open in non-blocking mode on non-linux os - :raises SPSDKConnectionError: if no device is available - :raises SPSDKConnectionError: if the device can not be opened - """ - logger.debug("Opening the sdio device.") - if not self._opened: - try: - self.device = open(self.path, "rb+", buffering=0) - if self.device is None: - raise SPSDKConnectionError("No device available") - if not self.is_blocking: - if not hasattr(os, "set_blocking"): - raise SPSDKError( - "Opening in non-blocking mode is available only on Linux" - ) - # pylint: disable=no-member # this is available only on Unix - os.set_blocking(self.device.fileno(), False) - self._opened = True - except Exception as error: - raise SPSDKConnectionError( - f"Unable to open device '{self.path}' VID={self.vid} PID={self.pid}" - ) from error - - def close(self) -> None: - """Close the interface. - - :raises SPSDKConnectionError: if no device is available - :raises SPSDKConnectionError: if the device can not be opened - """ - logger.debug("Closing the sdio Interface.") - if not self.device: - raise SPSDKConnectionError("No device available") - if self._opened: - try: - self.device.close() - self._opened = False - except Exception as error: - raise SPSDKConnectionError( - f"Unable to close device '{self.path}' VID={self.vid} PID={self.pid}" - ) from error - - def read(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises SPSDKTimeoutError: Time-out - :raises SPSDKConnectionError: When device was not open for reading - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for reading") - _read = self._read_blocking if self.is_blocking else self._read_non_blocking - data = _read(length=length, timeout=timeout) - if not data: - raise SPSDKTimeoutError() - logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") - return data - - def _read_blocking(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device in blocking mode. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises SPSDKConnectionError: When reading data from device fails - :raises SPSDKConnectionError: Raises if device is not opened for reading - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for writing") - logger.debug("Reading with blocking mode.") - try: - return self.device.read(length) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - def _read_non_blocking(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device in non-blocking mode. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises TimeoutError: When timeout occurs - :raises SPSDKConnectionError: When reading data from device fails - :raises SPSDKConnectionError: Raises if device is not opened for reading - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for reading") - logger.debug("Reading with non-blocking mode.") - has_data = 0 - no_data_continuous = 0 - - data = bytearray() - _timeout = Timeout(timeout or self.timeout, "ms") - while len(data) < length: - try: - buf = self.device.read(length) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - if buf is None: - time.sleep(0.05) # delay for access device - if has_data != 0: - no_data_continuous = no_data_continuous + 1 - else: - data.extend(buf) - logger.debug("expend buf") - has_data = has_data + 1 - no_data_continuous = 0 - - if no_data_continuous > 5: - break - if _timeout.overflow(): - logger.debug("SDIO interface : read timeout") - break - return bytes(data) - - def write(self, data: bytes, timeout: Optional[int] = None) -> None: - """Send data to device with non-blocking mode. - - :param data: Data to send - :param timeout: Write timeout - :raises SPSDKConnectionError: Raises an error if device is not available - :raises SPSDKConnectionError: When sending the data fails - :raises TimeoutError: When timeout occurs - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for writing.") - logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") - _write = self._write_blocking if self.is_blocking else self._write_non_blocking - _write(data=data, timeout=timeout) - - def _write_blocking(self, data: bytes, timeout: Optional[int] = None) -> None: - """Write data to device in blocking mode. - - :param data: Data to be written - :param timeout: Write timeout - - :raises SPSDKConnectionError: When writing data to device fails - :raises SPSDKConnectionError: Raises if device is not opened for writing - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for writing") - logger.debug("Writing in blocking mode") - try: - self.device.write(data) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - def _write_non_blocking(self, data: bytes, timeout: Optional[int] = None) -> None: - """Write data to device in non-blocking mode. - - :param data: Data to be written - :param timeout: Write timeout - - :raises SPSDKConnectionError: When writing data to device fails - :raises SPSDKConnectionError: Raises if device is not opened for writing - """ - if not self.device or not self.is_opened: - raise SPSDKConnectionError("Device is not opened for writing") - logger.debug("Writing in non-blocking mode") - tx_len = len(data) - _timeout = Timeout(timeout or self.timeout, "ms") - while tx_len > 0: - try: - wr_count = self.device.write(data) - time.sleep(0.05) - data = data[wr_count:] - tx_len -= wr_count - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if _timeout.overflow(): - raise SPSDKTimeoutError() - - def __str__(self) -> str: - """Return information about the SDIO interface.""" - return f"(0x{self.vid:04X}, 0x{self.pid:04X})" - - @classmethod - def scan( - cls, - device_path: str, - timeout: Optional[int] = None, - ) -> List[Self]: - """Scan connected SDIO devices. - - :param device_path: device path string - :param timeout: default read/write timeout - :return: matched SDIO device - """ - if device_path is None: - logger.debug("No sdio path has been defined.") - devices = [] - try: - logger.debug(f"Checking path: {device_path}") - device = cls(path=device_path, timeout=timeout or cls.DEFAULT_TIMEOUT) - device.open() - device.close() - devices = [device] if device else [] - except Exception as e: # pylint: disable=broad-except - logger.debug(f"{type(e).__name__}: {e}") - devices = [] - return devices diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py deleted file mode 100644 index 0237cce2..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/serial_device.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2023-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Low level serial device.""" -import logging -from typing import List, Optional - -from serial import Serial, SerialException, SerialTimeoutException -from serial.tools.list_ports import comports -from typing_extensions import Self - -from ....exceptions import SPSDKConnectionError -from ....utils.exceptions import SPSDKTimeoutError -from ....utils.interfaces.device.base import DeviceBase - -logger = logging.getLogger(__name__) - - -class SerialDevice(DeviceBase): - """Serial device class.""" - - default_baudrate = 115200 - default_timeout = 5000 - - def __init__( - self, - port: Optional[str] = None, - timeout: int = default_timeout, - baudrate: int = default_baudrate, - ): - """Initialize the UART interface. - - :param port: name of the serial port, defaults to None - :param baudrate: speed of the UART interface, defaults to 115200 - :param timeout: read/write timeout in milliseconds, defaults to 1000 - :raises SPSDKConnectionError: when there is no port available - """ - super().__init__() - self._timeout = timeout - try: - timeout_s = timeout / 1000 - self._device = Serial( - port=port, timeout=timeout_s, write_timeout=timeout_s, baudrate=baudrate - ) - self.expect_status = True - except SerialException as se: - logger.debug(f"Exception occurred during device opening: {se}") - self.expect_status = False - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - @property - def timeout(self) -> int: - """Timeout property.""" - return self._timeout - - @timeout.setter - def timeout(self, value: int) -> None: - """Timeout property setter.""" - self._timeout = value - self._device.timeout = value / 1000 - self._device.write_timeout = value / 1000 - - @property - def is_opened(self) -> bool: - """Indicates whether device is open. - - :return: True if device is open, False otherwise. - """ - if self.expect_status == False: - return False - else: - return self._device.is_open - - def open(self) -> None: - """Open the UART interface. - - :raises SPSDKConnectionError: when opening device fails - """ - if not self.is_opened: - try: - self._device.open() - except Exception as e: - self.close() - raise SPSDKConnectionError(str(e)) from e - - def close(self) -> None: - """Close the UART interface. - - :raises SPSDKConnectionError: when closing device fails - """ - if self.is_opened: - try: - self._device.reset_input_buffer() - self._device.reset_output_buffer() - self._device.close() - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - def read(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises SPSDKTimeoutError: Time-out - :raises SPSDKConnectionError: When reading data from device fails - """ - if not self.is_opened: - raise SPSDKConnectionError("Device is not opened for reading") - try: - data = self._device.read(length) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if not data: - raise SPSDKTimeoutError() - logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") - return data - - def write(self, data: bytes, timeout: Optional[int] = None) -> None: - """Send data to device. - - :param data: Data to send - :param timeout: Write timeout - :raises SPSDKTimeoutError: when sending of data times-out - :raises SPSDKConnectionError: when send data to device fails - """ - if not self.is_opened: - raise SPSDKConnectionError("Device is not opened for reading") - logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") - try: - self._device.reset_input_buffer() - self._device.reset_output_buffer() - self._device.write(data) - self._device.flush() - except SerialTimeoutException as e: - raise SPSDKTimeoutError( - f"Write timeout error. The timeout is set to {self._device.write_timeout} s. Consider increasing it." - ) from e - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - def __str__(self) -> str: - """Return information about the UART interface. - - :return: information about the UART interface - :raises SPSDKConnectionError: when information can not be collected from device - """ - try: - return self._device.port - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - - @classmethod - def scan( - cls, - port: Optional[str] = None, - baudrate: Optional[int] = None, - timeout: Optional[int] = None, - ) -> List[Self]: - """Scan connected serial ports. - - Returns list of serial ports with devices that respond to PING command. - If 'port' is specified, only that serial port is checked - If no devices are found, return an empty list. - - :param port: name of preferred serial port, defaults to None - :param baudrate: speed of the UART interface, defaults to 56700 - :param timeout: timeout in milliseconds, defaults to 5000 - :return: list of interfaces responding to the PING command - """ - baudrate = baudrate or cls.default_baudrate - timeout = timeout or 5000 - if port: - device = cls._check_port(port, baudrate, timeout) - devices = [device] if device else [] - else: - all_ports = [ - cls._check_port(comport.device, baudrate, timeout) - for comport in comports(include_links=True) - ] - devices = list(filter(None, all_ports)) - return devices - - @classmethod - def _check_port(cls, port: str, baudrate: int, timeout: int) -> Optional[Self]: - """Check if device on comport 'port' responds to PING command. - - :param port: name of port to check - :param baudrate: speed of the UART interface, defaults to 56700 - :param timeout: timeout in milliseconds - :return: None if device doesn't respond to PING, instance of Interface if it does - """ - try: - logger.debug( - f"Checking port: {port}, baudrate: {baudrate}, timeout: {timeout}" - ) - device = cls(port=port, baudrate=baudrate, timeout=timeout) - device.open() - device.close() - return device - except Exception as e: # pylint: disable=broad-except - logger.debug(f"{type(e).__name__}: {e}") - return None diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py deleted file mode 100644 index 38b48c6f..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/device/usbsio_device.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright (c) 2019-2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Low level usbsio device.""" -import logging -import re -from dataclasses import dataclass -from typing import List, Optional, Union - -import libusbsio -from libusbsio.libusbsio import LIBUSBSIO -from typing_extensions import Self - -from ....exceptions import SPSDKConnectionError, SPSDKError, SPSDKValueError -from ....utils.exceptions import SPSDKTimeoutError -from ....utils.interfaces.device.base import DeviceBase -from ....utils.misc import value_to_int -from ....utils.usbfilter import USBDeviceFilter - -logger = logging.getLogger(__name__) - - -@dataclass -class ScanArgs: - """Scan arguments dataclass.""" - - config: str - - @classmethod - def parse(cls, params: str) -> Self: - """Parse given scanning parameters into ScanArgs class. - - :param params: Parameters as a string - """ - return cls(config=params) - - -class UsbSioDevice(DeviceBase): - """USBSIO device class.""" - - def __init__( - self, dev: int = 0, config: Optional[str] = None, timeout: int = 5000 - ) -> None: - """Initialize the Interface object. - - :param dev: device index to be used, default is set to 0 - :param config: configuration string identifying spi or i2c SIO interface - :param timeout: read timeout in milliseconds, defaults to 5000 - :raises SPSDKError: When LIBUSBSIO device is not opened. - """ - # device is the LIBUSBSIO.PORT instance (LIBUSBSIO.SPI or LIBUSBSIO.I2C class) - self.port: Optional[Union[LIBUSBSIO.SPI, LIBUSBSIO.I2C]] = None - - # work with the global LIBUSBSIO instance - self.dev_ix = dev - self.sio = self._get_usbsio() - self._timeout = timeout - - # store USBSIO configuration and version - self.config = config - - @property - def timeout(self) -> int: - """Timeout property.""" - return self._timeout - - @timeout.setter - def timeout(self, value: int) -> None: - """Timeout property setter.""" - self._timeout = value - - @property - def is_opened(self) -> bool: - """Indicates whether device is open. - - :return: True if device is open, False othervise. - """ - return bool(self.port) - - def close(self) -> None: - """Close the interface.""" - if self.port: - self.port.Close() - self.port = None - self.sio.Close() - # re-init the libusb to prepare it for next open - self.sio.GetNumPorts() - - def __str__(self) -> str: - """Return string containing information about the interface.""" - class_name = self.__class__.__name__ - config = f":'{self.config}'" if self.config else "" - return f"libusbsio interface ({class_name}){config}" - - @staticmethod - def get_interface_cfg(config: str, interface: str) -> str: - """Return part of interface config. - - :param config: Full config of LIBUSBSIO - :param interface: Name of interface to find. - :return: Part with interface config. - """ - i = config.rfind(interface) - if i < 0: - return "" - return config[i:] - - @staticmethod - def _get_usbsio() -> LIBUSBSIO: - """Wraps getting USBSIO library to raise SPSDK errors in case of problem. - - :return: LIBUSBSIO object - :raises SPSDKError: When libusbsio library error or if no bridge device found - """ - try: - # get the global singleton instance of LIBUSBSIO library - libusbsio_logger = logging.getLogger("libusbsio") - return libusbsio.usbsio(loglevel=libusbsio_logger.getEffectiveLevel()) - except libusbsio.LIBUSBSIO_Exception as e: - raise SPSDKError(f"Error in libusbsio interface: {e}") from e - except Exception as e: - raise SPSDKError(str(e)) from e - - @classmethod - def scan( - cls, config: Optional[str] = None, timeout: int = 5000 - ) -> List[Union["UsbSioSPIDevice", "UsbSioI2CDevice"]]: - """Scan connected USB-SIO bridge devices. - - :param config: Configuration string identifying spi or i2c SIO interface - and could filter out USB devices - :param timeout: Read timeout in milliseconds, defaults to 5000 - :return: List of matching UsbSio devices - :raises SPSDKError: When libusbsio library error or if no bridge device found - :raises SPSDKValueError: Invalid configuration detected. - """ - cfg = config.split(",") if config else [] - re_spi = re.compile(r"^spi(?P\d*)") - re_i2c = re.compile(r"^i2c(?P\d*)") - spi = None - i2c = None - for cfg_part in cfg: - match_i2c = re_i2c.match(cfg_part.lower()) - if match_i2c: - i2c = value_to_int(match_i2c.group("index"), 0) - match_spi = re_spi.match(cfg_part.lower()) - if match_spi: - spi = value_to_int(match_spi.group("index"), 0) - if i2c is not None and spi is not None: - raise SPSDKValueError( - f"Cannot be specified spi and i2c together in configuration: {cfg}" - ) - intf_specified = i2c is not None or spi is not None - - port_indexes = cls.get_usbsio_devices(config) - sio = cls._get_usbsio() - devices: List[Union["UsbSioSPIDevice", "UsbSioI2CDevice"]] = [] - for port in port_indexes: - if not sio.Open(port): - raise SPSDKError(f"Cannot open libusbsio bridge {port}.") - i2c_ports = sio.GetNumI2CPorts() - if i2c_ports: - if i2c is not None: - devices.append( - UsbSioI2CDevice( - dev=port, port=i2c, config=config, timeout=timeout - ) - ) - elif not intf_specified: - devices.extend( - [ - UsbSioI2CDevice(dev=port, port=p, timeout=timeout) - for p in range(i2c_ports) - ] - ) - spi_ports = sio.GetNumSPIPorts() - if spi_ports: - if spi is not None: - devices.append( - UsbSioSPIDevice( - dev=port, port=spi, config=config, timeout=timeout - ) - ) - elif not intf_specified: - devices.extend( - [ - UsbSioSPIDevice(dev=port, port=p, timeout=timeout) - for p in range(spi_ports) - ] - ) - if sio.Close() < 0: - raise SPSDKError(f"Cannot close libusbsio bridge {port}.") - # re-init the libusb to prepare it for next open - sio.GetNumPorts() - return devices - - @classmethod - def get_usbsio_devices(cls, config: Optional[str] = None) -> List[int]: - """Returns list of ports indexes of USBSIO devices. - - It could be filtered by standard SPSDK USB filters. - - :param config: Could contain USB filter configuration, defaults to None - :return: List of port indexes of founded USBSIO device - """ - - def _filter_usb(sio: LIBUSBSIO, ports: List[int], flt: str) -> List[int]: - """Filter the LIBUSBSIO device. - - :param sio: LIBUSBSIO instance. - :param ports: Input list of LIBUSBSIO available ports. - :param flt: Filter string (PATH, PID/VID, SERIAL_NUMBER) - :raises SPSDKError: When libusbsio library error or if no bridge device found - :return: List with selected device, empty list otherwise. - """ - usb_filter = USBDeviceFilter(flt.casefold()) - port_indexes = [] - for port in ports: - info = sio.GetDeviceInfo(port) - if not info: - raise SPSDKError( - f"Cannot retrive information from LIBUSBSIO device {port}." - ) - dev_info = { - "vendor_id": info.vendor_id, - "product_id": info.product_id, - "serial_number": info.serial_number, - "path": info.path, - } - if usb_filter.compare(dev_info): - port_indexes.append(port) - break - return port_indexes - - cfg = config.split(",") if config else [] - port_indexes = [] - - sio = UsbSioDevice._get_usbsio() - # it may already be open (?), in that case, just close it - We are scan function! - if sio.IsOpen(): - sio.Close() - - port_indexes.extend(list(range(sio.GetNumPorts()))) - - # filter out the USB devices - if cfg and cfg[0] == "usb": - port_indexes = _filter_usb(sio, port_indexes, cfg[1]) - - return port_indexes - - -class UsbSioSPIDevice(UsbSioDevice): - """USBSIO SPI interface.""" - - def __init__( - self, - config: Optional[str] = None, - dev: int = 0, - port: int = 0, - ssel_port: int = 0, - ssel_pin: int = 15, - speed_khz: int = 1000, - cpol: int = 1, - cpha: int = 1, - timeout: int = 5000, - ) -> None: - """Initialize the UsbSioSPI Interface object. - - :param config: configuration string passed from command line - :param dev: device index to be used, default is set to 0 - :param port: default SPI port to be used, typically 0 as only one port is supported by LPCLink2/MCULink - :param ssel_port: bridge GPIO port used to drive SPI SSEL signal - :param ssel_pin: bridge GPIO pin used to drive SPI SSEL signal - :param speed_khz: SPI clock speed in kHz - :param cpol: SPI clock polarity mode - :param cpha: SPI clock phase mode - :param timeout: read timeout in milliseconds, defaults to 5000 - :raises SPSDKError: When port configuration cannot be parsed - """ - super().__init__(dev=dev, config=config, timeout=timeout) - - # default configuration taken from parameters (and their default values) - self.spi_port = port - self.spi_sselport = ssel_port - self.spi_sselpin = ssel_pin - self.spi_speed_khz = speed_khz - self.spi_cpol = cpol - self.spi_cpha = cpha - - # values can be also overridden by a configuration string - if config: - # config format: spi[,,,,,] - cfg = self.get_interface_cfg(config, "spi").split(",") - try: - self.spi_sselport = int(cfg[1], 0) - self.spi_sselpin = int(cfg[2], 0) - self.spi_speed_khz = int(cfg[3], 0) - self.spi_cpol = int(cfg[4], 0) - self.spi_cpha = int(cfg[5], 0) - except IndexError: - pass - except Exception as e: - raise SPSDKError( - "Cannot parse lpcusbsio SPI parameters.\n" - "Expected: spi[,,,,,]\n" - f"Given: {config}" - ) from e - - def open(self) -> None: - """Open the interface.""" - if not self.sio.IsOpen(): - self.sio.Open(self.dev_ix) - - self.port: LIBUSBSIO.SPI = self.sio.SPI_Open( - portNum=self.spi_port, - busSpeed=self.spi_speed_khz * 1000, - cpol=self.spi_cpol, - cpha=self.spi_cpha, - ) - if not self.port: - raise SPSDKError("Cannot open lpcusbsio SPI interface.\n") - - def read(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises SPSDKConnectionError: When reading data from device fails - :raises TimeoutError: When no data received - """ - try: - (data, result) = self.port.Transfer( - devSelectPort=self.spi_sselport, - devSelectPin=self.spi_sselpin, - txData=None, - size=length, - ) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if result < 0 or not data: - raise SPSDKTimeoutError() - logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") - return data - - def write(self, data: bytes, timeout: Optional[int] = None) -> None: - """Send data to device. - - :param data: Data to send - :param timeout: Write timeout - :raises SPSDKConnectionError: When sending the data fails - :raises SPSDKTimeoutError: When data could not be written - """ - logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") - try: - (dummy, result) = self.port.Transfer( - devSelectPort=self.spi_sselport, - devSelectPin=self.spi_sselpin, - txData=data, - ) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if result < 0: - raise SPSDKTimeoutError() - - -class UsbSioI2CDevice(UsbSioDevice): - """USBSIO I2C interface.""" - - def __init__( - self, - config: Optional[str] = None, - dev: int = 0, - port: int = 0, - address: int = 0x10, - speed_khz: int = 100, - timeout: int = 5000, - ) -> None: - """Initialize the UsbSioI2C Interface object. - - :param config: configuration string passed from command line - :param dev: device index to be used, default is set to 0 - :param port: default I2C port to be used, typically 0 as only one port is supported by LPCLink2/MCULink - :param address: I2C target device address - :param speed_khz: I2C clock speed in kHz - :param timeout: read timeout in milliseconds, defaults to 5000 - :raises SPSDKError: When port configuration cannot be parsed - """ - super().__init__(dev=dev, config=config, timeout=timeout) - - # default configuration taken from parameters (and their default values) - self.i2c_port = port - self.i2c_address = address - self.i2c_speed_khz = speed_khz - - # values can be also overridden by a configuration string - if config: - # config format: i2c[,
,] - cfg = self.get_interface_cfg(config, "i2c").split(",") - try: - self.i2c_address = int(cfg[1], 0) - self.i2c_speed_khz = int(cfg[2], 0) - except IndexError: - pass - except Exception as e: - raise SPSDKError( - "Cannot parse lpcusbsio I2C parameters.\n" - "Expected: i2c[,
,]\n" - f"Given: {config}" - ) from e - - def open(self) -> None: - """Open the interface.""" - if not self.sio.IsOpen(): - self.sio.Open(self.dev_ix) - self.port: LIBUSBSIO.I2C = self.sio.I2C_Open( - clockRate=self.i2c_speed_khz * 1000, portNum=self.i2c_port - ) - if not self.port: - raise SPSDKError("Cannot open lpcusbsio I2C interface.\n") - - def read(self, length: int, timeout: Optional[int] = None) -> bytes: - """Read 'length' amount for bytes from device. - - :param length: Number of bytes to read - :param timeout: Read timeout - :return: Data read from the device - :raises SPSDKConnectionError: When reading data from device fails - :raises SPSDKTimeoutError: When no data received - """ - try: - (data, result) = self.port.DeviceRead( - devAddr=self.i2c_address, rxSize=length - ) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if result < 0 or not data: - raise SPSDKTimeoutError() - logger.debug(f"<{' '.join(f'{b:02x}' for b in data)}>") - return data - - def write(self, data: bytes, timeout: Optional[int] = None) -> None: - """Send data to device. - - :param data: Data to send - :param timeout: Write timeout - :raises SPSDKConnectionError: When sending the data fails - :raises TimeoutError: When data NAKed or could not be written - """ - logger.debug(f"[{' '.join(f'{b:02x}' for b in data)}]") - try: - result = self.port.DeviceWrite(devAddr=self.i2c_address, txData=data) - except Exception as e: - raise SPSDKConnectionError(str(e)) from e - if result < 0: - raise SPSDKTimeoutError() diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py deleted file mode 100644 index ede09d3b..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/interfaces/scanner_helper.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2023 NXP -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Helper module used for supporting the scanning.""" - -from dataclasses import dataclass -from typing import Dict, Optional, Tuple - -from ...exceptions import SPSDKKeyError - - -def parse_plugin_config(plugin_conf: str) -> Tuple[str, str]: - """Extract 'identifier' from plugin params and build the params back to original format. - - :param plugin_conf: Plugin configuration string as given on command line - :return: Tuple with identifier and params - """ - params_dict: Dict[str, str] = dict([tuple(p.split("=")) for p in plugin_conf.split(",")]) # type: ignore - if "identifier" not in params_dict: - raise SPSDKKeyError("Plugin parameter must contain 'identifier' key") - identifier = params_dict.pop("identifier") - params = ",".join([f"{key}={value}" for key, value in params_dict.items()]) - return identifier, params - - -@dataclass -class InterfaceParams: - """Interface input parameters.""" - - identifier: str - is_defined: bool - params: Optional[str] = None - extra_params: Optional[str] = None diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py deleted file mode 100644 index 907a23a2..00000000 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/registers.py +++ /dev/null @@ -1,1373 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# -# Copyright 2020-2024 NXP -# -# SPDX-License-Identifier: BSD-3-Clause -"""Module to handle registers descriptions with support for XML files.""" - -import logging -import re -import xml.etree.ElementTree as ET -from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union -from xml.dom import minidom - -from ..exceptions import SPSDKError, SPSDKValueError -from ..utils.exceptions import ( - SPSDKRegsError, - SPSDKRegsErrorBitfieldNotFound, - SPSDKRegsErrorEnumNotFound, - SPSDKRegsErrorRegisterGroupMishmash, - SPSDKRegsErrorRegisterNotFound, -) -from ..utils.images import BinaryImage, BinaryPattern -from ..utils.misc import ( - Endianness, - format_value, - get_bytes_cnt_of_int, - value_to_bool, - value_to_bytes, - value_to_int, - write_file, -) - -HTMLDataElement = Mapping[str, Union[str, dict, list]] -HTMLData = List[HTMLDataElement] - -logger = logging.getLogger(__name__) - - -class RegsEnum: - """Storage for register enumerations.""" - - def __init__( - self, name: str, value: Any, description: str, max_width: int = 0 - ) -> None: - """Constructor of RegsEnum class. Used to store enumeration information of bitfield. - - :param name: Name of enumeration. - :param value: Value of enumeration. - :param description: Text description of enumeration. - :param max_width: Maximal width of enum value used to format output - :raises SPSDKRegsError: Invalid input value. - """ - self.name = name or "N/A" - try: - self.value = value_to_int(value) - except (TypeError, ValueError, SPSDKError) as exc: - raise SPSDKRegsError(f"Invalid Enum Value: {value}") from exc - self.description = description or "N/A" - self.max_width = max_width - - @classmethod - def from_xml_element(cls, xml_element: ET.Element, maxwidth: int = 0) -> "RegsEnum": - """Initialization Enum by XML ET element. - - :param xml_element: Input XML subelement with enumeration data. - :param maxwidth: The maximal width of bitfield for this enum (used for formatting). - :return: The instance of this class. - :raises SPSDKRegsError: Error during enum XML parsing. - """ - name = xml_element.attrib.get("name", "N/A") - if "value" not in xml_element.attrib: - raise SPSDKRegsError(f"Missing Enum Value Key for {name}.") - - raw_val = xml_element.attrib["value"] - try: - value = value_to_int(raw_val) - except (TypeError, ValueError, SPSDKError) as exc: - raise SPSDKRegsError(f"Invalid Enum Value: {raw_val}") from exc - - description = xml_element.attrib.get("description", "N/A").replace( - " ", "\n" - ) - - return cls(name, value, description, maxwidth) - - def get_value_int(self) -> int: - """Method returns Integer value of enum. - - :return: Integer value of Enum. - """ - return self.value - - def get_value_str(self) -> str: - """Method returns formatted value. - - :return: Formatted string with enum value. - """ - return format_value(self.value, self.max_width) - - def add_et_subelement(self, parent: ET.Element) -> None: - """Creates the register XML structure in ElementTree. - - :param parent: The parent object of ElementTree. - """ - element = ET.SubElement(parent, "bit_field_value") - element.set("name", self.name) - element.set("value", self.get_value_str()) - element.set("description", self.description) - - def __str__(self) -> str: - """Overrides 'ToString()' to print register. - - :return: Friendly string with enum information. - """ - output = "" - output += f"Name: {self.name}\n" - output += f"Value: {self.get_value_str()}\n" - output += f"Description: {self.description}\n" - - return output - - -class ConfigProcessor: - """Base class for processing configuration data.""" - - NAME = "NOP" - - def __init__(self, description: str = "") -> None: - """Initialize the processor.""" - self.description = description - - def pre_process(self, value: int) -> int: - """Pre-process value coming from config file.""" - return value - - def post_process(self, value: int) -> int: - """Post-process value going to config file.""" - return value - - def width_update(self, value: int) -> int: - """Update bit-width of value going to config file.""" - return value - - @classmethod - def get_method_name(cls, config_string: str) -> str: - """Return config processor method name.""" - return config_string.split(":")[0] - - @classmethod - def get_params(cls, config_string: str) -> Dict[str, int]: - """Return config processor method parameters.""" - - def split_params(param: str) -> Tuple[str, str]: - """Split key=value pair into a tuple.""" - parts = param.split("=") - if len(parts) != 2: - raise SPSDKRegsError( - f"Invalid param setting: '{param}'. Expected format '='" - ) - return (parts[0], parts[1]) - - parts = config_string.split(";", maxsplit=1)[0].split(":") - if len(parts) == 1: - return {} - params = parts[1].split(",") - params_dict: Dict[str, str] = dict(split_params(p) for p in params) - return {key.lower(): value_to_int(value) for key, value in params_dict.items()} - - @classmethod - def get_description(cls, config_string: str) -> str: - """Return extra description for config processor.""" - parts = config_string.partition(";") - return parts[2].replace("DESC=", "") - - @classmethod - def from_str(cls, config_string: str) -> "ConfigProcessor": - """Create config processor instance from configuration string.""" - return cls(config_string) - - @classmethod - def from_xml(cls, element: ET.Element) -> Optional["ConfigProcessor"]: - """Create config processor from XML data entry.""" - processor_node = element.find("alias[@type='CONFIG_PREPROCESS']") - if processor_node is None: - return None - if "value" not in processor_node.attrib: - raise SPSDKRegsError("CONFIG_PREPROCESS alias node doesn't have a value") - config_string = processor_node.attrib["value"] - method_name = cls.get_method_name(config_string=config_string) - for klass in cls.__subclasses__(): - if klass.NAME == method_name: - return klass.from_str(config_string=config_string) - return None - - -class ShiftRightConfigProcessor(ConfigProcessor): - """Config processor performing the right-shift operation.""" - - NAME = "SHIFT_RIGHT" - - def __init__(self, count: int, description: str = "") -> None: - """Initialize the right-shift config processor. - - :param count: Count of bit for shift operation - :param description: Extra description for config processor, defaults to "" - """ - super().__init__( - description=description - or f"Actual binary value is shifted by {count} bits to right." - ) - self.count = count - - def pre_process(self, value: int) -> int: - """Pre-process value coming from config file.""" - return value >> self.count - - def post_process(self, value: int) -> int: - """Post-process value going to config file.""" - return value << self.count - - def width_update(self, value: int) -> int: - """Update bit-width of value going to config file.""" - return value + self.count - - @classmethod - def from_str(cls, config_string: str) -> "ShiftRightConfigProcessor": - """Create config processor instance from configuration string.""" - name = cls.get_method_name(config_string=config_string) - if name != cls.NAME: - raise SPSDKRegsError(f"Invalid method name '{name}' expected {cls.NAME}") - params = cls.get_params(config_string=config_string) - if "count" not in params: - raise SPSDKRegsError(f"{cls.NAME} requires the COUNT parameter") - description = cls.get_description(config_string=config_string) - return cls(count=value_to_int(params["count"]), description=description) - - -class RegsBitField: - """Storage for register bitfields.""" - - def __init__( - self, - parent: "RegsRegister", - name: str, - offset: int, - width: int, - description: Optional[str] = None, - reset_val: Any = "0", - access: str = "RW", - hidden: bool = False, - config_processor: Optional[ConfigProcessor] = None, - ) -> None: - """Constructor of RegsBitField class. Used to store bitfield information. - - :param parent: Parent register of bitfield. - :param name: Name of bitfield. - :param offset: Bit offset of bitfield. - :param width: Bit width of bitfield. - :param description: Text description of bitfield. - :param reset_val: Reset value of bitfield. - :param access: Access type of bitfield. - :param hidden: The bitfield will be hidden from standard searches. - """ - self.parent = parent - self.name = name or "N/A" - self.offset = offset - self.width = width - self.description = description or "N/A" - self.reset_value = value_to_int(reset_val, 0) - self.access = access - self.hidden = hidden - self._enums: List[RegsEnum] = [] - self.config_processor = config_processor or ConfigProcessor() - self.config_width = self.config_processor.width_update(width) - self.set_value(self.reset_value, raw=True) - - @classmethod - def from_xml_element( - cls, xml_element: ET.Element, parent: "RegsRegister" - ) -> "RegsBitField": - """Initialization register by XML ET element. - - :param xml_element: Input XML subelement with register data. - :param parent: Reference to parent RegsRegister object. - :return: The instance of this class. - """ - name = xml_element.attrib.get("name", "N/A") - offset = value_to_int(xml_element.attrib.get("offset", 0)) - width = value_to_int(xml_element.attrib.get("width", 0)) - description = xml_element.attrib.get("description", "N/A").replace( - " ", "\n" - ) - access = xml_element.attrib.get("access", "R/W") - reset_value = value_to_int(xml_element.attrib.get("reset_value", 0)) - hidden = xml_element.tag != "bit_field" - config_processor = ConfigProcessor.from_xml(xml_element) - - bitfield = cls( - parent, - name, - offset, - width, - description, - reset_value, - access, - hidden, - config_processor, - ) - - for xml_enum in xml_element.findall("bit_field_value"): - bitfield.add_enum(RegsEnum.from_xml_element(xml_enum, width)) - - return bitfield - - def has_enums(self) -> bool: - """Returns if the bitfields has enums. - - :return: True is has enums, False otherwise. - """ - return len(self._enums) > 0 - - def get_enums(self) -> List[RegsEnum]: - """Returns bitfield enums. - - :return: List of bitfield enumeration values. - """ - return self._enums - - def add_enum(self, enum: RegsEnum) -> None: - """Add bitfield enum. - - :param enum: New enumeration value for bitfield. - """ - self._enums.append(enum) - - def get_value(self) -> int: - """Returns integer value of the bitfield. - - :return: Current value of bitfield. - """ - reg_val = self.parent.get_value(raw=False) - value = reg_val >> self.offset - mask = (1 << self.width) - 1 - value = value & mask - value = self.config_processor.post_process(value) - return value - - def get_reset_value(self) -> int: - """Returns integer reset value of the bitfield. - - :return: Reset value of bitfield. - """ - return self.reset_value - - def set_value(self, new_val: Any, raw: bool = False) -> None: - """Updates the value of the bitfield. - - :param new_val: New value of bitfield. - :param raw: If set, no automatic modification of value is applied. - :raises SPSDKValueError: The input value is out of range. - """ - new_val_int = value_to_int(new_val) - new_val_int = self.config_processor.pre_process(new_val_int) - if new_val_int > 1 << self.width: - raise SPSDKValueError("The input value is out of bitfield range") - reg_val = self.parent.get_value(raw=raw) - - mask = ((1 << self.width) - 1) << self.offset - reg_val = reg_val & ~mask - value = (new_val_int << self.offset) & mask - reg_val = reg_val | value - self.parent.set_value(reg_val, raw) - - def set_enum_value(self, new_val: str, raw: bool = False) -> None: - """Updates the value of the bitfield by its enum value. - - :param new_val: New enum value of bitfield. - :param raw: If set, no automatic modification of value is applied. - :raises SPSDKRegsErrorEnumNotFound: Input value cannot be decoded. - """ - try: - val_int = self.get_enum_constant(new_val) - except SPSDKRegsErrorEnumNotFound: - # Try to decode standard input - try: - val_int = value_to_int(new_val) - except TypeError: - raise SPSDKRegsErrorEnumNotFound # pylint: disable=raise-missing-from - self.set_value(val_int, raw) - - def get_enum_value(self) -> Union[str, int]: - """Returns enum value of the bitfield. - - :return: Current value of bitfield. - """ - value = self.get_value() - for enum in self._enums: - if enum.get_value_int() == value: - return enum.name - # return value - return self.get_hex_value() - - def get_hex_value(self) -> str: - """Get the value of register in string hex format. - - :return: Hexadecimal value of register. - """ - fmt = f"0{self.config_width // 4}X" - val = f"0x{format(self.get_value(), fmt)}" - return val - - def get_enum_constant(self, enum_name: str) -> int: - """Returns constant representation of enum by its name. - - :return: Constant of enum. - :raises SPSDKRegsErrorEnumNotFound: The enum has not been found. - """ - for enum in self._enums: - if enum.name == enum_name: - return enum.get_value_int() - - raise SPSDKRegsErrorEnumNotFound( - f"The enum for {enum_name} has not been found." - ) - - def get_enum_names(self) -> List[str]: - """Returns list of the enum strings. - - :return: List of enum names. - """ - return [x.name for x in self._enums] - - def add_et_subelement(self, parent: ET.Element) -> None: - """Creates the register XML structure in ElementTree. - - :param parent: The parent object of ElementTree. - """ - element = ET.SubElement( - parent, "reserved_bit_field" if self.hidden else "bit_field" - ) - element.set("offset", hex(self.offset)) - element.set("width", str(self.width)) - element.set("name", self.name) - element.set("access", self.access) - element.set("reset_value", format_value(self.reset_value, self.width)) - element.set("description", self.description) - for enum in self._enums: - enum.add_et_subelement(element) - - def __str__(self) -> str: - """Override 'ToString()' to print register. - - :return: Friendly looking string that describes the bitfield. - """ - output = "" - output += f"Name: {self.name}\n" - output += f"Offset: {self.offset} bits\n" - output += f"Width: {self.width} bits\n" - output += f"Access: {self.access} bits\n" - output += f"Reset val:{self.reset_value}\n" - output += f"Description: \n {self.description}\n" - if self.hidden: - output += "This is hidden bitfield!\n" - - i = 0 - for enum in self._enums: - output += f"Enum #{i}: \n" + str(enum) - i += 1 - - return output - - -class RegsRegister: - """Initialization register by input information.""" - - def __init__( - self, - name: str, - offset: int, - width: int, - description: Optional[str] = None, - reverse: bool = False, - access: Optional[str] = None, - config_as_hexstring: bool = False, - otp_index: Optional[int] = None, - reverse_subregs_order: bool = False, - base_endianness: Endianness = Endianness.BIG, - alt_widths: Optional[List[int]] = None, - ) -> None: - """Constructor of RegsRegister class. Used to store register information. - - :param name: Name of register. - :param offset: Byte offset of register. - :param width: Bit width of register. - :param description: Text description of register. - :param reverse: Multi byte register value could be printed in reverse order. - :param access: Access type of register. - :param config_as_hexstring: Config is stored as a hex string. - :param otp_index: Index of OTP fuse. - :param reverse_subregs_order: Reverse order of sub registers. - :param base_endianness: Base endianness for bytes import/export of value. - :param alt_widths: List of alternative widths. - """ - if width % 8 != 0: - raise SPSDKValueError( - "SPSDK Register supports only widths in multiply 8 bits." - ) - self.name = name - self.offset = offset - self.width = width - self.description = description or "N/A" - self.access = access or "RW" - self.reverse = reverse - self._bitfields: List[RegsBitField] = [] - self._set_value_hooks: List = [] - self._value = 0 - self._reset_value = 0 - self.config_as_hexstring = config_as_hexstring - self.otp_index = otp_index - self.reverse_subregs_order = reverse_subregs_order - self.base_endianness = base_endianness - self.alt_widths = alt_widths - self._alias_names: List[str] = [] - - # Grouped register members - self.sub_regs: List["RegsRegister"] = [] - self._sub_regs_width_init = False - self._sub_regs_width = 0 - - def __eq__(self, obj: Any) -> bool: - """Compare if the objects has same settings.""" - if not isinstance(obj, self.__class__): - return False - if obj.name != self.name: - return False - if obj.width != self.width: - return False - if obj.reverse != self.reverse: - return False - if obj._value != self._value: - return False - if obj._reset_value != self._reset_value: - return False - return True - - @classmethod - def from_xml_element(cls, xml_element: ET.Element) -> "RegsRegister": - """Initialization register by XML ET element. - - :param xml_element: Input XML subelement with register data. - :return: The instance of this class. - """ - name = xml_element.attrib.get("name", "N/A") - offset = value_to_int(xml_element.attrib.get("offset", 0)) - width = value_to_int(xml_element.attrib.get("width", 0)) - description = xml_element.attrib.get("description", "N/A").replace( - " ", "\n" - ) - reverse = (xml_element.attrib.get("reversed", "False")) == "True" - access = xml_element.attrib.get("access", "N/A") - otp_index_raw = xml_element.attrib.get("otp_index") - otp_index = None - if otp_index_raw: - otp_index = value_to_int(otp_index_raw) - reg = cls( - name, - offset, - width, - description, - reverse, - access, - otp_index=otp_index, - ) - value = xml_element.attrib.get("value") - if value: - reg.set_value(value) - - if xml_element.text: - xml_bitfields = xml_element.findall("bit_field") - xml_bitfields.extend(xml_element.findall("reserved_bit_field")) - xml_bitfields_len = len(xml_bitfields) - for xml_bitfield in xml_bitfields: - bitfield = RegsBitField.from_xml_element(xml_bitfield, reg) - if ( - xml_bitfields_len == 1 - and bitfield.width == reg.width - and not bitfield.has_enums() - ): - if len(reg.description) < len(bitfield.description): - reg.description = bitfield.description - reg.access = bitfield.access - reg._reset_value = bitfield.reset_value - else: - if reg.access == "N/A": - reg.access = "Bitfields depended" - reg.add_bitfield(bitfield) - return reg - - def add_alias(self, alias: str) -> None: - """Add alias name to register. - - :param alias: Register name alias. - """ - if not alias in self._alias_names: - self._alias_names.append(alias) - - def has_group_registers(self) -> bool: - """Returns true if register is compounded from sub-registers. - - :return: True if register has sub-registers, False otherwise. - """ - return len(self.sub_regs) > 0 - - def add_group_reg(self, reg: "RegsRegister") -> None: - """Add group element for this register. - - :param reg: Register member of this register group. - :raises SPSDKRegsErrorRegisterGroupMishmash: When any inconsistency is detected. - """ - first_member = not self.has_group_registers() - if first_member: - if self.offset == 0: - self.offset = reg.offset - if self.width == 0: - self.width = reg.width - else: - self._sub_regs_width_init = True - self._sub_regs_width = reg.width - if self.access == "RW": - self.access = reg.access - else: - # There is strong rule that supported group MUST be in one row in memory! - if not self._sub_regs_width_init: - if self.offset + self.width // 8 != reg.offset: - raise SPSDKRegsErrorRegisterGroupMishmash( - f"The register {reg.name} doesn't follow the previous one." - ) - self.width += reg.width - else: - if self.offset + self.width // 8 <= reg.offset: - raise SPSDKRegsErrorRegisterGroupMishmash( - f"The register {reg.name} doesn't follow the previous one." - ) - self._sub_regs_width += reg.width - if self._sub_regs_width > self.width: - raise SPSDKRegsErrorRegisterGroupMishmash( - f"The register {reg.name} bigger width than is defined." - ) - if self.sub_regs[0].width != reg.width: - raise SPSDKRegsErrorRegisterGroupMishmash( - f"The register {reg.name} has different width." - ) - if self.access != reg.access: - raise SPSDKRegsErrorRegisterGroupMishmash( - f"The register {reg.name} has different access type." - ) - reg.base_endianness = self.base_endianness - self.sub_regs.append(reg) - - def add_et_subelement(self, parent: ET.Element) -> None: - """Creates the register XML structure in ElementTree. - - :param parent: The parent object of ElementTree. - """ - element = ET.SubElement(parent, "register") - element.set("offset", hex(self.offset)) - element.set("width", str(self.width)) - element.set("name", self.name) - element.set("reversed", str(self.reverse)) - element.set("description", self.description) - if self.otp_index: - element.set("otp_index", str(self.otp_index)) - for bitfield in self._bitfields: - bitfield.add_et_subelement(element) - - def set_value(self, val: Any, raw: bool = False) -> None: - """Set the new value of register. - - :param val: The new value to set. - :param raw: Do not use any modification hooks. - :raises SPSDKError: When invalid values is loaded into register - """ - try: - if isinstance(val, (bytes, bytearray)): - value = int.from_bytes(val, self.base_endianness.value) - else: - value = value_to_int(val) - if value >= 1 << self.width: - raise SPSDKError( - f"Input value {value} doesn't fit into register of width {self.width}." - ) - - alt_width = self.get_alt_width(value) - - if not raw: - for hook in self._set_value_hooks: - value = hook[0](value, hook[1]) - if self.reverse: - # The value_to_int internally is using BIG endian - val_bytes = value_to_bytes( - value, - align_to_2n=False, - byte_cnt=alt_width // 8, - endianness=Endianness.BIG, - ) - value = value.from_bytes(val_bytes, Endianness.LITTLE.value) - - if self.has_group_registers(): - # Update also values in sub registers - subreg_width = self.sub_regs[0].width - sub_regs = self.sub_regs[: alt_width // subreg_width] - for index, sub_reg in enumerate(sub_regs, start=1): - if self.reverse_subregs_order: - bit_pos = alt_width - index * subreg_width - else: - bit_pos = (index - 1) * subreg_width - - sub_reg.set_value( - (value >> bit_pos) & ((1 << subreg_width) - 1), raw=raw - ) - else: - self._value = value - - except SPSDKError as exc: - raise SPSDKError(f"Loaded invalid value {str(val)}") from exc - - def reset_value(self, raw: bool = False) -> None: - """Reset the value of register. - - :param raw: Do not use any modification hooks. - """ - self.set_value(self.get_reset_value(), raw) - - def get_alt_width(self, value: int) -> int: - """Get alternative width of register. - - :param value: Input value to recognize width - :return: Current width - """ - alt_width = self.width - if self.alt_widths: - real_byte_cnt = get_bytes_cnt_of_int(value, align_to_2n=False) - self.alt_widths.sort() - for alt in self.alt_widths: - if real_byte_cnt <= alt // 8: - alt_width = alt - break - return alt_width - - def get_value(self, raw: bool = False) -> int: - """Get the value of register. - - :param raw: Do not use any modification hooks. - """ - if self.has_group_registers(): - # Update local value, by the sub register values - subreg_width = self.sub_regs[0].width - sub_regs_value = 0 - for index, sub_reg in enumerate(self.sub_regs, start=1): - if self.reverse_subregs_order: - bit_pos = self.width - index * subreg_width - else: - bit_pos = (index - 1) * subreg_width - sub_regs_value |= sub_reg.get_value(raw=raw) << (bit_pos) - value = sub_regs_value - else: - value = self._value - - alt_width = self.get_alt_width(value) - - if not raw and self.reverse: - val_bytes = value_to_bytes( - value, - align_to_2n=False, - byte_cnt=alt_width // 8, - endianness=self.base_endianness, - ) - value = value.from_bytes( - val_bytes, - Endianness.BIG.value - if self.base_endianness == Endianness.LITTLE - else Endianness.LITTLE.value, - ) - - return value - - def get_bytes_value(self, raw: bool = False) -> bytes: - """Get the bytes value of register. - - :param raw: Do not use any modification hooks. - :return: Register value in bytes. - """ - value = self.get_value(raw=raw) - return value_to_bytes( - value, - align_to_2n=False, - byte_cnt=self.get_alt_width(value) // 8, - endianness=self.base_endianness, - ) - - def get_hex_value(self, raw: bool = False) -> str: - """Get the value of register in string hex format. - - :param raw: Do not use any modification hooks. - :return: Hexadecimal value of register. - """ - val_int = self.get_value(raw=raw) - count = "0" + str(self.get_alt_width(val_int) // 4) - value = f"{val_int:{count}X}" - if not self.config_as_hexstring: - value = "0x" + value - return value - - def get_reset_value(self) -> int: - """Returns reset value of the register. - - :return: Reset value of register. - """ - value = self._reset_value - for bitfield in self._bitfields: - width = bitfield.width - offset = bitfield.offset - val = bitfield.reset_value - value |= (val & ((1 << width) - 1)) << offset - - return value - - def add_bitfield(self, bitfield: RegsBitField) -> None: - """Add register bitfield. - - :param bitfield: New bitfield value for register. - """ - self._bitfields.append(bitfield) - - def get_bitfields(self, exclude: Optional[List[str]] = None) -> List[RegsBitField]: - """Returns register bitfields. - - Method allows exclude some bitfields by their names. - :param exclude: Exclude list of bitfield names if needed. - :return: Returns List of register bitfields. - """ - ret = [] - for bitf in self._bitfields: - if bitf.hidden: - continue - if exclude and bitf.name.startswith(tuple(exclude)): - continue - ret.append(bitf) - return ret - - def get_bitfield_names(self, exclude: Optional[List[str]] = None) -> List[str]: - """Returns list of the bitfield names. - - :param exclude: Exclude list of bitfield names if needed. - :return: List of bitfield names. - """ - return [x.name for x in self.get_bitfields(exclude)] - - def find_bitfield(self, name: str) -> RegsBitField: - """Returns the instance of the bitfield by its name. - - :param name: The name of the bitfield. - :return: Instance of the bitfield. - :raises SPSDKRegsErrorBitfieldNotFound: The bitfield doesn't exist. - """ - for bitfield in self._bitfields: - if name == bitfield.name: - return bitfield - - raise SPSDKRegsErrorBitfieldNotFound( - f" The {name} is not found in register {self.name}." - ) - - def add_setvalue_hook(self, hook: Callable, context: Optional[Any] = None) -> None: - """Set the value hook for write operation. - - :param hook: Callable hook for set value operation. - :param context: Context data for this hook. - """ - self._set_value_hooks.append((hook, context)) - - def __str__(self) -> str: - """Override 'ToString()' to print register. - - :return: Friendly looking string that describes the register. - """ - output = "" - output += f"Name: {self.name}\n" - output += f"Offset: 0x{self.offset:04X}\n" - output += f"Width: {self.width} bits\n" - output += f"Access: {self.access}\n" - output += f"Description: \n {self.description}\n" - if self.otp_index: - output += f"OTP Word: \n {self.otp_index}\n" - - i = 0 - for bitfield in self._bitfields: - output += f"Bitfield #{i}: \n" + str(bitfield) - i += 1 - - return output - - -class Registers: - """SPSDK Class for registers handling.""" - - TEMPLATE_NOTE = ( - "All registers is possible to define also as one value although the bitfields are used. " - "Instead of bitfields: ... field, the value: ... definition works as well." - ) - - def __init__( - self, device_name: str, base_endianness: Endianness = Endianness.BIG - ) -> None: - """Initialization of Registers class.""" - self._registers: List[RegsRegister] = [] - self.dev_name = device_name - self.base_endianness = base_endianness - - def __eq__(self, obj: Any) -> bool: - """Compare if the objects has same settings.""" - if not ( - isinstance(obj, self.__class__) - and obj.dev_name == self.dev_name - and obj.base_endianness == self.base_endianness - ): - return False - ret = obj._registers == self._registers - return ret - - def find_reg(self, name: str, include_group_regs: bool = False) -> RegsRegister: - """Returns the instance of the register by its name. - - :param name: The name of the register. - :param include_group_regs: The algorithm will check also group registers. - :return: Instance of the register. - :raises SPSDKRegsErrorRegisterNotFound: The register doesn't exist. - """ - for reg in self._registers: - if name == reg.name: - return reg - if name in reg._alias_names: - return reg - if include_group_regs and reg.has_group_registers(): - for sub_reg in reg.sub_regs: - if name == sub_reg.name: - return sub_reg - - raise SPSDKRegsErrorRegisterNotFound( - f"The {name} is not found in loaded registers for {self.dev_name} device." - ) - - def add_register(self, reg: RegsRegister) -> None: - """Adds register into register list. - - :param reg: Register to add to the class. - :raises SPSDKError: Invalid type has been provided. - :raises SPSDKRegsError: Cannot add register with same name - """ - if not isinstance(reg, RegsRegister): - raise SPSDKError("The 'reg' has invalid type.") - - if reg.name in self.get_reg_names(): - raise SPSDKRegsError(f"Cannot add register with same name: {reg.name}.") - - for idx, register in enumerate(self._registers): - # TODO solve problem with group register that are always at 0 offset - if register.offset == reg.offset != 0: - logger.debug( - f"Found register at the same offset {hex(reg.offset)}" - f", adding {reg.name} as an alias to {register.name}" - ) - self._registers[idx].add_alias(reg.name) - self._registers[idx]._bitfields.extend(reg._bitfields) - return - # update base endianness for all registers in group - reg.base_endianness = self.base_endianness - self._registers.append(reg) - - def remove_registers(self) -> None: - """Remove all registers.""" - self._registers.clear() - - def get_registers( - self, exclude: Optional[List[str]] = None, include_group_regs: bool = False - ) -> List[RegsRegister]: - """Returns list of the registers. - - Method allows exclude some register by their names. - :param exclude: Exclude list of register names if needed. - :param include_group_regs: The algorithm will check also group registers. - :return: List of register names. - """ - if exclude: - regs = [r for r in self._registers if not r.name.startswith(tuple(exclude))] - else: - regs = self._registers.copy() - if include_group_regs: - sub_regs = [] - for reg in regs: - if reg.has_group_registers(): - sub_regs.extend(reg.sub_regs) - regs.extend(sub_regs) - - return regs - - def get_reg_names( - self, exclude: Optional[List[str]] = None, include_group_regs: bool = False - ) -> List[str]: - """Returns list of the register names. - - :param exclude: Exclude list of register names if needed. - :param include_group_regs: The algorithm will check also group registers. - :return: List of register names. - """ - return [x.name for x in self.get_registers(exclude, include_group_regs)] - - def reset_values(self, exclude: Optional[List[str]] = None) -> None: - """The method reset values in registers. - - :param exclude: The list of register names to be excluded. - """ - for reg in self.get_registers(exclude): - reg.reset_value(True) - - def __str__(self) -> str: - """Override 'ToString()' to print register. - - :return: Friendly looking string that describes the registers. - """ - output = "" - output += "Device name: " + self.dev_name + "\n" - for reg in self._registers: - output += str(reg) + "\n" - - return output - - def write_xml(self, file_name: str) -> None: - """Write loaded register structures into XML file. - - :param file_name: The name of XML file that should be created. - """ - xml_root = ET.Element("regs") - for reg in self._registers: - reg.add_et_subelement(xml_root) - - no_pretty_data = minidom.parseString( - ET.tostring(xml_root, encoding="unicode", short_empty_elements=False) - ) - write_file(no_pretty_data.toprettyxml(), file_name, encoding="utf-8") - - def image_info( - self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros") - ) -> BinaryImage: - """Export Registers into binary information. - - :param size: Result size of Image, 0 means automatic minimal size. - :param pattern: Pattern of gaps, defaults to "zeros" - """ - image = BinaryImage(self.dev_name, size=size, pattern=pattern) - for reg in self._registers: - description = reg.description - if reg._alias_names: - description += f"\n Alias names: {', '.join(reg._alias_names)}" - image.add_image( - BinaryImage( - reg.name, - reg.width // 8, - offset=reg.offset, - description=description, - binary=reg.get_bytes_value(raw=True), - ) - ) - - return image - - def export( - self, size: int = 0, pattern: BinaryPattern = BinaryPattern("zeros") - ) -> bytes: - """Export Registers into binary. - - :param size: Result size of Image, 0 means automatic minimal size. - :param pattern: Pattern of gaps, defaults to "zeros" - """ - return self.image_info(size, pattern).export() - - def parse(self, binary: bytes) -> None: - """Parse the binary data values into loaded registers. - - :param binary: Binary data to parse. - """ - bin_len = len(binary) - if bin_len < len(self.image_info()): - logger.info( - f"Input binary is smaller than registers supports: {bin_len} != {len(self.image_info())}" - ) - for reg in self.get_registers(): - if bin_len < reg.offset + reg.width // 8: - logger.debug(f"Parsing of binary block ends at {reg.name}") - break - reg.set_value(binary[reg.offset : reg.offset + reg.width // 8], raw=True) - - def _get_bitfield_yaml_description(self, bitfield: RegsBitField) -> str: - """Create the valuable comment for bitfield. - - :param bitfield: Bitfield used to generate description. - :return: Bitfield description. - """ - description = f"Offset: {bitfield.offset}b, Width: {bitfield.config_width}b" - if bitfield.description not in ("", "."): - description += ", " + bitfield.description.replace(" ", "\n") - if bitfield.config_processor.description: - description += ".\n NOTE: " + bitfield.config_processor.description - if bitfield.has_enums(): - for enum in bitfield.get_enums(): - descr = enum.description if enum.description != "." else enum.name - enum_description = descr.replace(" ", "\n") - description += ( - f"\n- {enum.name}, ({enum.get_value_int()}): {enum_description}" - ) - return description - - def get_validation_schema(self) -> Dict: - """Get the JSON SCHEMA for registers. - - :return: JSON SCHEMA. - """ - properties: Dict[str, Any] = {} - for reg in self.get_registers(): - bitfields = reg.get_bitfields() - reg_schema = [ - { - "type": ["string", "number"], - "skip_in_template": len(bitfields) > 0, - # "format": "number", # TODO add option to hexstring - "template_value": f"{reg.get_hex_value()}", - }, - { # Obsolete type - "type": "object", - "required": ["value"], - "skip_in_template": True, - "additionalProperties": False, - "properties": { - "value": { - "type": ["string", "number"], - # "format": "number", # TODO add option to hexstring - "template_value": f"{reg.get_hex_value()}", - } - }, - }, - ] - - if bitfields: - bitfields_schema = {} - for bitfield in bitfields: - if not bitfield.has_enums(): - bitfields_schema[bitfield.name] = { - "type": ["string", "number"], - "title": f"{bitfield.name}", - "description": self._get_bitfield_yaml_description( - bitfield - ), - "template_value": bitfield.get_value(), - } - else: - bitfields_schema[bitfield.name] = { - "type": ["string", "number"], - "title": f"{bitfield.name}", - "description": self._get_bitfield_yaml_description( - bitfield - ), - "enum_template": bitfield.get_enum_names(), - "minimum": 0, - "maximum": (1 << bitfield.width) - 1, - "template_value": bitfield.get_enum_value(), - } - # Extend register schema by obsolete style - reg_schema.append( - { - "type": "object", - "required": ["bitfields"], - "skip_in_template": True, - "additionalProperties": False, - "properties": { - "bitfields": { - "type": "object", - "properties": bitfields_schema, - } - }, - } - ) - # Extend by new style of bitfields - reg_schema.append( - { - "type": "object", - "skip_in_template": False, - "required": [], - "additionalProperties": False, - "properties": bitfields_schema, - }, - ) - - properties[reg.name] = { - "title": f"{reg.name}", - "description": f"{reg.description}", - "oneOf": reg_schema, - } - - return {"type": "object", "title": self.dev_name, "properties": properties} - - # pylint: disable=no-self-use #It's better to have this function visually close to callies - def _filter_by_names( - self, items: List[ET.Element], names: List[str] - ) -> List[ET.Element]: - """Filter out all items in the "items" tree,whose name starts with one of the strings in "names" list. - - :param items: Items to be filtered out. - :param names: Names to filter out. - :return: Filtered item elements list. - """ - return [ - item for item in items if not item.attrib["name"].startswith(tuple(names)) - ] - - # pylint: disable=dangerous-default-value - def load_registers_from_xml( - self, - xml: str, - filter_reg: Optional[List[str]] = None, - grouped_regs: Optional[List[dict]] = None, - ) -> None: - """Function loads the registers from the given XML. - - :param xml: Input XML data in string format. - :param filter_reg: List of register names that should be filtered out. - :param grouped_regs: List of register prefixes names to be grouped into one. - :raises SPSDKRegsError: XML parse problem occurs. - """ - - def is_reg_in_group(reg: str) -> Union[dict, None]: - """Help function to recognize if the register should be part of group.""" - if grouped_regs: - for group in grouped_regs: - # pylint: disable=anomalous-backslash-in-string # \d is a part of the regex pattern - if re.fullmatch(f"{group['name']}" + r"\d+", reg) is not None: - return group - return None - - try: - xml_elements = ET.parse(xml) - except ET.ParseError as exc: - raise SPSDKRegsError(f"Cannot Parse XML data: {str(exc)}") from exc - xml_registers = xml_elements.findall("register") - xml_registers = self._filter_by_names(xml_registers, filter_reg or []) - # Load all registers into the class - for xml_reg in xml_registers: - group = is_reg_in_group(xml_reg.attrib["name"]) - if group: - try: - group_reg = self.find_reg(group["name"]) - except SPSDKRegsErrorRegisterNotFound: - group_reg = RegsRegister( - name=group["name"], - offset=value_to_int(group.get("offset", 0)), - width=value_to_int(group.get("width", 0)), - description=group.get( - "description", f"Group of {group['name']} registers." - ), - reverse=value_to_bool(group.get("reversed", False)), - access=group.get("access", None), - config_as_hexstring=group.get("config_as_hexstring", False), - reverse_subregs_order=group.get("reverse_subregs_order", False), - alt_widths=group.get("alternative_widths"), - ) - - self.add_register(group_reg) - group_reg.add_group_reg(RegsRegister.from_xml_element(xml_reg)) - else: - self.add_register(RegsRegister.from_xml_element(xml_reg)) - - def load_yml_config(self, yml_data: Dict[str, Any]) -> None: - """The function loads the configuration from YML file. - - :param yml_data: The YAML commented data with register values. - """ - for reg_name in yml_data.keys(): - reg_value = yml_data[reg_name] - register = self.find_reg(reg_name, include_group_regs=True) - if isinstance(reg_value, dict): - if "value" in reg_value.keys(): - raw_val = reg_value["value"] - val = ( - int(raw_val, 16) - if register.config_as_hexstring and isinstance(raw_val, str) - else value_to_int(raw_val) - ) - register.set_value(val, False) - else: - bitfields = ( - reg_value["bitfields"] - if "bitfields" in reg_value.keys() - else reg_value - ) - for bitfield_name in bitfields: - bitfield_val = bitfields[bitfield_name] - try: - bitfield = register.find_bitfield(bitfield_name) - except SPSDKRegsErrorBitfieldNotFound: - logger.error( - f"The {bitfield_name} is not found in register {register.name}." - ) - continue - try: - bitfield.set_enum_value(bitfield_val, True) - except SPSDKValueError as e: - raise SPSDKError( - f"Bitfield value: {hex(bitfield_val)} of {bitfield.name} is out of range." - + f"\nBitfield width is {bitfield.width} bits" - ) from e - except SPSDKError: - # New versions of register data do not contain register and bitfield value in enum - old_bitfield = bitfield_val - bitfield_val = bitfield_val.replace( - bitfield.name + "_", "" - ).replace(register.name + "_", "") - # Some bitfield were renamed from ENABLE to ALLOW - bitfield_val = ( - "ALLOW" if bitfield_val == "ENABLE" else bitfield_val - ) - logger.warning( - f"Bitfield {old_bitfield} not found, trying backward" - " compatibility mode with {bitfield_val}" - ) - bitfield.set_enum_value(bitfield_val, True) - - # Run the processing of loaded register value - register.set_value(register.get_value(True), False) - elif isinstance(reg_value, (int, str)): - val = ( - int(reg_value, 16) - if register.config_as_hexstring and isinstance(reg_value, str) - else value_to_int(reg_value) - ) - register.set_value(val, False) - - else: - logger.error(f"There are no data for {reg_name} register.") - - logger.debug(f"The register {reg_name} has been loaded from configuration.") - - def get_config(self, diff: bool = False) -> Dict[str, Any]: - """Get the whole configuration in dictionary. - - :param diff: Get only configuration with difference value to reset state. - :return: Dictionary of registers values. - """ - ret: Dict[str, Any] = {} - for reg in self.get_registers(): - if diff and reg.get_value(raw=True) == reg.get_reset_value(): - continue - bitfields = reg.get_bitfields() - if bitfields: - btf = {} - for bitfield in bitfields: - if diff and bitfield.get_value() == bitfield.get_reset_value(): - continue - btf[bitfield.name] = bitfield.get_enum_value() - ret[reg.name] = btf - else: - ret[reg.name] = reg.get_hex_value() - - return ret From c5f23db525988ce47ef76cc156bdc0b5dec5ccef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 21 Mar 2024 16:42:47 +0100 Subject: [PATCH 08/11] Remove now-unused gmssl --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7777fdd2..43336f8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ "libusbsio", "nethsm >= 1.0.0,<2", "asn1tools >= 0.166.0", - "gmssl >= 3.2, < 4", ] dynamic = ["version", "description"] From 49387f0f93b481970cb58620ca6d4fac2e432aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 22 Mar 2024 10:41:13 +0100 Subject: [PATCH 09/11] Update mypy and type-check the vendored spsdk --- pynitrokey/cli/fido2.py | 2 +- .../bootloader/lpc55_upload/apps/utils/utils.py | 3 +++ .../trussed/bootloader/lpc55_upload/crypto/types.py | 5 +++-- .../trussed/bootloader/lpc55_upload/mboot/memories.py | 2 +- .../bootloader/lpc55_upload/utils/crypto/otfad.py | 11 +++++++++++ pyproject.toml | 10 ++++------ 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pynitrokey/cli/fido2.py b/pynitrokey/cli/fido2.py index 52accae4..43d94b4b 100644 --- a/pynitrokey/cli/fido2.py +++ b/pynitrokey/cli/fido2.py @@ -786,7 +786,7 @@ def version(serial: Optional[str], udp: bool) -> None: locked = "" # @todo: if len(res) > 3: - if res[3]: # type: ignore + if res[3]: locked = "locked" else: locked = "unlocked" diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py index e4fd9638..e4db5c8b 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/apps/utils/utils.py @@ -5,8 +5,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +import os from typing import Dict +from ...utils.misc import get_abs_path, write_file + def filepath_from_config( config: Dict, diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py index 225b9435..ec5dd5b7 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/types.py @@ -11,8 +11,9 @@ from cryptography import utils from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509.base import Version -from cryptography.x509.extensions import ExtensionOID, Extensions, KeyUsage -from cryptography.x509.name import Name, NameOID, ObjectIdentifier +from cryptography.x509.extensions import Extensions, KeyUsage +from cryptography.x509.name import Name +from cryptography.x509.oid import ExtensionOID, NameOID, ObjectIdentifier from ..exceptions import SPSDKError diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py index 15a4c828..78cd28b2 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/mboot/memories.py @@ -43,7 +43,7 @@ def get_legacy_str(cls, key: str) -> Optional[int]: :return: new enum value """ new_key = LEGACY_MEM_ID.get(key) - return cast(int, cls.get_tag(new_key)) if new_key else None + return cls.get_tag(new_key) if new_key else None @classmethod def get_legacy_int(cls, key: int) -> Optional[str]: diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py index dc7ae1d7..bb28a85d 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/utils/crypto/otfad.py @@ -7,8 +7,19 @@ """The module provides support for On-The-Fly encoding for RTxxx devices.""" +import logging +from struct import pack from typing import Any, Dict, List, Optional, Union +from crcmod.predefined import mkPredefinedCrcFun + +from ...crypto.rng import random_bytes +from ...crypto.symmetric import Counter, aes_ctr_encrypt, aes_key_wrap +from ...exceptions import SPSDKError, SPSDKValueError +from ...utils.misc import Endianness, align_block + +logger = logging.getLogger(__name__) + class KeyBlob: """OTFAD KeyBlob: The class specifies AES key and counter initial value for specified address range. diff --git a/pyproject.toml b/pyproject.toml index 43336f8d..c60d9910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "libusbsio", "nethsm >= 1.0.0,<2", "asn1tools >= 0.166.0", + "pyyaml >= 6.0.1", ] dynamic = ["version", "description"] @@ -53,7 +54,7 @@ dev = [ "flit >=3.2,<4", "ipython", "isort", - "mypy >=1.4,<1.5", + "mypy >=1.9,<1.10", "pyinstaller ~=6.5.0", "pyinstaller-versionfile ==2.1.1; sys_platform=='win32'", "types-requests", @@ -98,6 +99,7 @@ module = [ "pynitrokey.libnk", "pynitrokey.start.*", "pynitrokey.test_secrets_app", + "pynitrokey.trussed.bootloader.lpc55_upload.*", ] check_untyped_defs = false disallow_any_generics = false @@ -123,11 +125,6 @@ ignore_errors = true module = "pynitrokey.trussed.bootloader.nrf52" disallow_untyped_calls = false -# pynitrokey.nk3.bootloader.lpc55_upload is only temporary in this package -[[tool.mypy.overrides]] -module = "pynitrokey.trussed.bootloader.lpc55_upload.*" -ignore_errors = true - # libraries without annotations [[tool.mypy.overrides]] module = [ @@ -149,6 +146,7 @@ module = [ "libusbsio.*", "fastjsonschema", "deepmerge.*", + "crcmod.*", ] ignore_missing_imports = true From 740442bd8960b41466c152995617a70552bb8c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 22 Mar 2024 11:03:17 +0100 Subject: [PATCH 10/11] Fix compatibility with cryptography 42 --- pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py index 06ab3dee..aa40bfc0 100644 --- a/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py +++ b/pynitrokey/trussed/bootloader/lpc55_upload/crypto/keys.py @@ -740,7 +740,7 @@ def _get_ec_curve_object(name: EccCurve) -> ec.EllipticCurve: for key_object in ec._CURVE_TYPES: if key_object.lower() == name.lower(): # pylint: disable=protected-access - return ec._CURVE_TYPES[key_object]() + return ec._CURVE_TYPES[key_object] raise SPSDKValueError(f"The EC curve with name '{name}' is not supported.") diff --git a/pyproject.toml b/pyproject.toml index c60d9910..0033eb9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "certifi >= 14.5.14", "cffi", "click >=8.0, <=8.1.3", - "cryptography >=41.0.4,<44", + "cryptography >=42.0.4,<44", "ecdsa", "fido2 >=1.1.2,<2", "intelhex", @@ -32,7 +32,6 @@ dependencies = [ "python-dateutil ~= 2.7.0", "pyusb", "requests", - # "spsdk >=2.0,<2.2", "tqdm", "tlv8", "typing_extensions ~= 4.3.0", @@ -44,6 +43,7 @@ dependencies = [ "nethsm >= 1.0.0,<2", "asn1tools >= 0.166.0", "pyyaml >= 6.0.1", + "types-PyYAML>= 6.0.1", ] dynamic = ["version", "description"] From b73dd4cb35fa2dc864d84d54a3247e44871bb40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 17 Apr 2024 15:47:55 +0200 Subject: [PATCH 11/11] Fix type error that could not happen in practice --- pynitrokey/cli/fido2.py | 1 - pynitrokey/fido2/client.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pynitrokey/cli/fido2.py b/pynitrokey/cli/fido2.py index 43d94b4b..ea2656c8 100644 --- a/pynitrokey/cli/fido2.py +++ b/pynitrokey/cli/fido2.py @@ -784,7 +784,6 @@ def version(serial: Optional[str], udp: bool) -> None: res = nkfido2.find(serial, udp=udp).solo_version() major, minor, patch = res[:3] locked = "" - # @todo: if len(res) > 3: if res[3]: locked = "locked" diff --git a/pynitrokey/fido2/client.py b/pynitrokey/fido2/client.py index 4e6bb006..6bd3d1f8 100644 --- a/pynitrokey/fido2/client.py +++ b/pynitrokey/fido2/client.py @@ -210,12 +210,12 @@ def bootloader_version(self) -> Tuple[int, int, int]: return (data[0], data[1], data[2]) return (0, 0, data[0]) - def solo_version(self) -> Union[bytes, Tuple[int, int, int]]: + def solo_version(self) -> bytes: try: return self.send_data_hid(0x61, b"") except CtapError: data = self.exchange(SoloExtension.version) - return (data[0], data[1], data[2]) + return data[:3] def write_flash(self, addr: int, data: bytes) -> None: self.exchange(SoloBootloader.write, addr, data)