From a807cee28a973ce8782d2dc9403f5b5983a3866a Mon Sep 17 00:00:00 2001 From: Artur Hadasz Date: Tue, 14 Jan 2025 11:38:36 +0100 Subject: [PATCH] Align encryption mechanisms to match the way signing works Ref: NCSDK-30935 Signed-off-by: Artur Hadasz --- ncs/Kconfig | 28 +- ncs/encrypt_script.py | 291 ++++++--------------- suit_generator/args.py | 2 + suit_generator/cli.py | 2 + suit_generator/cmd_encrypt.py | 222 ++++++++++++++++ suit_generator/cmd_sign.py | 2 +- suit_generator/suit_encrypt_script_base.py | 86 ++++++ 7 files changed, 420 insertions(+), 213 deletions(-) create mode 100644 suit_generator/cmd_encrypt.py create mode 100644 suit_generator/suit_encrypt_script_base.py diff --git a/ncs/Kconfig b/ncs/Kconfig index 01dec54..4c26c72 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..07ccbb6 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 SuitEncryptAlgorithms(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) @@ -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): + def generate_digest_size_for_plain_text(self, plaintext: bytes): """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() - 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,7 +91,7 @@ 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, @@ -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 == SuitEncryptAlgorithms.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 == SuitEncryptAlgorithms.COSE_ALG_DIRECT.value: nonce, tag, ciphertext = self.kms.encrypt( plaintext=asset_plaintext, key_name=key_name, @@ -189,15 +137,14 @@ def parse_encrypted_assets(self, asset_bytes): return init_vector, tag, encrypted_content - def generate_encrypted_payload(self, encrypted_content, tag, output_directory: Path): + def generate_encrypted_payload(self, encrypted_content, tag): """Generate the encrypted payload file. This method writes the encrypted content and authentication tag to a binary file. """ - 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): + def generate_suit_encryption_info(self, iv, encrypted_cek, key_id): """Generate the SUIT encryption information file. This method creates a CBOR-encoded SUIT encryption information structure and writes it to a binary file. @@ -206,7 +153,7 @@ def generate_suit_encryption_info(self, iv, encrypted_cek, string_key_id, output # protected cbor2.dumps( { - SuitIds.COSE_ALG.value: SuitAlgorithms.COSE_ALG_AES_GCM_256.value, + SuitIds.COSE_ALG.value: SuitEncryptAlgorithms.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 - ): + def generate_encryption_info_and_encrypted_payload(self, encrypted_asset: Path, encrypted_cek, key_id: int): """Generate encryption information and encrypted payload files. 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. """ 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 = SuitEncryptAlgorithms.COSE_ALG_A256KW.value + else: + self.cose_kw_alg = SuitEncryptAlgorithms.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 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] + """ + 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 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] + """ + 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..7e68c4c --- /dev/null +++ b/suit_generator/cmd_encrypt.py @@ -0,0 +1,222 @@ +# +# 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 auto_int(x): + """Convert a string to an integer - allowing for formats like 0x... needed for argparse.""" + return int(x, 0) + + +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=auto_int, + 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=auto_int, + 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 + 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 7f07c95..5fd03e5 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..350bac5 --- /dev/null +++ b/suit_generator/suit_encrypt_script_base.py @@ -0,0 +1,86 @@ +# +# Copyright (c) 2024 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 + + 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