diff --git a/ncs/Kconfig b/ncs/Kconfig index 7c774b3..d4af825 100755 --- a/ncs/Kconfig +++ b/ncs/Kconfig @@ -65,16 +65,30 @@ config SUIT_ENVELOPE_TARGET_ENCRYPT if SUIT_ENVELOPE_TARGET_ENCRYPT -config SUIT_ENVELOPE_TARGET_ENCRYPT_STRING_KEY_ID - string "The string key ID used to identify the encryption key on the device" - default "FWENC_APPLICATION_GEN1" if SOC_NRF54H20_CPUAPP_COMMON - default "FWENC_RADIOCORE_GEN1" if SOC_NRF54H20_CPURAD_COMMON - help - This string is translated to the numeric KEY ID by the encryption script +choice SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN + prompt "SUIT envelope encryption key generation" + default SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + + config SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + bool "Key generation 1" + + config SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN2 + bool "Key generation 2" +endchoice + +config SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_ID + hex "The key ID used to identify the encryption key on the device" + default 0x40022000 if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + default 0x40022001 if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN2 + default 0x40032000 if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + default 0x40032001 if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN2 config SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_NAME string "Name of the key used for encryption - to identify the key in the KMS" - default SUIT_ENVELOPE_TARGET_ENCRYPT_STRING_KEY_ID + default "FWENC_APPLICATION_GEN1" if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + default "FWENC_APPLICATION_GEN2" if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN2 + default "FWENC_RADIOCORE_GEN1" if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN1 + default "FWENC_RADIOCORE_GEN2" if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_ENCRYPT_KEY_GEN2 choice SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG prompt "Algorithm used to calculate the digest of the plaintext firmware" diff --git a/ncs/encrypt_script.py b/ncs/encrypt_script.py index f475dea..fe2016b 100644 --- a/ncs/encrypt_script.py +++ b/ncs/encrypt_script.py @@ -5,21 +5,23 @@ # """Script to create artifacts needed by a SUIT envelope for encrypted firmware.""" -import os import cbor2 import importlib.util import sys -from argparse import ArgumentParser -from argparse import RawTextHelpFormatter from pathlib import Path from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from enum import Enum, unique from suit_generator.suit_kms_base import SuitKMSBase +from suit_generator.suit_encrypt_script_base import ( + SuitEncryptorBase, + SuitDigestAlgorithms, + SuitKWAlgorithms, +) @unique -class SuitAlgorithms(Enum): +class SuitCoseEncryptAlgorithms(Enum): """Suit algorithms.""" COSE_ALG_AES_GCM_128 = 1 @@ -39,41 +41,6 @@ class SuitIds(Enum): COSE_IV = 5 -class SuitDigestAlgorithms(Enum): - """Suit digest algorithms.""" - - SHA_256 = "sha-256" - SHA_384 = "sha-384" - SHA_512 = "sha-512" - SHAKE128 = "shake128" - SHAKE256 = "shake256" - - def __str__(self): - return self.value - - -class SuitKWAlgorithms(Enum): - """Supported SUIT Key wrap/derivation algorithms.""" - - A256KW = "aes-kw-256" - DIRECT = "direct" - - def __str__(self): - return self.value - - -KEY_IDS = { - "FWENC_APPLICATION_GEN1": 0x40022000, - "FWENC_APPLICATION_GEN2": 0x40022001, - "FWENC_RADIOCORE_GEN1": 0x40032000, - "FWENC_RADIOCORE_GEN2": 0x40032001, - "FWENC_CELL_GEN1": 0x40042000, - "FWENC_CELL_GEN2": 0x40042001, - "FWENC_WIFICORE_GEN1": 0x40062000, - "FWENC_WIFICORE_GEN2": 0x40062001, -} - - def _import_module_from_path(module_name, file_path): # Helper function to import a python module from a file path. spec = importlib.util.spec_from_file_location(module_name, file_path) @@ -84,7 +51,7 @@ def _import_module_from_path(module_name, file_path): class DigestGenerator: - """Class to generate digests for plaintext files using specified hash algorithms.""" + """Class to generate digests for plaintext using specified hash algorithms.""" _hash_func = { SuitDigestAlgorithms.SHA_256.value: hashes.SHA256(), @@ -100,34 +67,19 @@ def __init__(self, hash_name: str): raise ValueError(f"Unsupported hash algorithm: {hash_name}") self._hash_name = hash_name - def generate_digest_size_for_plain_text(self, plaintext_file_path: Path, output_directory: Path): - """Class to generate digests for plaintext files using specified hash algorithms.""" - plaintext = [] - with open(plaintext_file_path, "rb") as plaintext_file: - plaintext = plaintext_file.read() - + def generate_digest_size_for_plain_text(self, plaintext: bytes): + """Class to generate digests for plaintext using specified hash algorithms.""" func = hashes.Hash(self._hash_func[self._hash_name], backend=default_backend()) func.update(plaintext) digest = func.finalize() - with open(os.path.join(output_directory, "plain_text_digest.bin"), "wb") as file: - file.write(digest) - with open(os.path.join(output_directory, "plain_text_size.txt"), "w") as file: - file.write(str(len(plaintext))) + return digest, len(plaintext) -class Encryptor: +class Encryptor(SuitEncryptorBase): """Class to handle encryption operations using specified key wrap algorithms.""" kms = None - def __init__(self, kw_alg: SuitKWAlgorithms): - """Initialize the Encryptor with a specified key wrap algorithm.""" - if kw_alg == SuitKWAlgorithms.A256KW: - self.cose_kw_alg = SuitAlgorithms.COSE_ALG_A256KW.value - else: - self.cose_kw_alg = SuitAlgorithms.COSE_ALG_DIRECT.value - pass - def init_kms_backend(self, kms_script, context): """Initialize the KMS from the provided script backend based on the passed context.""" module_name = "SuitKMS_module" @@ -139,10 +91,10 @@ def init_kms_backend(self, kms_script, context): raise ValueError(f"Class {type(self.kms)} does not implement the required SuitKMSBase interface") self.kms.init_kms(context) - def generate_kms_artifacts(self, plaintext_file_path: Path, key_name: str, context: str): + def generate_kms_artifacts(self, asset_plaintext: bytes, key_name: str, context: str): """Generate encrypted artifacts using the key management system. - This method reads the plaintext file, encrypts it using the specified key wrap algorithm, + This method encrypts asset_plaintext bytes using the specified key wrap algorithm, and returns the encrypted asset and encrypted content encryption key (CEK). """ @@ -157,18 +109,14 @@ def generate_kms_artifacts(self, plaintext_file_path: Path, key_name: str, conte [0x83, 0x67, 0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x43, 0xA1, 0x01, 0x03, 0x40] ) - asset_plaintext = [] - with open(plaintext_file_path, "rb") as plaintext_file: - asset_plaintext = plaintext_file.read() - nonce = None tag = None ciphertext = None encrypted_cek = None - if self.cose_kw_alg == SuitAlgorithms.COSE_ALG_A256KW.value: + if self.cose_kw_alg == SuitCoseEncryptAlgorithms.COSE_ALG_A256KW.value: raise ValueError("AES Key Wrap 256 is not supported yet") - elif self.cose_kw_alg == SuitAlgorithms.COSE_ALG_DIRECT.value: + elif self.cose_kw_alg == SuitCoseEncryptAlgorithms.COSE_ALG_DIRECT.value: nonce, tag, ciphertext = self.kms.encrypt( plaintext=asset_plaintext, key_name=key_name, @@ -189,24 +137,23 @@ def parse_encrypted_assets(self, asset_bytes): return init_vector, tag, encrypted_content - def generate_encrypted_payload(self, encrypted_content, tag, output_directory: Path): - """Generate the encrypted payload file. + def generate_encrypted_payload(self, encrypted_content, tag): + """Generate the encrypted payload. - This method writes the encrypted content and authentication tag to a binary file. + This method returns the encrypted payload consisting of the encrypted content and the authentication tag. """ - with open(os.path.join(output_directory, "encrypted_content.bin"), "wb") as file: - file.write(tag + encrypted_content) + return tag + encrypted_content - def generate_suit_encryption_info(self, iv, encrypted_cek, string_key_id, output_directory: Path): - """Generate the SUIT encryption information file. + def generate_suit_encryption_info(self, iv, encrypted_cek, key_id): + """Generate the SUIT encryption information. - This method creates a CBOR-encoded SUIT encryption information structure and writes it to a binary file. + This method creates a CBOR-encoded SUIT encryption information structure. """ Cose_Encrypt = [ # protected cbor2.dumps( { - SuitIds.COSE_ALG.value: SuitAlgorithms.COSE_ALG_AES_GCM_256.value, + SuitIds.COSE_ALG.value: SuitCoseEncryptAlgorithms.COSE_ALG_AES_GCM_256.value, } ), # unprotected @@ -223,7 +170,7 @@ def generate_suit_encryption_info(self, iv, encrypted_cek, string_key_id, output # unprotected { SuitIds.COSE_ALG.value: self.cose_kw_alg, - SuitIds.COSE_KEY_ID.value: cbor2.dumps(KEY_IDS[string_key_id]), + SuitIds.COSE_KEY_ID.value: cbor2.dumps(key_id), }, # ciphertext encrypted_cek, @@ -234,148 +181,82 @@ def generate_suit_encryption_info(self, iv, encrypted_cek, string_key_id, output Cose_Encrypt_Tagged = cbor2.CBORTag(96, Cose_Encrypt) encryption_info = cbor2.dumps(cbor2.dumps(Cose_Encrypt_Tagged)) - with open(os.path.join(output_directory, "suit_encryption_info.bin"), "wb") as file: - file.write(encryption_info) + return encryption_info - def generate_encryption_info_and_encrypted_payload( - self, encrypted_asset: Path, encrypted_cek: Path, output_directory: Path, string_key_id: str - ): - """Generate encryption information and encrypted payload files. + def generate_encryption_info_and_encrypted_payload(self, encrypted_asset: Path, encrypted_cek, key_id: int): + """Generate encryption information and encrypted payload. This method parses the encrypted asset to extract the initialization vector, tag, and encrypted content. - It then generates the encrypted payload file and the SUIT encryption information file. + It then generates the encrypted payload and the SUIT encryption information. """ init_vector, tag, encrypted_content = self.parse_encrypted_assets(encrypted_asset) - self.generate_encrypted_payload(encrypted_content, tag, output_directory) - self.generate_suit_encryption_info(init_vector, encrypted_cek, string_key_id, output_directory) - - -def create_encrypt_and_generate_subparser(top_parser): - """Create a subparser for the 'encrypt-and-generate' command.""" - parser = top_parser.add_parser("encrypt-and-generate", help="First encrypt the payload, then generate the files.") - - parser.add_argument("--firmware", required=True, type=Path, help="Input, plaintext firmware.") - parser.add_argument( - "--key-name", required=True, type=str, help="Name of the key used by the KMS to identify the key." - ) - parser.add_argument( - "--string-key-id", - required=True, - type=str, - choices=KEY_IDS.keys(), - metavar="STRING_KEY_ID", - help="The string key ID used to identify the key on the device - translated to a numeric KEY ID.", - ) - parser.add_argument( - "--context", - type=str, - help="Any context information that should be passed to the KMS backend during initialization and encryption.", - ) - parser.add_argument("--output-dir", required=True, type=Path, help="Directory to store the output files") - parser.add_argument( - "--hash-alg", - default=SuitDigestAlgorithms.SHA_256.value, - type=SuitDigestAlgorithms, - choices=list(SuitDigestAlgorithms), - help="Algorithm used to create plaintext digest.", - ) - parser.add_argument( - "--kw-alg", - default=SuitKWAlgorithms.DIRECT.value, - type=SuitKWAlgorithms, - choices=list(SuitKWAlgorithms), - help="Key wrap algorithm used to wrap the CEK.", - ) - parser.add_argument( - "--kms-script", - default=Path(__file__).parent / "basic_kms.py", - help="Python script containing a SuitKMS class with an encrypt function - used to communicate with a KMS.", - ) - - -def create_generate_subparser(top_parser): - """Create a subparser for the 'generate' command.""" - parser = top_parser.add_parser("generate", help="Only generate files based on encrypted firmware") - - parser.add_argument( - "--encrypted-firmware", - required=True, - type=Path, - help="Input, encrypted firmware in form iv|tag|encrypted_firmware", - ) - parser.add_argument("--encrypted-key", required=True, type=Path, help="Encrypted content/asset encryption key") - parser.add_argument( - "--string-key-id", - required=True, - type=str, - choices=KEY_IDS.keys(), - help="The string key ID used to identify the key on the device - translated to a numeric KEY ID.", - ) - parser.add_argument( - "--kw-alg", - default=SuitKWAlgorithms.DIRECT.value, - type=SuitKWAlgorithms, - choices=list(SuitKWAlgorithms), - help="Key wrap algorithm used to wrap the CEK.", - ) - parser.add_argument("--output-dir", required=True, type=Path, help="Directory to store the output files") - - -def create_subparsers(parser): - """Create subparsers for the main parser. - - This function adds subparsers for different commands to the main parser. - """ - subparsers = parser.add_subparsers(dest="command", required=True, help="Choose subcommand:") - - create_encrypt_and_generate_subparser(subparsers) - create_generate_subparser(subparsers) - - -if __name__ == "__main__": - parser = ArgumentParser( - description="""This script allows to output artifacts needed by a SUIT envelope for encrypted firmware. - -It has two modes of operation: - - encrypt-and-generate: First encrypt the payload, then generate the files. - - generate: Only generate files based on encrypted firmware and the encrypted content/asset encryption key. - Note the encrypted firmware should match the format iv|tag|encrypted_firmware - -In both cases the output files are: - encrypted_content.bin - encrypted content of the firmware concatenated with the tag (encrypted firmware|16 byte tag). - This file is used as the payload in the SUIT envelope. - suit_encryption_info.bin - The binary contents which should be included in the SUIT envelope as the contents of the suit-encryption-info parameter. - -Additionally, the encrypt-and-generate mode generates the following file: - plain_text_digest.bin - The digest of the plaintext firmware. - plain_text_size.txt - The size of the plaintext firmware in bytes. - """, # noqa: W291, E501 - formatter_class=RawTextHelpFormatter, - ) - - create_subparsers(parser) - - arguments = parser.parse_args() - - encrypted_asset = None - encrypted_cek = None - - encryptor = Encryptor(arguments.kw_alg) - - if arguments.command == "encrypt-and-generate": - encryptor.init_kms_backend(arguments.kms_script, arguments.context) - digest_generator = DigestGenerator(arguments.hash_alg.value) - digest_generator.generate_digest_size_for_plain_text(arguments.firmware, arguments.output_dir) - encrypted_asset, encrypted_cek = encryptor.generate_kms_artifacts( - arguments.firmware, arguments.key_name, arguments.context + encryption_info = self.generate_suit_encryption_info(init_vector, encrypted_cek, key_id) + return encrypted_content, tag, encryption_info + + def _kw_alg_convert(self, kw_alg): + if kw_alg == SuitKWAlgorithms.A256KW: + self.cose_kw_alg = SuitCoseEncryptAlgorithms.COSE_ALG_A256KW.value + else: + self.cose_kw_alg = SuitCoseEncryptAlgorithms.COSE_ALG_DIRECT.value + + def encrypt_and_generate( + self, + firmware: bytes, + key_name: str, + key_id: int, + context: str, + hash_alg: SuitDigestAlgorithms, + kw_alg: SuitKWAlgorithms, + kms_script: Path, + ) -> tuple[bytes, bytes, bytes, bytes, int]: + """ + Encrypt the payload and return the encryption artifacts. + + :param firmware: The plaintext firmware. + :param key_name: The name of the key used by the KMS to identify the key. + :param key_id: The key ID used to identify the key on the device. + :param context: Any context information that should be passed to the KMS backend during initialization + and encryption. + :param hash_alg: The algorithm used to create plaintext digest. + :param kw_alg: Key wrap algorithm used to wrap the CEK. + :param kms_script: Python script containing a SuitKMS class with an encrypt function - used to communicate + with a KMS. + + :return: The encrypted payload, tag, encryption info, digest, and plaintext length. + :rtype: tuple[bytes, bytes, bytes, bytes, int] + """ + self._kw_alg_convert(kw_alg) + self.init_kms_backend(kms_script, context) + + digest_generator = DigestGenerator(hash_alg.value) + digest, plaintext_len = digest_generator.generate_digest_size_for_plain_text(firmware) + encrypted_asset, encrypted_cek = self.generate_kms_artifacts(firmware, key_name, context) + encrypted_payload, tag, encryption_info = self.generate_encryption_info_and_encrypted_payload( + encrypted_asset, encrypted_cek, key_id ) + return encrypted_payload, tag, encryption_info, digest, plaintext_len + + def generate( + self, encrypted_asset: bytes, encrypted_cek: bytes, key_id: int, kw_alg: SuitKWAlgorithms + ) -> tuple[bytes, bytes, bytes]: + """ + Generate encryption artifacts on encrypted firmware and the encrypted content/asset encryption key. + + :param encrypted_asset: The encrypted firmware in form iv|tag|encrypted_firmware. + :param encrypted_cek: The encrypted content/asset encryption key. + :param key_id: The key ID used to identify the key on the device. + :param kw_alg: Key wrap algorithm used to wrap the CEK. + + :return: The encrypted payload, tag, encryption info. + :rtype: tuple[bytes, bytes, bytes] + """ + if kw_alg == SuitKWAlgorithms.A256KW: + if encrypted_cek is None: + raise ValueError("Encrypted CEK is required for AES Key Wrap 256") + self.kw_alg_convert(kw_alg) + return self.generate_encryption_info_and_encrypted_payload(encrypted_asset, encrypted_cek, key_id) - if arguments.command == "generate": - with open(arguments.encrypted_firmware, "rb") as file: - encrypted_asset = file.read() - with open(arguments.encrypted_key, "rb") as file: - encrypted_cek = file.read() - encryptor.generate_encryption_info_and_encrypted_payload( - encrypted_asset, encrypted_cek, arguments.output_dir, arguments.string_key_id - ) +def suit_encryptor_factory(): + """Get an Encryptor object.""" + return Encryptor() diff --git a/suit_generator/args.py b/suit_generator/args.py index 2e79dd5..2986ad7 100644 --- a/suit_generator/args.py +++ b/suit_generator/args.py @@ -18,6 +18,7 @@ from suit_generator.cmd_cache_create import add_arguments as cache_create_args from suit_generator.cmd_payload_extract import add_arguments as payload_extract_args from suit_generator.cmd_sign import add_arguments as sign_args +from suit_generator.cmd_encrypt import add_arguments as encrypt_args def _parser() -> ArgumentParser: @@ -34,6 +35,7 @@ def _parser() -> ArgumentParser: cache_create_args(subparsers) payload_extract_args(subparsers) sign_args(subparsers) + encrypt_args(subparsers) return parser diff --git a/suit_generator/cli.py b/suit_generator/cli.py index fdf21a4..48aef7f 100644 --- a/suit_generator/cli.py +++ b/suit_generator/cli.py @@ -21,6 +21,7 @@ cmd_cache_create, cmd_payload_extract, cmd_sign, + cmd_encrypt, args, ) from suit_generator.exceptions import GeneratorError, SUITError @@ -43,6 +44,7 @@ cmd_cache_create.CACHE_CREATE_CMD: cmd_cache_create.main, cmd_payload_extract.PAYLOAD_EXTRACT_CMD: cmd_payload_extract.main, cmd_sign.SIGN_CMD: cmd_sign.main, + cmd_encrypt.ENCRYPT_CMD: cmd_encrypt.main, } diff --git a/suit_generator/cmd_encrypt.py b/suit_generator/cmd_encrypt.py new file mode 100644 index 0000000..0fade6c --- /dev/null +++ b/suit_generator/cmd_encrypt.py @@ -0,0 +1,217 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""Generate encryption artifacts for SUIT.""" + +import uuid +import logging +import importlib.util +import sys +import os +from argparse import RawTextHelpFormatter +from pathlib import Path +from suit_generator.suit_encrypt_script_base import ( + SuitEncryptorBase, + SuitDigestAlgorithms, + SuitKWAlgorithms, +) +from suit_generator.exceptions import GeneratorError + +ENCRYPT_AND_GENERATE_FIRMWARE_CMD = "encrypt-and-generate" +GENERATE_INFO_FIRMWARE_CMD = "generate-info" + +log = logging.getLogger(__name__) + +ENCRYPT_CMD = "encrypt" + + +def _import_module_from_path(module_name, file_path): + # Helper function to import a python module from a file path. + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def _import_encryptor(encrypt_script: Path) -> SuitEncryptorBase: + module_name = "SuitEncryptScript_module" + uuid.uuid4().hex + encryptor_module = _import_module_from_path(module_name, encrypt_script) + if not hasattr(encryptor_module, "suit_encryptor_factory"): + raise ValueError(f"Module {encrypt_script} does not contain a suit_encryptor_factory function.") + encryptor = encryptor_module.suit_encryptor_factory() + if not isinstance(encryptor, SuitEncryptorBase): + raise ValueError(f"Class {type(encryptor)} does not implement the required SuitEnvelopeSignerBase interface") + + return encryptor + + +def add_arguments(parser): + """Add additional arguments to the passed parser.""" + cmd_encrypt_arg_parser = parser.add_parser( + ENCRYPT_CMD, + help="Generate encryption artifacts for SUIT.", + description="""This script allows to output artifacts needed by a SUIT envelope for encrypted firmware. + +It has two modes of operation: + - encrypt-and-generate: First encrypt the payload, then generate the files. + - generate: Only generate files based on encrypted firmware and the encrypted content/asset encryption key. + Note the encrypted firmware should match the format iv|tag|encrypted_firmware + +In both cases the output files are: + encrypted_content.bin - encrypted content of the firmware concatenated with the tag (encrypted firmware|16 byte tag). + This file is used as the payload in the SUIT envelope. + suit_encryption_info.bin - The binary contents which should be included in the SUIT envelope as the contents of the suit-encryption-info parameter. + +Additionally, the encrypt-and-generate mode generates the following file: + plain_text_digest.bin - The digest of the plaintext firmware. + plain_text_size.txt - The size of the plaintext firmware in bytes. + """, # noqa: W291, E501 + formatter_class=RawTextHelpFormatter, + ) + + cmd_encrypt_subparsers = cmd_encrypt_arg_parser.add_subparsers( + dest="encrypt_subcommand", required=True, help="Choose encrypt subcommand" + ) + + cmd_encrypt_and_generate_parser = cmd_encrypt_subparsers.add_parser( + ENCRYPT_AND_GENERATE_FIRMWARE_CMD, help="First encrypt the payload, then generate the files." + ) + + cmd_encrypt_and_generate_parser.add_argument( + "--firmware", required=True, type=Path, help="Input, plaintext firmware." + ) + cmd_encrypt_and_generate_parser.add_argument( + "--key-name", required=True, type=str, help="Name of the key used by the KMS to identify the key." + ) + cmd_encrypt_and_generate_parser.add_argument( + "--key-id", + required=True, + type=lambda x: int(x, 0), + help="Key ID used to identify the key on the device.", + ) + cmd_encrypt_and_generate_parser.add_argument( + "--context", + type=str, + help="Any context information that should be passed to the KMS backend during initialization and encryption.", + ) + cmd_encrypt_and_generate_parser.add_argument( + "--output-dir", required=True, type=Path, help="Directory to store the output files" + ) + cmd_encrypt_and_generate_parser.add_argument( + "--hash-alg", + default=SuitDigestAlgorithms.SHA_256.value, + type=SuitDigestAlgorithms, + choices=list(SuitDigestAlgorithms), + help="Algorithm used to create plaintext digest.", + ) + cmd_encrypt_and_generate_parser.add_argument( + "--kw-alg", + default=SuitKWAlgorithms.DIRECT.value, + type=SuitKWAlgorithms, + choices=list(SuitKWAlgorithms), + help="Key wrap algorithm used to wrap the CEK.", + ) + cmd_encrypt_and_generate_parser.add_argument( + "--kms-script", + help="Python script containing a SuitKMS class with an encrypt function - used to communicate with a KMS.", + ) + + cmd_encrypt_and_generate_parser.add_argument( + "--encrypt-script", + required=True, + help="Encrypt script used to generate the encryption artifacts. " + + "It must contain a function suit_encryptor_factory() returning an object implementing SuitEncryptorBase.", + ) + + cmd_generate_info_parser = cmd_encrypt_subparsers.add_parser( + GENERATE_INFO_FIRMWARE_CMD, help="Only generate artifacts based on encrypted firmware." + ) + + cmd_generate_info_parser.add_argument( + "--encrypted-firmware", + required=True, + type=Path, + help="Input, encrypted firmware in form iv|tag|encrypted_firmware", + ) + cmd_generate_info_parser.add_argument( + "--encrypted-key", required=True, type=Path, help="Encrypted content/asset encryption key" + ) + cmd_generate_info_parser.add_argument( + "--key-id", + required=True, + type=lambda x: int(x, 0), + help="Key ID used to identify the key on the device.", + ) + cmd_generate_info_parser.add_argument( + "--kw-alg", + default=SuitKWAlgorithms.DIRECT.value, + type=SuitKWAlgorithms, + choices=list(SuitKWAlgorithms), + help="Key wrap algorithm used to wrap the CEK.", + ) + cmd_generate_info_parser.add_argument( + "--output-dir", required=True, type=Path, help="Directory to store the output files" + ) + + cmd_generate_info_parser.add_argument( + "--encrypt-script", + required=True, + help="Encrypt script used to generate the encryption artifacts. " + + "It must contain a function suit_encryptor_factory() returning an object implementing SuitEncryptorBase.", + ) + + +def encrypt_and_generate(**kwargs): + """Encrypt the payload and generate the files.""" + encryptor = _import_encryptor(kwargs["encrypt_script"]) + with open(kwargs["firmware"], "rb") as file: + plaintext = file.read() + encrypted_content, tag, encryption_info, digest, plaintext_len = encryptor.encrypt_and_generate( + plaintext, + kwargs["key_name"], + kwargs["key_id"], + kwargs["context"], + kwargs["hash_alg"], + kwargs["kw_alg"], + kwargs["kms_script"], + ) + with open(os.path.join(kwargs["output_dir"], "plain_text_digest.bin"), "wb") as file: + file.write(digest) + with open(os.path.join(kwargs["output_dir"], "plain_text_size.txt"), "w") as file: + file.write(str(plaintext_len)) + with open(os.path.join(kwargs["output_dir"], "suit_encryption_info.bin"), "wb") as file: + file.write(encryption_info) + with open(os.path.join(kwargs["output_dir"], "encrypted_content.bin"), "wb") as file: + file.write(tag + encrypted_content) + + +def generate_info(**kwargs): + """Generate files based on encrypted firmware and the encrypted content/asset encryption key.""" + encryptor = _import_encryptor(kwargs["encrypt_script"]) + with open(kwargs["encrypted_firmware"], "rb") as file: + encrypted_firmware = file.read() + with open(kwargs["encrypted_key"], "rb") as file: + encrypted_key = file.read() + encrypted_content, tag, encryption_info = encryptor.generate( + encrypted_firmware, + encrypted_key, + kwargs["key_id"], + kwargs["kw_alg"], + ) + with open(os.path.join(kwargs["output_dir"], "suit_encryption_info.bin"), "wb") as file: + file.write(encryption_info) + with open(os.path.join(kwargs["output_dir"], "encrypted_content.bin"), "wb") as file: + file.write(tag + encrypted_content) + + +def main(**kwargs) -> None: + """Sign a SUIT envelope.""" + if kwargs["encrypt_subcommand"] == ENCRYPT_AND_GENERATE_FIRMWARE_CMD: + encrypt_and_generate(**kwargs) + elif kwargs["encrypt_subcommand"] == GENERATE_INFO_FIRMWARE_CMD: + generate_info(**kwargs) + else: + raise GeneratorError(f"Invalid 'encrypt' subcommand: {kwargs['encrypt_subcommand']}") diff --git a/suit_generator/cmd_sign.py b/suit_generator/cmd_sign.py index e325a0d..f9e0f6c 100644 --- a/suit_generator/cmd_sign.py +++ b/suit_generator/cmd_sign.py @@ -21,7 +21,7 @@ from suit_generator.exceptions import GeneratorError from argparse import RawTextHelpFormatter -SIGN_SINGLE_LEVEL_CMD = "single_level" +SIGN_SINGLE_LEVEL_CMD = "single-level" SIGN_RECURSIVE_CMD = "recursive" log = logging.getLogger(__name__) diff --git a/suit_generator/suit_encrypt_script_base.py b/suit_generator/suit_encrypt_script_base.py new file mode 100644 index 0000000..48df919 --- /dev/null +++ b/suit_generator/suit_encrypt_script_base.py @@ -0,0 +1,84 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""A base abstract class for any SUIT sign script implementations.""" + +from abc import ABC, abstractmethod +from enum import Enum, unique +from pathlib import Path + + +@unique +class SuitDigestAlgorithms(Enum): + """Suit digest algorithms.""" + + SHA_256 = "sha-256" + SHA_384 = "sha-384" + SHA_512 = "sha-512" + SHAKE128 = "shake128" + SHAKE256 = "shake256" + + def __str__(self): + return self.value + + +class SuitKWAlgorithms(Enum): + """Supported SUIT Key wrap/derivation algorithms.""" + + A256KW = "aes-kw-256" + DIRECT = "direct" + + def __str__(self): + return self.value + + +class SuitEncryptorBase(ABC): + """Base abstract class for the Encryptor implementations.""" + + @abstractmethod + def encrypt_and_generate( + self, + firmware: bytes, + key_name: str, + key_id: int, + context: str, + hash_alg: SuitDigestAlgorithms, + kw_alg: SuitKWAlgorithms, + kms_script: Path, + ) -> tuple[bytes, bytes, bytes, bytes, int]: + """ + Encrypt the payload and generate the files. + + :param firmware: The plaintext firmware. + :param key_name: The name of the key used by the KMS to identify the key. + :param key_id: The key ID used to identify the key on the device. + :param context: Any context information that should be passed to the KMS backend during initialization + and encryption. + :param hash_alg: The algorithm used to create plaintext digest. + :param kw_alg: Key wrap algorithm used to wrap the CEK. + :param kms_script: Python script containing a SuitKMS class with an encrypt function - used to communicate + with a KMS. + + :return: The encrypted payload, tag, encryption info, digest, and plaintext length. + :rtype: tuple[bytes, bytes, bytes, bytes, int] + """ + pass + + @abstractmethod + def generate( + self, encrypted_asset: bytes, encrypted_cek: bytes, key_id: int, kw_alg: SuitKWAlgorithms + ) -> tuple[bytes, bytes, bytes]: + """ + Generate files based on encrypted firmware and the encrypted content/asset encryption key. + + :param encrypted_asset: The encrypted firmware in form iv|tag|encrypted_firmware. + :param encrypted_cek: The encrypted content/asset encryption key. + :param key_id: The key ID used to identify the key on the device. + :param kw_alg: Key wrap algorithm used to wrap the CEK. + + :return: The encrypted payload, tag, encryption info. + :rtype: tuple[bytes, bytes, bytes] + """ + pass