diff --git a/ncs/Kconfig b/ncs/Kconfig index d4d709d..7c774b3 100755 --- a/ncs/Kconfig +++ b/ncs/Kconfig @@ -105,4 +105,70 @@ config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_NAME default "shake128" if SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHAKE128 default "shake256" if SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHAKE256 -endif # SUIT_ENVELOPE_TARGET_ENCRYPT \ No newline at end of file +endif # SUIT_ENVELOPE_TARGET_ENCRYPT + +config SUIT_ENVELOPE_TARGET_SIGN + bool "Sign the target envelope" + +if SUIT_ENVELOPE_TARGET_SIGN + +choice SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN + prompt "SUIT envelope signing key generation" + default SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + + config SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + bool "Key generation 1" + + config SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN2 + bool "Key generation 2" + + config SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN3 + bool "Key generation 3" +endchoice + +config SUIT_ENVELOPE_TARGET_SIGN_KEY_ID + hex "The key ID used to identify the public key on the device" + default 0x40022100 if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + default 0x40022101 if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN2 + default 0x40022102 if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN3 + default 0x40032100 if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + default 0x40032101 if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN2 + default 0x40032102 if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN3 + help + This string is translated to the numeric KEY ID by the encryption script + +config SUIT_ENVELOPE_TARGET_SIGN_PRIVATE_KEY_NAME + string "Name of the private key used for signing - to identify the key in the KMS" + default "MANIFEST_APPLICATION_GEN1_priv" if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + default "MANIFEST_APPLICATION_GEN2_priv" if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN2 + default "MANIFEST_APPLICATION_GEN3_priv" if SOC_NRF54H20_CPUAPP_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN3 + default "MANIFEST_RADIOCORE_GEN1_priv" if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN1 + default "MANIFEST_RADIOCORE_GEN2_priv" if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN2 + default "MANIFEST_RADIOCORE_GEN3_priv" if SOC_NRF54H20_CPURAD_COMMON && SUIT_ENVELOPE_TARGET_SIGN_KEY_GEN3 + +choice SUIT_ENVELOPE_TARGET_SIGN_ALG + prompt "Algorithm used to sign the target envelope" + default SUIT_ENVELOPE_TARGET_SIGN_ALG_EDDSA + +config SUIT_ENVELOPE_TARGET_SIGN_ALG_EDDSA + bool "Use the EdDSA algorithm" + +config SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_256 + bool "Use the ECDSA algorithm with key length of 256 bits" + +config SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_384 + bool "Use the ECDSA algorithm with key length of 384 bits" + +config SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_521 + bool "Use the ECDSA algorithm with key length of 521 bits" + +endchoice + +config SUIT_ENVELOPE_TARGET_SIGN_ALG_NAME + string "String name of the algorithm used to sign the target envelope" + default "eddsa" if SUIT_ENVELOPE_TARGET_SIGN_ALG_EDDSA + default "es-256" if SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_256 + default "es-384" if SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_384 + default "es-521" if SUIT_ENVELOPE_TARGET_SIGN_ALG_ECDSA_521 + +endif # SUIT_ENVELOPE_TARGET_SIGN diff --git a/ncs/basic_kms.py b/ncs/basic_kms.py index 9545605..ca72ba3 100644 --- a/ncs/basic_kms.py +++ b/ncs/basic_kms.py @@ -9,6 +9,16 @@ from pathlib import Path from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_der_private_key +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +import math + from suit_generator.suit_kms_base import SuitKMSBase import json @@ -72,6 +82,73 @@ def encrypt(self, plaintext, key_name, context, aad) -> tuple[bytes, bytes, byte return nonce, tag, ciphertext + def _verify_signing_key_type(self, private_key, algorithm) -> bool: + """Verify if the key type matches the provided key.""" + if isinstance(private_key, EllipticCurvePrivateKey): + return f"es-{private_key.key_size}" == algorithm + elif isinstance(private_key, Ed25519PrivateKey) or isinstance(private_key, Ed448PrivateKey): + return "eddsa" == algorithm + else: + raise ValueError(f"Key {type(private_key)} not supported") + + def _create_cose_es_signature(self, input_data, private_key: bytes) -> bytes: + """Create ECDSA signature and return signature bytes.""" + hash_map = {256: hashes.SHA256(), 384: hashes.SHA384(), 521: hashes.SHA512()} + dss_signature = private_key.sign(input_data, ec.ECDSA(hash_map[private_key.key_size])) + r, s = decode_dss_signature(dss_signature) + return r.to_bytes(math.ceil(private_key.key_size / 8), byteorder="big") + s.to_bytes( + math.ceil(private_key.key_size / 8), byteorder="big" + ) + + def _create_cose_ed_signature(self, input_data, private_key: bytes) -> bytes: + """Create ECDSA signature and return signature bytes.""" + return private_key.sign(input_data) + + def _get_sign_method(self, private_key) -> bool: + """Return sign method based on key type.""" + if isinstance(private_key, EllipticCurvePrivateKey): + return self._create_cose_es_signature + elif isinstance(private_key, Ed25519PrivateKey) or isinstance(private_key, Ed448PrivateKey): + return self._create_cose_ed_signature + else: + raise ValueError(f"Key {type(private_key)} not supported") + + def sign(self, data, key_name, algorithm, context) -> bytes: + """ + Sign the data with a private key. + + :param data: The data to be signed. + :param key_name: The name of the private key to be used. + :param algorithm: The name of the algorithm to be used. + Used to verify if the key in the provided file contains a key of a compatible type. + :param context: The context to be used + + :return: The signature. + :rtype: bytes + """ + # TODO: support DER format + key_file_name = key_name + ".pem" + private_key_path = self.keys_directory / key_file_name + loaders = { + ".pem": load_pem_private_key, + ".der": load_der_private_key, + } + + try: + loader = loaders[private_key_path.suffix] + except KeyError as e: + raise ValueError("Unrecognized private key format. Extension must be {per,der}") from e + with open(private_key_path, "rb") as private_key: + private_key = loader(private_key.read(), None) + + if not self._verify_signing_key_type(private_key, algorithm): + raise ValueError(f"Key {key_file_name} is not compatible with algorithm {algorithm}") + + sign_method = self._get_sign_method(private_key) + signature = sign_method(data, private_key) + + return signature + def suit_kms_factory(): """Get a KMS object.""" diff --git a/ncs/encrypt_script.py b/ncs/encrypt_script.py index e200dff..f475dea 100644 --- a/ncs/encrypt_script.py +++ b/ncs/encrypt_script.py @@ -132,6 +132,8 @@ 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" kms_module = _import_module_from_path(module_name, kms_script) + if not hasattr(kms_module, "suit_kms_factory"): + raise ValueError(f"Python script {kms_script} does not contain the required suit_kms_factory function") self.kms = kms_module.suit_kms_factory() if not isinstance(self.kms, SuitKMSBase): raise ValueError(f"Class {type(self.kms)} does not implement the required SuitKMSBase interface") diff --git a/ncs/sample_recursive_sign_config.json b/ncs/sample_recursive_sign_config.json new file mode 100644 index 0000000..d3d3a18 --- /dev/null +++ b/ncs/sample_recursive_sign_config.json @@ -0,0 +1,20 @@ +{ + "key-name": "MANIFEST_OEM_ROOT_GEN1_priv", + "key-id": "0x4000AA00", + "alg": "eddsa", + "context": "", + "sign-script": "", + "kms-script": "", + "omit-signing": false, + "already-signed-action": "error", + "dependencies" : { + "#radio" : { + "key-name": "MANIFEST_RADIOCORE_GEN1_priv", + "key-id": "0x40032100" + }, + "#application" : { + "key-name": "MANIFEST_APPLICATION_GEN1_priv", + "key-id": "0x40022100" + } + } +} \ No newline at end of file diff --git a/ncs/sign_script.py b/ncs/sign_script.py index 92ea682..f3b777d 100644 --- a/ncs/sign_script.py +++ b/ncs/sign_script.py @@ -14,44 +14,24 @@ """ from __future__ import annotations -import math - import cbor2 -import uuid +import importlib.util +import sys -from argparse import ArgumentParser from pathlib import Path -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_der_private_key -from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey -from collections import defaultdict from enum import Enum, unique - -# -# User note: -# Rename the files to 'key_private_.der' if you are using keys in DER format. -# -PRIVATE_KEYS = { - 0x40000000: Path(__file__).parent / "key_private.pem", - 0x4000AA00: Path(__file__).parent / "key_private_OEM_ROOT_GEN1.pem", - 0x40022100: Path(__file__).parent / "key_private_APPLICATION_GEN1.pem", - 0x40032100: Path(__file__).parent / "key_private_RADIO_GEN1.pem", -} +from suit_generator.suit_kms_base import SuitKMSBase +from suit_generator.suit_sign_script_base import ( + SuitEnvelopeSignerBase, + SignatureAlreadyPresentActions, + SuitSignAlgorithms, +) @unique class SuitAlgorithms(Enum): """Suit algorithms.""" - COSE_ALG_SHA_256 = -16 - COSE_ALG_SHAKE128 = -18 - COSE_ALG_SHA_384 = -43 - COSE_ALG_SHA_512 = -44 - COSE_ALG_SHAKE256 = -45 COSE_ALG_ES_256 = -7 COSE_ALG_ES_384 = -35 COSE_ALG_ES_521 = -36 @@ -68,30 +48,32 @@ class SuitIds(Enum): SUIT_MANIFEST_COMPONENT_ID = 5 -DEFAULT_KEY_ID = 0x40000000 - -KEY_IDS = { - "nRF54H20_sample_root": 0x4000AA00, # MANIFEST_PUBKEY_OEM_ROOT_GEN1 - "nRF54H20_sample_app": 0x40022100, # MANIFEST_PUBKEY_APPLICATION_GEN1 - "nRF54H20_sample_rad": 0x40032100, # MANIFEST_PUBKEY_RADIO_GEN1 -} - -DOMAIN_NAME = "nordicsemi.com" - - class SignerError(Exception): """Signer exception.""" -class Signer: +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 + + +class Signer(SuitEnvelopeSignerBase): """Signer implementation.""" - def __init__(self): - """Initialize signer.""" - domain_name = uuid.uuid5(uuid.NAMESPACE_DNS, DOMAIN_NAME) - self._key_ids = defaultdict(lambda: DEFAULT_KEY_ID) - for name, val in KEY_IDS.items(): - self._key_ids[uuid.uuid5(domain_name, name).hex] = val + def init_kms_backend(self, kms_script): + """Initialize the KMS from the provided script backend based on the passed context.""" + module_name = "SuitKMS_module" + kms_module = _import_module_from_path(module_name, kms_script) + if not hasattr(kms_module, "suit_kms_factory"): + raise ValueError(f"Python script {kms_script} does not contain the required suit_kms_factory function") + self.kms = kms_module.suit_kms_factory() + if not isinstance(self.kms, SuitKMSBase): + raise ValueError(f"Class {type(self.kms)} does not implement the required SuitKMSBase interface") + self.kms.init_kms(self._context) @staticmethod def create_authentication_block(protected: dict | None, unprotected: dict | None, signature: bytes): @@ -111,6 +93,26 @@ def get_digest(self): digest = cbor2.loads(auth_block[0]) return digest + def already_signed_action(self, action: SignatureAlreadyPresentActions): + """Check if the envelope is already signed - if it is, handle this case.""" + auth_block = cbor2.loads(self.envelope.value[SuitIds.SUIT_AUTHENTICATION_WRAPPER.value]) + for auth in auth_block: + if not isinstance(auth, bytes): + continue + auth_deserialized = cbor2.loads(auth) + if isinstance(auth_deserialized, cbor2.CBORTag) and auth_deserialized.tag == 18: + if action == SignatureAlreadyPresentActions.ERROR: + raise SignerError("The envelope has already been signed and already-signed-action is set to error.") + elif action == SignatureAlreadyPresentActions.REMOVE_OLD: + auth_block.remove(auth) + self.envelope.value[SuitIds.SUIT_AUTHENTICATION_WRAPPER.value] = cbor2.dumps(auth_block) + elif action == SignatureAlreadyPresentActions.SKIP: + self._skip_signing = True + pass + elif action == SignatureAlreadyPresentActions.APPEND: + raise NotImplementedError("Append signature action is not implemented yet.") + break + def add_signature(self, signature: bytes, protected: dict, unprotected: dict | None = None): """Add signature object to the envelope.""" new_auth = self.create_authentication_block(protected, unprotected, signature) @@ -118,100 +120,54 @@ def add_signature(self, signature: bytes, protected: dict, unprotected: dict | N auth_block.append(cbor2.dumps(new_auth)) self.envelope.value[SuitIds.SUIT_AUTHENTICATION_WRAPPER.value] = cbor2.dumps(auth_block) - def load_envelope(self, input_file: Path) -> None: - """Load suit envelope.""" - with open(input_file, "rb") as fh: - self.envelope = cbor2.load(fh) - - def save_envelope(self, output_file: Path) -> None: - """Store envelope.""" - with open(output_file, "wb") as fh: - cbor2.dump(self.envelope, fh) - - def _get_sign_method(self) -> callable: - """Return sign method based on key type.""" - if isinstance(self._key, EllipticCurvePrivateKey): - return self._create_cose_es_signature - elif isinstance(self._key, Ed25519PrivateKey) or isinstance(self._key, Ed448PrivateKey): - return self._create_cose_ed_signature - else: - raise SignerError(f"Key {type(self._key)} not supported") - - @property - def _algorithm_name(self) -> str: - """Get algorithm name.""" - hash_alg = SuitAlgorithms(self.get_digest()[0]) - if isinstance(self._key, EllipticCurvePrivateKey): - return f"COSE_ALG_ES_{self._key.key_size}" - elif isinstance(self._key, Ed25519PrivateKey) or isinstance(self._key, Ed448PrivateKey): - return "COSE_ALG_EdDSA" - else: - raise SignerError(f"Key {type(self._key)} with {hash_alg} is not supported") - - def _create_cose_es_signature(self, input_data: bytes) -> bytes: - """Create ECDSA signature and return signature bytes.""" - hash_map = {256: hashes.SHA256(), 384: hashes.SHA384(), 521: hashes.SHA512()} - dss_signature = self._key.sign(input_data, ec.ECDSA(hash_map[self._key.key_size])) - r, s = decode_dss_signature(dss_signature) - return r.to_bytes(math.ceil(self._key.key_size / 8), byteorder="big") + s.to_bytes( - math.ceil(self._key.key_size / 8), byteorder="big" - ) - - def _create_cose_ed_signature(self, input_data: bytes) -> bytes: - """Create ECDSA signature and return signature bytes.""" - return self._key.sign(input_data) - - def _get_manifest_class_id(self): - manifest = cbor2.loads(self.envelope.value[SuitIds.SUIT_MANIFEST.value]) - if ( - SuitIds.SUIT_MANIFEST_COMPONENT_ID.value in manifest - and len(manifest[SuitIds.SUIT_MANIFEST_COMPONENT_ID.value]) == 2 - ): - return manifest[SuitIds.SUIT_MANIFEST_COMPONENT_ID.value][1].hex() - else: - return None - - def _get_key_id_for_manifest_class(self): - return self._key_ids[self._get_manifest_class_id()] - - def _get_private_key_path_for_manifest_class(self) -> Path: - key_id = self._key_ids[self._get_manifest_class_id()] - return PRIVATE_KEYS[key_id] - - def sign(self, private_key_path: Path = None) -> None: - """Add signature to the envelope.""" - loaders = { - ".pem": load_pem_private_key, - ".der": load_der_private_key, - } - - if private_key_path is None: - private_key_path = self._get_private_key_path_for_manifest_class() - - try: - loader = loaders[private_key_path.suffix] - except KeyError as e: - raise ValueError("Unrecognized private key format. Extension must be {per,der}") from e - with open(private_key_path, "rb") as private_key: - self._key = loader(private_key.read(), None) - sign_method = self._get_sign_method() + def sign_envelope( + self, + input_envelope: cbor2.CBORTag, + key_name: str, + key_id: int, + algorithm: SuitSignAlgorithms, + context: str, + kms_script: Path, + already_signed_action: SignatureAlreadyPresentActions, + ) -> cbor2.CBORTag: + """ + Add signature to the envelope. + + :param input_envelope: The input envelope to sign. + :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 algorithm: The algorithm used to sign the envelope. + :param context: Any context information that should be passed to the KMS backend during initialization + and signing. + :param kms_script: Python script containing a SuitKMS class with a sign function - used to communicate + with a KMS. + :param already_signed_action: Action to take when a signature is already present in the envelope. + + :return: The signed envelope. + :rtype: bytes + """ + self._key_name = key_name + self._key_id = key_id + self._algorithm = algorithm + self._context = context + self._skip_signing = False + self.envelope = input_envelope + + self.init_kms_backend(kms_script) + self.already_signed_action(already_signed_action) + + if self._skip_signing: + return protected = { - SuitIds.COSE_ALG.value: SuitAlgorithms[self._algorithm_name].value, - SuitIds.COSE_KEY_ID.value: cbor2.dumps(self._get_key_id_for_manifest_class()), + SuitIds.COSE_ALG.value: SuitAlgorithms["COSE_ALG_" + self._algorithm.name].value, + SuitIds.COSE_KEY_ID.value: cbor2.dumps(self._key_id), } cose = self.create_cose_structure(protected=protected) - signature = sign_method(cose) + signature = self.kms.sign(cose, self._key_name, self._algorithm.value, self._context) self.add_signature(signature, protected=protected) + return self.envelope -if __name__ == "__main__": - parser = ArgumentParser() - parser.add_argument("--input-file", required=True, type=Path, help="Input envelope.") - parser.add_argument("--output-file", required=True, type=Path, help="Output envelope.") - - arguments = parser.parse_args() - - signer = Signer() - signer.load_envelope(arguments.input_file) - signer.sign() - signer.save_envelope(arguments.output_file) +def suit_signer_factory(): + """Get a Signer object.""" + return Signer() diff --git a/suit_generator/args.py b/suit_generator/args.py index 26be64f..2e79dd5 100644 --- a/suit_generator/args.py +++ b/suit_generator/args.py @@ -17,6 +17,7 @@ from suit_generator.cmd_mpi import add_arguments as mpi_args 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 def _parser() -> ArgumentParser: @@ -32,6 +33,7 @@ def _parser() -> ArgumentParser: mpi_args(subparsers) cache_create_args(subparsers) payload_extract_args(subparsers) + sign_args(subparsers) return parser diff --git a/suit_generator/cli.py b/suit_generator/cli.py index 6518e94..fdf21a4 100644 --- a/suit_generator/cli.py +++ b/suit_generator/cli.py @@ -20,6 +20,7 @@ cmd_mpi, cmd_cache_create, cmd_payload_extract, + cmd_sign, args, ) from suit_generator.exceptions import GeneratorError, SUITError @@ -41,6 +42,7 @@ cmd_mpi.MPI_CMD: cmd_mpi.main, 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, } diff --git a/suit_generator/cmd_sign.py b/suit_generator/cmd_sign.py new file mode 100644 index 0000000..e325a0d --- /dev/null +++ b/suit_generator/cmd_sign.py @@ -0,0 +1,316 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""CMD_SIGN CLI command entry point.""" + +import json +import uuid +import logging +import cbor2 +import importlib.util +import sys +import os +from pathlib import Path +from suit_generator.suit_sign_script_base import ( + SuitEnvelopeSignerBase, + SignatureAlreadyPresentActions, + SuitSignAlgorithms, +) +from suit_generator.exceptions import GeneratorError +from argparse import RawTextHelpFormatter + +SIGN_SINGLE_LEVEL_CMD = "single_level" +SIGN_RECURSIVE_CMD = "recursive" + +log = logging.getLogger(__name__) + +SIGN_CMD = "sign" + + +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_signer(sign_script: Path) -> SuitEnvelopeSignerBase: + module_name = "SuitSignScript_module" + uuid.uuid4().hex + signer_module = _import_module_from_path(module_name, sign_script) + if not hasattr(signer_module, "suit_signer_factory"): + raise ValueError(f"Module {sign_script} does not contain a suit_signer_factory function.") + signer = signer_module.suit_signer_factory() + if not isinstance(signer, SuitEnvelopeSignerBase): + raise ValueError(f"Class {type(signer)} does not implement the required SuitEnvelopeSignerBase interface") + + return signer + + +class RecursiveSigner: + """Recursively sings a SUIT envelope.""" + + def __init__( + self, + envelope: cbor2.CBORTag, + envelope_json: dict, + envelope_name: str, + sign_script: Path = None, + kms_script: Path = None, + algorithm: SuitSignAlgorithms = SuitSignAlgorithms.EdDSA, + context: str = None, + ): + """Initialize the RecursiveSigner.""" + self.envelope = envelope + self.envelope_name = envelope_name + self.sign_script = sign_script + self.kms_script = kms_script + self.alg = algorithm + self.context = context + self.dependencies = [] + self.omit_signing = False + self.already_signed_action = SignatureAlreadyPresentActions.ERROR + if "omit-signing" in envelope_json: + self.omit_signing = envelope_json["omit-signing"] + if "key-name" not in envelope_json and not self.omit_signing: + raise ValueError( + f"key-name not found in {envelope_name}, but signing is required (omit-signing is not set)." + ) + self.key_name = envelope_json["key-name"] + + if "key-id" not in envelope_json and not self.omit_signing: + raise ValueError(f"key-id not found in {envelope_name}, but signing is required (omit-signing is not set).") + self.key_id = int(envelope_json["key-id"], 0) + + if "sign-script" in envelope_json: + self.sign_script = envelope_json["sign_script"] + elif self.sign_script is None: + if os.environ.get("NCS_SUIT_SIGN_SCRIPT"): + self.sign_script = os.environ.get("NCS_SUIT_SIGN_SCRIPT") + elif os.environ.get("ZEPHYR_BASE"): + self.sign_script = os.environ.get("ZEPHYR_BASE") + "/../modules/lib/suit-generator/ncs/sign_script.py" + else: + raise ValueError( + "sign-script not defined in the configuration and " + + "NCS_SUIT_SIGN_SCRIPT nor ZEPHYR_BASE are not set in the environment. " + + "Cannot set the sign script." + ) + self.signer = _import_signer(self.sign_script) + + if "kms-script" in envelope_json: + self.kms_script = envelope_json["kms_script"] + elif self.kms_script is None: + if os.environ.get("NCS_SUIT_KMS_SCRIPT"): + self.kms_script = os.environ.get("NCS_SUIT_KMS_SCRIPT") + elif os.environ.get("ZEPHYR_BASE"): + self.kms_script = os.environ.get("ZEPHYR_BASE") + "/../modules/lib/suit-generator/ncs/basic_kms.py" + else: + raise ValueError( + "kms-script not defined in the configuration and" + + "NCS_SUIT_KMS_SCRIPT nor ZEPHYR_BASE are not set in the environment." + + "Cannot set the KMS script." + ) + + if "alg" in envelope_json: + self.alg = SuitSignAlgorithms(envelope_json["alg"]) + if "context" in envelope_json: + self.context = envelope_json["context"] + if "already-signed-action" in envelope_json: + self.already_signed_action = SignatureAlreadyPresentActions(envelope_json["already-signed-action"]) + + if "dependencies" in envelope_json: + if not isinstance(envelope_json["dependencies"], dict): + raise ValueError(f"dependencies in {envelope_name} is not a dictionary.") + for dep in envelope_json["dependencies"]: + self.dependencies.append( + RecursiveSigner( + self._load_dependency(dep), + envelope_json["dependencies"][dep], + dep, + self.sign_script, + self.kms_script, + self.alg, + self.context, + ) + ) + + def _load_dependency(self, dependency_name): + """Load the dependency from the envelope.""" + if dependency_name not in self.envelope.value: + raise ValueError(f"Dependency {dependency_name} not found in {self.envelope_name}.") + if not isinstance(self.envelope.value[dependency_name], bytes): + raise ValueError(f"Dependency {dependency_name} in {self.envelope_name} is invalid.") + try: + dependency_envelope = cbor2.loads(self.envelope.value[dependency_name]) + except cbor2.CBORDecodeError: + raise ValueError(f"Failed decoding dependency {dependency_name} in {self.envelope_name}") + if not isinstance(dependency_envelope, cbor2.CBORTag): + raise ValueError(f"Dependency {dependency_name} in {self.envelope_name} is not a valid envelope.") + + return dependency_envelope + + def _sign(self): + self.envelope = self.signer.sign_envelope( + self.envelope, + self.key_name, + self.key_id, + self.alg, + self.context, + self.kms_script, + self.already_signed_action, + ) + + def recursive_sign(self): + """Recursively sign the envelope and the dependencies.""" + for dep in self.dependencies: + self.envelope.value[dep.envelope_name] = cbor2.dumps(dep.recursive_sign()) + if not self.omit_signing: + self._sign() + return self.envelope + + +def add_arguments(parser): + """Add additional arguments to the passed parser.""" + cmd_sign_arg_parser = parser.add_parser(SIGN_CMD, help="Sign a SUIT envelope.") + + cmd_sign_subparsers = cmd_sign_arg_parser.add_subparsers( + dest="sign_subcommand", required=True, help="Choose sign subcommand" + ) + + cmd_sign_single_level = cmd_sign_subparsers.add_parser( + SIGN_SINGLE_LEVEL_CMD, + help="Sign a single envelope - one level signing", + ) + cmd_sign_single_level.add_argument("--input-envelope", required=True, type=Path, help="Input envelope.") + cmd_sign_single_level.add_argument("--output-envelope", required=True, type=Path, help="Output envelope.") + cmd_sign_single_level.add_argument( + "--key-name", required=True, type=str, help="Name of the key used by the KMS to identify the key." + ) + cmd_sign_single_level.add_argument( + "--key-id", + required=True, + type=lambda x: int(x, 0), + help="The key ID used to identify the key on the device", + ) + cmd_sign_single_level.add_argument( + "--alg", + type=SuitSignAlgorithms, + choices=list(SuitSignAlgorithms), + default=SuitSignAlgorithms.ES_256, + help="Algorithm used to sign the envelope.", + ) + cmd_sign_single_level.add_argument( + "--context", + type=str, + help="Any context information that should be passed to the KMS backend during initialization and signing.", + ) + cmd_sign_single_level.add_argument( + "--sign-script", + required=True, + help="Sign script used to attach a signature to the envelope. " + + "It must contain a function suit_signer_factory() returning an object implementing SuitEnvelopeSignerBase.", + ) + cmd_sign_single_level.add_argument( + "--kms-script", + required=True, + help="Python script containing a SuitKMS class with an sign function - used to communicate with a KMS.", + ) + cmd_sign_single_level.add_argument( + "--already-signed-action", + type=SignatureAlreadyPresentActions, + choices=list(SignatureAlreadyPresentActions), + default=SignatureAlreadyPresentActions.ERROR, + help="Action to take when a signature is already present in the envelope.", + ) + + cmd_sign_recursive = cmd_sign_subparsers.add_parser( + SIGN_RECURSIVE_CMD, + help="Recursively sign the top level envelope and the integrated dependency envelopes.", + description="""Recursively sign the top level envelope and the integrated dependency envelopes. + +This command signs or omits signing of the top level envelope +and the integrated dependecy envelopes based on the provided configuration JSON file. + +The configuration is a JSON dictionary with the following available attributes (most of them are optional): + "sign-script" - path to the script used for signing a single envelope (default: sign_script.py) + "kms-script" - path to the KMS script used by the sign-script (default: basic_kms.py) + "alg" - algorithm used for signing (default: ed25519) + "context" - context used by the KMS script (default: None) + "key-name" - name of the key used for signing (required if omit-signing is not set) + "key-id" - key ID of the public key used to identify the key on the device (required if omit-signing is not set) + "already-signed-action" - action to be taken if the envelope is already signed (default: error) + Possible values: "error", "skip", "remove-old" + "omit-signing" - boolean value indicating whether the envelope should be signed or not. + By default the envelope is signed (omit-signing set to false). + Note, that even if set to true the dependencies will still be parsed and optionally signed. + "dependencies" - dictionary containing the integrated dependency envelope. + The keys are the names matching the names of the integrated dependencies in the parent envelope. + The values are the configuration dictionaries - all the mentioned attributes are avalable + inside these dictionaries. + + For reference see the suit-generator/ncs/sample_recursive_sign_config.json file + """, # noqa: W291, E501 + formatter_class=RawTextHelpFormatter, + ) + cmd_sign_recursive.add_argument("--input-envelope", required=True, type=Path, help="Input envelope.") + cmd_sign_recursive.add_argument("--output-envelope", required=True, type=Path, help="Output envelope.") + cmd_sign_recursive.add_argument("--configuration", required=True, type=Path, help="A .json configuration file") + + +def load_envelope(input_file: Path) -> cbor2.CBORTag: + """Load suit envelope.""" + with open(input_file, "rb") as fh: + envelope = cbor2.load(fh) + return envelope + + +def save_envelope(output_file: Path, envelope) -> None: + """Store envelope.""" + with open(output_file, "wb") as fh: + cbor2.dump(envelope, fh) + + +def single_level_sign(envelope, **kwargs) -> cbor2.CBORTag: + """Sign a single envelope, without parsing it recursivelu.""" + signer = _import_signer(kwargs["sign_script"]) + envelope = signer.sign_envelope( + envelope, + kwargs["key_name"], + kwargs["key_id"], + kwargs["alg"], + kwargs["context"], + kwargs["kms_script"], + kwargs["already_signed_action"], + ) + return envelope + + +def recursive_sign(envelope, **kwargs) -> cbor2.CBORTag: + """Sign a SUIT envelope recursively.""" + try: + with open(kwargs["configuration"], "r") as fh: + configuration = json.load(fh) + except json.JSONDecodeError: + raise json.JSONDecodeError(f"{recursive_sign} is not a valid JSON file.") + + recursive_signer = RecursiveSigner(envelope, configuration, kwargs["input_envelope"]) + return recursive_signer.recursive_sign() + + +def main(**kwargs) -> None: + """Sign a SUIT envelope.""" + envelope = load_envelope(kwargs["input_envelope"]) + if kwargs["sign_subcommand"] == SIGN_SINGLE_LEVEL_CMD: + envelope = single_level_sign(envelope, **kwargs) + elif kwargs["sign_subcommand"] == SIGN_RECURSIVE_CMD: + envelope = recursive_sign(envelope, **kwargs) + pass + else: + raise GeneratorError(f"Invalid 'sign' subcommand: {kwargs['sign_subcommand']}") + + if envelope is None: + raise ValueError("Signing the envelope failed - resulting envelope is empty.") + save_envelope(kwargs["output_envelope"], envelope) diff --git a/suit_generator/suit_kms_base.py b/suit_generator/suit_kms_base.py index d6632f8..80650f2 100644 --- a/suit_generator/suit_kms_base.py +++ b/suit_generator/suit_kms_base.py @@ -23,7 +23,7 @@ def init_kms(self, context) -> None: @abstractmethod def encrypt(self, plaintext, key_name, context, aad) -> tuple[bytes, bytes, bytes]: """ - Encrypt the plainext with an AES key. + Encrypt the plaintext with an AES key. :param plaintext: The plaintext to be encrypted. :param key_name: The name of the key to be used. @@ -33,3 +33,20 @@ def encrypt(self, plaintext, key_name, context, aad) -> tuple[bytes, bytes, byte :rtype: tuple[bytes, bytes, bytes] """ pass + + @abstractmethod + def sign(self, data, key_name, algorithm, context) -> bytes: + """ + Sign the data with a private key. + + :param data: The data to be signed. + :param key_name: The name of the private key to be used. + :param algorithm: The name of the algorithm to be used. + For file based KMS, this can be used to verify if the key in the + provided file contains a key of the correct type. + :param context: The context to be used + + :return: The signature. + :rtype: bytes + """ + pass diff --git a/suit_generator/suit_sign_script_base.py b/suit_generator/suit_sign_script_base.py new file mode 100644 index 0000000..51b8457 --- /dev/null +++ b/suit_generator/suit_sign_script_base.py @@ -0,0 +1,69 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""A base abstract class for any SUIT sign script implementations.""" + +import cbor2 +from abc import ABC, abstractmethod +from enum import Enum, unique +from pathlib import Path + + +@unique +class SuitSignAlgorithms(Enum): + """Suit signing algorithms.""" + + ES_256 = "es-256" + ES_384 = "es-384" + ES_521 = "es-521" + EdDSA = "eddsa" + + def __str__(self): + return self.value + + +class SignatureAlreadyPresentActions(Enum): + """Action to take when a signature is already present in the envelope.""" + + ERROR = "error" + REMOVE_OLD = "remove-old" + SKIP = "skip" + APPEND = "append" + + def __str__(self): + return self.value + + +class SuitEnvelopeSignerBase(ABC): + """Base abstract class for the Signer implementations.""" + + @abstractmethod + def sign_envelope( + self, + input_envelope: cbor2.CBORTag, + key_name: str, + key_id: int, + algorithm: SuitSignAlgorithms, + context: str, + kms_script: Path, + already_signed_action: SignatureAlreadyPresentActions, + ) -> cbor2.CBORTag: + """ + Add signature to the envelope. + + :param input_envelope: The input envelope to sign. + :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 algorithm: The algorithm used to sign the envelope. + :param context: Any context information that should be passed to the KMS backend during initialization + and signing. + :param kms_script: Python script containing a SuitKMS class with a sign function - used to communicate + with a KMS. + :param already_signed_action: Action to take when a signature is already present in the envelope. + + :return: The signed envelope. + :rtype: bytes + """ + pass diff --git a/tests/test_ncs_sign_script.py b/tests/test_ncs_sign_script.py index 1cf6bf5..4f66d43 100644 --- a/tests/test_ncs_sign_script.py +++ b/tests/test_ncs_sign_script.py @@ -4,18 +4,11 @@ # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause # """Unit tests for ncs example signing script.""" -import shutil - import pytest import binascii import pathlib import os import cbor2 -import subprocess -import sys -import uuid - -from unittest.mock import patch from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives import hashes @@ -32,7 +25,10 @@ suit_cose_key_id, ) -from ncs.sign_script import Signer, SignerError +from suit_generator.suit_sign_script_base import SignatureAlreadyPresentActions, SuitSignAlgorithms + +from ncs.sign_script import Signer, suit_signer_factory +import ncs.basic_kms TEMP_DIRECTORY = pathlib.Path("test_test_data") @@ -141,7 +137,7 @@ def setup_and_teardown(tmp_path_factory): fh.write(PRIVATE_KEYS["ES_384"]) with open("key_private_es_521.pem", "wb") as fh: fh.write(PRIVATE_KEYS["ES_521"]) - with open("key_private_ed25519.pem", "wb") as fh: + with open("key_private_eddsa.pem", "wb") as fh: fh.write(PRIVATE_KEYS["Ed25519"]) with open("key_private_rs2048.pem", "wb") as fh: fh.write(PRIVATE_KEYS["RS2048"]) @@ -157,8 +153,10 @@ def setup_and_teardown(tmp_path_factory): def test_ncs_cose(setup_and_teardown): """Test if is possible to create cose structure using ncs sign_script.py.""" - signer = Signer() - signer.load_envelope("test_envelope.suit") + signer = suit_signer_factory() + with open("test_envelope.suit", "rb") as fh: + envelope = cbor2.load(fh) + signer.envelope = envelope cose_binary = signer.create_cose_structure({1: -7}) cose_cbor = cbor2.loads(cose_binary) assert isinstance(cose_cbor, list) @@ -166,16 +164,17 @@ def test_ncs_cose(setup_and_teardown): def test_ncs_auth_block(setup_and_teardown): """Test if is possible to create authentication block using ncs sign_script.py.""" - signer = Signer() - signer.load_envelope("test_envelope.suit") + signer = suit_signer_factory() auth_block = signer.create_authentication_block({}, {}, b"\xDE\xAD\xBE\xEF") assert isinstance(auth_block, cbor2.CBORTag) def test_ncs_get_digest_object(setup_and_teardown): """Test if is possible to extract digest object using ncs sign_script.py.""" - signer = Signer() - signer.load_envelope("test_envelope.suit") + signer = suit_signer_factory() + with open("test_envelope.suit", "rb") as fh: + envelope = cbor2.load(fh) + signer.envelope = envelope assert signer.get_digest() == [ -16, binascii.a2b_hex("6658ea560262696dd1f13b782239a064da7c6c5cbaf52fded428a6fc83c7e5af"), @@ -184,17 +183,24 @@ def test_ncs_get_digest_object(setup_and_teardown): @pytest.mark.parametrize( "private_key", - ["es_256", "es_384", "es_521", "ed25519"], + ["es_256", "es_384", "es_521", "eddsa"], ) def test_ncs_signing(setup_and_teardown, private_key): """Test if is possible to sign manifest.""" - signer = Signer() - signer.load_envelope("test_envelope.suit") - signer.sign(pathlib.Path(f"key_private_{private_key}.pem")) - signer.save_envelope("test_envelope_signed.suit") - - with open("test_envelope_signed.suit", "rb") as fh: - envelope = SuitEnvelopeTagged.from_cbor(fh.read()) + signer = suit_signer_factory() + + with open("test_envelope.suit", "rb") as fh: + envelope_unsigned = cbor2.load(fh) + envelope_signed_cbor_tag = signer.sign_envelope( + envelope_unsigned, + f"key_private_{private_key}", + 0x40000000, + SuitSignAlgorithms(private_key.replace("_", "-")), + os.path.dirname(os.path.realpath("key_private_es_256.pem")), + ncs.basic_kms.__file__, + SignatureAlreadyPresentActions.ERROR, + ) + envelope = SuitEnvelopeTagged.from_cbor(cbor2.dumps(envelope_signed_cbor_tag)) assert envelope is not None assert suit_authentication_wrapper in envelope.SuitEnvelopeTagged.value.SuitEnvelope @@ -231,13 +237,19 @@ def test_envelope_sign_and_verify(setup_and_teardown, input_data, amount_of_payl with open("test_envelope.suit", "wb") as fh: fh.write(envelope.to_cbor()) - signer = Signer() - signer.load_envelope("test_envelope.suit") - signer.sign(pathlib.Path("key_private_es_256.pem")) - signer.save_envelope("test_envelope_signed.suit") - - with open("test_envelope_signed.suit", "rb") as fh: - envelope = SuitEnvelopeTagged.from_cbor(fh.read()) + signer = suit_signer_factory() + with open("test_envelope.suit", "rb") as fh: + envelope_unsigned = cbor2.load(fh) + envelope_signed_cbor_tag = signer.sign_envelope( + envelope_unsigned, + "key_private_es_256", + 0x40000000, + SuitSignAlgorithms("es-256"), + os.path.dirname(os.path.realpath("key_private_es_256.pem")), + ncs.basic_kms.__file__, + SignatureAlreadyPresentActions.ERROR, + ) + envelope = SuitEnvelopeTagged.from_cbor(cbor2.dumps(envelope_signed_cbor_tag)) signature = ( envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper] @@ -274,57 +286,38 @@ def test_envelope_sign_and_verify(setup_and_teardown, input_data, amount_of_payl def test_ncs_signing_unsupported(setup_and_teardown): """Test if SignerError is raised in case of unsupported key used.""" signer = Signer() - signer.load_envelope("test_envelope.suit") - with pytest.raises(SignerError): - signer.sign(pathlib.Path("key_private_rs2048.pem")) - -@patch("ncs.sign_script.DEFAULT_KEY_ID", 0x0C0FFE) -def test_ncs_signing_manifest_component_id_known_default_key_used(setup_and_teardown): - """Test if default key id is selected in case of unknown suit-manifest-component-id received.""" - signer = Signer() - signer.load_envelope("test_envelope_manifest_component_id.suit") - parsed_manifest_id = signer._get_manifest_class_id() - - domain_name = uuid.uuid5(uuid.NAMESPACE_DNS, "nordicsemi.com") - expected_manifest_id = uuid.uuid5(domain_name, "unit_test_envelope").hex - - assert parsed_manifest_id == expected_manifest_id - - signer.sign(pathlib.Path("key_private_es_256.pem")) - signer.save_envelope("test_envelope_signed.suit") + with open("test_envelope.suit", "rb") as fh: + envelope_unsigned = cbor2.load(fh) + signer = suit_signer_factory() + with pytest.raises(ValueError): + signer.sign_envelope( + envelope_unsigned, + "key_private_rs2048", + 0x40000000, + SuitSignAlgorithms("rs2048"), + os.path.dirname(os.path.realpath("key_private_rs2048.pem")), + ncs.basic_kms.__file__, + SignatureAlreadyPresentActions.ERROR, + ) - with open("test_envelope_signed.suit", "rb") as fh: - envelope = SuitEnvelopeTagged.from_cbor(fh.read()) - assert envelope is not None - assert suit_authentication_wrapper in envelope.SuitEnvelopeTagged.value.SuitEnvelope - assert hasattr(envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper], "SuitAuthentication") - assert hasattr( - envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper].SuitAuthentication[1], - "SuitAuthenticationBlock", +def test_ncs_signing_key_id_check(setup_and_teardown): + """Test if key_id is selected according to the received key-id argument.""" + + with open("test_envelope_manifest_component_id.suit", "rb") as fh: + envelope_unsigned = cbor2.load(fh) + signer = suit_signer_factory() + envelope_signed_cbor_tag = signer.sign_envelope( + envelope_unsigned, + "key_private_es_256", + 0xFFEEDDBB, + SuitSignAlgorithms("es-256"), + os.path.dirname(os.path.realpath("key_private_es_256.pem")), + ncs.basic_kms.__file__, + SignatureAlreadyPresentActions.ERROR, ) - assert ( - envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper] - .SuitAuthentication[1] - .SuitAuthenticationBlock.CoseSign1Tagged.value.CoseSign1[0] - .SuitHeaderMap[suit_cose_key_id] - .value.value - == 0x0C0FFE - ) - - -@patch("ncs.sign_script.KEY_IDS", {"unit_test_envelope": 0xFFEEDDBB, "some_other_sample": 0xFFFFFFFF}) -def test_ncs_signing_manifest_component_id_known_non_default(setup_and_teardown): - """Test if key_id is selected according to the received suit-manifest-component-id.""" - signer = Signer() - signer.load_envelope("test_envelope_manifest_component_id.suit") - - signer.sign(pathlib.Path("key_private_es_256.pem")) - signer.save_envelope("test_envelope_signed.suit") - - with open("test_envelope_signed.suit", "rb") as fh: - envelope = SuitEnvelopeTagged.from_cbor(fh.read()) + envelope = SuitEnvelopeTagged.from_cbor(cbor2.dumps(envelope_signed_cbor_tag)) assert ( envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper] @@ -334,44 +327,3 @@ def test_ncs_signing_manifest_component_id_known_non_default(setup_and_teardown) .value.value == 0xFFEEDDBB ) - - -@patch("ncs.sign_script.DEFAULT_KEY_ID", 0xDEADBEEF) -def test_ncs_signing_manifest_component_id_unknown(setup_and_teardown): - """Test if default key_id is used in case of not available suit-manifest-class-id.""" - signer = Signer() - signer.load_envelope("test_envelope_manifest_component_id.suit") - - signer.sign(pathlib.Path("key_private_es_256.pem")) - signer.save_envelope("test_envelope_signed.suit") - - with open("test_envelope_signed.suit", "rb") as fh: - envelope = SuitEnvelopeTagged.from_cbor(fh.read()) - - assert ( - envelope.SuitEnvelopeTagged.value.SuitEnvelope[suit_authentication_wrapper] - .SuitAuthentication[1] - .SuitAuthenticationBlock.CoseSign1Tagged.value.CoseSign1[0] - .SuitHeaderMap[suit_cose_key_id] - .value.value - == 0xDEADBEEF - ) - - -def test_ncs_sign_cli_interface(setup_and_teardown): - """Test if is possible to call cli interface.""" - shutil.copyfile( - "key_private_es_256.pem", - pathlib.Path(os.path.dirname(os.path.abspath(__file__))).parent / "ncs" / "key_private.pem", - ) - completed_process = subprocess.run( - [ - sys.executable, - pathlib.Path(os.path.dirname(os.path.abspath(__file__))).parent / "ncs" / "sign_script.py", - "--input-file", - "test_envelope.suit", - "--output-file", - "test_envelope_signed_cli.suit", - ] - ) - assert completed_process.returncode == 0