diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index f810dbb9..bf09d825 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -2,9 +2,9 @@ name: Code scan on: push: - branches: [ main, develop ] + branches: [ main, develop, ncs ] pull_request: - branches: [ main, develop ] + branches: [ main, develop, ncs ] jobs: code-check: diff --git a/.gitignore b/.gitignore index 66fb8559..f4990a73 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__ /doc/build/* /build/* +suit-generator.log* diff --git a/build_configuration/__init__.py b/build_configuration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build_configuration/configuration.py b/build_configuration/configuration.py new file mode 100644 index 00000000..1956ba7a --- /dev/null +++ b/build_configuration/configuration.py @@ -0,0 +1,49 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""The module to create and parse build configuration.""" + +import re +import string + + +class BuildConfiguration(dict): + """Represents a build system configuration, providing access to KConfig values. + + This class reads configuration data from a specified file and parses it. + Configuration data is accessible as a dictionary. + """ + + config_value_pattern = re.compile(r"(?P[A-Za-z0-9_]+)=(?P.*)") + + def __init__(self, input_file: str = ".config") -> None: + """Initialize a BuildConfiguration object.""" + super().__init__() + try: + with open(input_file, "r") as fh: + self._config_data = fh.readlines() + except FileNotFoundError as e: + raise SystemExit(e) + self._parse() + + def _parse(self) -> None: + """Parse input .config file and populate the configuration dictionary.""" + for config_line in self._config_data: + if re_result := self.config_value_pattern.match(config_line): + kconfig_name = re_result.group("kconfig_name") + kconfig_value = re_result.group("kconfig_value") + if kconfig_value == "y": + # boolean value + kconfig_value = True + elif kconfig_value.startswith("0x") and all(c in string.hexdigits for c in kconfig_value[2:]): + # hexadecimal value + kconfig_value = int(kconfig_value, base=16) + elif kconfig_value.startswith('"') and kconfig_value.endswith('"'): + # string value + kconfig_value = kconfig_value[1:-1] + elif kconfig_value.isdecimal(): + # int value + kconfig_value = int(kconfig_value, base=10) + super().__setitem__(kconfig_name, kconfig_value) diff --git a/examples/input_files/envelope_2_hierarchical.yaml b/examples/input_files/envelope_2_hierarchical.yaml index ad6aa1fe..f60a949c 100644 --- a/examples/input_files/envelope_2_hierarchical.yaml +++ b/examples/input_files/envelope_2_hierarchical.yaml @@ -169,23 +169,14 @@ SUIT_Envelope_Tagged: namespace: nordicsemi.com name: nRF54H20_sample_app suit-shared-sequence: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: nordicsemi.com - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_sample_rad - - suit-directive-set-component-index: 2 + - suit-directive-set-component-index: [1, 2] - suit-directive-override-parameters: suit-parameter-vendor-identifier: RFC4122_UUID: nordicsemi.com suit-parameter-class-identifier: RFC4122_UUID: namespace: nordicsemi.com - name: nRF54H20_sample_app - - suit-directive-set-component-index: [1, 2] + name: nRF54H20_sample_root - suit-condition-vendor-identifier: - suit-send-record-success - suit-send-record-failure diff --git a/ncs/802154_rad_envelope.yaml.jinja2 b/ncs/802154_rad_envelope.yaml.jinja2 deleted file mode 100644 index b49d9745..00000000 --- a/ncs/802154_rad_envelope.yaml.jinja2 +++ /dev/null @@ -1,99 +0,0 @@ -{%- set mpi_rad_vendor_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_rad_class_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_rad') %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - - - MEM - - {{ _802154_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(_802154_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }} - - {{ _802154_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }} - - - CAND_IMG - - 0 - suit-shared-sequence: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_rad_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ _802154_rpmsg_subimage['binary'] }} - suit-parameter-image-size: - file: {{ _802154_rpmsg_subimage['binary'] }} - - suit-condition-vendor-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-condition-class-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ _802154_rpmsg_subimage['binary'] }} - suit-validate: - - suit-directive-set-component-index: 0 - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-invoke: - - suit-directive-set-component-index: 0 - - suit-directive-invoke: - - suit-send-record-failure - suit-install: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ _802154_rpmsg_subimage['name'] }}' - - suit-directive-fetch: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-text: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-text: - en: - '["MEM", {{ _802154_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }}, {{ get_absolute_address(_802154_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }}, {{ _802154_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }}]': - suit-text-vendor-name: Nordic Semiconductor ASA - suit-text-model-name: nRF54H20_cpurad - suit-text-vendor-domain: nordicsemi.com - suit-text-model-info: The nRF54H20 radio core - suit-text-component-description: Sample radio core FW - suit-text-component-version: v1.0.0 - suit-integrated-payloads: - '#{{ _802154_rpmsg_subimage['name'] }}': {{ _802154_rpmsg_subimage['binary'] }} \ No newline at end of file diff --git a/ncs/Kconfig b/ncs/Kconfig index a53dc2e3..d4d709d3 100755 --- a/ncs/Kconfig +++ b/ncs/Kconfig @@ -4,180 +4,105 @@ # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause # -menuconfig SUIT_ENVELOPE - bool "Create SUIT envelope" - help - Enable DFU SUIT envelope creation - default y if SSF_SUIT_SERVICE_ENABLED && SOC_NRF54H20_CPUAPP - -if SUIT_ENVELOPE - -config SUIT_ENVELOPE_SIGN - bool "Sign created SUIT envelope" - help - Sign created SUIT envelope by external script - default n - -config SUIT_ENVELOPE_SEQUENCE_NUM - int "Sequence number of the generated SUIT manifest" - range 0 2147483647 - default 1 - -config SUIT_ENVELOPE_DEFAULT_TEMPLATE - string "Path to the default envelope template (deprecated)" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/root_with_nordic_top_envelope.yaml.jinja2" if SOC_NRF54H20_CPUAPP - default SUIT_ENVELOPE_SYSCTRL_TEMPLATE if SOC_NRF54H20_CPUSYS - default SUIT_ENVELOPE_SECDOM_TEMPLATE if SOC_NRF54H20_CPUSEC - help - Path to the root template, that is used if the application directory does not - contain an input root envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - This KConfig is available for backward compatibility and will be removed soon. - -config SUIT_ENVELOPE_ROOT_TEMPLATE - string "Path to the default root envelope template" - default SUIT_ENVELOPE_DEFAULT_TEMPLATE - help - Path to the default root envelope template, that is used if the application directory does not - contain an input envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -config SUIT_ENVELOPE_APP_TEMPLATE - string "Path to the default application envelope template" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/app_envelope.yaml.jinja2" - help - Path to the default application envelope template, that is used if the application directory does not - contain an input application envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -config SUIT_ENVELOPE_HCI_RPMSG_SUBIMAGE_TEMPLATE - string "Path to the default radio envelope template" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/rad_envelope.yaml.jinja2" - help - Path to the default radio envelope template, that is used if the application directory does not - contain an input radio envelope template file. - -config SUIT_ENVELOPE_MULTIPROTOCOL_RPMSG_SUBIMAGE_TEMPLATE - string "Path to the default multiprotocol radio envelope template" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/multiprotocol_rad_envelope.yaml.jinja2" - help - Path to the default multiprotocol radio envelope template, that is used if the application - directory does not contain an input radio envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -config SUIT_ENVELOPE_802154_RPMSG_SUBIMAGE_TEMPLATE - string "Path to the default 802154 radio envelope template" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/802154_rad_envelope.yaml.jinja2" - help - Path to the default 802154 radio envelope template, that is used if the application - directory does not contain an input radio envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -config SUIT_ENVELOPE_EDITABLE_TEMPLATES_LOCATION - string "Path to the folder with envelope templates" - default "../../" - help - Path to the folder containing editable templates used to create binary envelopes. - Input templates are created by the build system during first build from the SUIT_ENVELOPE_DEFAULT_TEMPLATE. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -# TODO: Consider renaming to not cause a confusion with SUIT_PREPARE_SECDOM_UPDATE -config SUIT_ENVELOPE_SECDOM - bool "Create SUIT files required by secure domain" - help - Create SUIT storage file required by secure domain in case secure domain has been included in the build - default y if INCLUDE_SECDOM - -config SUIT_ENVELOPE_SIGN_SCRIPT - string "Location of SUIT sign script" - depends on SUIT_ENVELOPE_SIGN - help - Python script called to sign SUIT envelope. - You can use either absolute or relative path. - In case relative path is used, the build system uses NRF parent directory. - Script need to accept two arguments: - - --input-file - location of unsigned envelope in the build system - - --output-file - location of signed envelope to create by script - default "modules/lib/suit-generator/ncs/sign_script.py" - -config SUIT_PREPARE_SECDOM_UPDATE - bool "Create SUIT envelope for SDFW update" - depends on IS_SECURE_DOMAIN_FW - default y if !HW_REVISION_SOC1 - -config SUIT_ENVELOPE_SECDOM_TEMPLATE - string "Location of template file for preparing secdom yaml envelope" - help - Jinja2 template file used to generate yaml file for secure domain update. - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/secdom_update_envelope.yaml.jinja2" - -config SUIT_ENVELOPE_SYSCTRL_TEMPLATE - string "Path to the default system controller envelope template" - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/sysctrl_envelope.yaml.jinja2" - help - Path to the default system controller envelope template, that is used if the system controller directory does not - contain an input system controller envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - -config SUIT_ENVELOPE_SECDOM_IMPRIMATUR_SICR_BIN - string "Name of Imprimatur's build artifact containing SICR section needed for SDFW update" - default "urot_update_sm.bin" - -config SUIT_ENVELOPE_SECDOM_IMPRIMATUR_PUBLIC_KEY_BIN - string "Name of Imprimatur's build artifact containing public key used for signing the SDFW update candidate" - default "public_key.bin" - -config SUIT_ENVELOPE_SECDOM_IMPRIMATUR_SIGNATURE_BIN - string "Name of Imprimatur's build artifact containing signature of the SDFW update candidate" - default "signature.bin" - -config SUIT_LOG_SECDOM_VERSION - bool "Log version of Secdom FW during its startup" - help - For testing purposes. - default n - -config SUIT_SECDOM_VERSION - string "Version of Secdom FW" - help - For testing Secdom FW updates. - Version of Secdom FW to be logged during its startup. - default "0.0.1" - -config SUIT_ENVELOPE_SYSCTRL - bool "Create SUIT files required by sysctrl" - help - Create SUIT envelope for sysctrl in case it has been included in the build - default y if INCLUDE_SYSCTRL - -config SUIT_ENVELOPE_SYSCTRL_TEMPLATE - string "Location of template file for preparing sysctrl yaml envelope" - help - Path to the sysctrl template, that is used if the application directory does not - contain an input sysctrl envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/sysctrl_envelope.yaml.jinja2" - -config SUIT_ENVELOPE_TOP_TEMPLATE - string "Location of template file for preparing nordic-top yaml envelope" - help - Path to the nordic-top template, that is used if the application directory does not - contain an input nordic-top envelope template file. - You can use either absolute or relative path. - In case relative path is used, the build system uses CMAKE_SOURCE_DIR directory. - default "${ZEPHYR_SUIT_GENERATOR_MODULE_DIR}/ncs/nordic_top_envelope.yaml.jinja2" if SOC_NRF54H20_CPUAPP - -config SUIT_ENVELOPE_ROOT_TARGET - string "Map root target to custom target to overwite output aftifacts names." - default "secdom" if SOC_NRF54H20_CPUSEC && !HW_REVISION_SOC1 - default "sysctrl" if SOC_NRF54H20_CPUSYS && !HW_REVISION_SOC1 - -endif # SUIT_ENVELOPE +# Kconfig below is slated for removal once SUIT service is available in the NCS. +config SSF_SUIT_SERVICE_ENABLED + bool + +config SUIT_ENVELOPE_TEMPLATE_FILENAME + string "Path to the envelope template" + default "app_envelope.yaml.jinja2" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && !SUIT_RECOVERY + default "rad_envelope.yaml.jinja2" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && !SUIT_RECOVERY + default "app_recovery_local_envelope.yaml.jinja2" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && SUIT_RECOVERY + default "rad_recovery_envelope.yaml.jinja2" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && SUIT_RECOVERY + +config SUIT_ENVELOPE_TARGET + string "Target name inside the envelope templates" + default "application" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && !SUIT_RECOVERY + default "radio" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && !SUIT_RECOVERY + default "app_recovery_img" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && SUIT_RECOVERY + default "rad_recovery" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && SUIT_RECOVERY + +config SUIT_ENVELOPE_OUTPUT_ARTIFACT + string "Name of the output merged artifact" + default "merged.hex" + +config SUIT_RECOVERY + bool "The given image is part of a SUIT recovery application" + depends on !NRF_REGTOOL_GENERATE_UICR + +config SUIT_LOCAL_ENVELOPE_GENERATE + bool "Generate local envelope" + default y if SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPUAPP || SOC_NRF9230_ENGB_CPURAD + +config SUIT_DFU_CACHE_EXTRACT_IMAGE + bool "Extract firmware image to DFU cache" + help + Extracts the firmware image to a DFU cache file, which can be then flashed separately + to the device (instead of being integrated into the SUIT envelope). If using the default + SUIT envelope template, this will also remove the firmware image from the SUIT envelope + integrated payloads. + +if SUIT_DFU_CACHE_EXTRACT_IMAGE + +config SUIT_DFU_CACHE_EXTRACT_IMAGE_PARTITION + int "The number of the DFU partition to which the image will be extracted" + help + This option will ensure that images which set it to the same number will be extracted + to the same dfu cache file. + default 1 + +config SUIT_DFU_CACHE_EXTRACT_IMAGE_URI + string "The URI used as key for the image in the DFU cache" + default "cache://application.bin" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && !SUIT_RECOVERY + default "cache://radio.bin" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && !SUIT_RECOVERY + default "cache://app_recovery.bin" if (SOC_NRF54H20_CPUAPP_COMMON || SOC_NRF9230_ENGB_CPUAPP) && SUIT_RECOVERY + default "cache://rad_recovery.bin" if (SOC_NRF54H20_CPURAD_COMMON || SOC_NRF9230_ENGB_CPURAD) && SUIT_RECOVERY + +endif # SUIT_DFU_CACHE_EXTRACT_IMAGE + +config SUIT_ENVELOPE_TARGET_ENCRYPT + bool "Encrypt the target image" + +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 + +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 + +choice SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG + prompt "Algorithm used to calculate the digest of the plaintext firmware" + default SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA256 + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA256 + bool "Use the SHA-256 algorithm" + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA384 + bool "Use the SHA-384 algorithm" + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA512 + bool "Use the SHA-512 algorithm" + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHAKE128 + bool "Use the SHAKE128 algorithm" + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHAKE256 + bool "Use the SHAKE256 algorithm" + +endchoice + +config SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_NAME + string + default "sha-256" if SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA256 + default "sha-384" if SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA384 + default "sha-512" if SUIT_ENVELOPE_TARGET_ENCRYPT_PLAINTEXT_HASH_ALG_SHA512 + 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 diff --git a/ncs/app_envelope.yaml.jinja2 b/ncs/app_envelope.yaml.jinja2 deleted file mode 100644 index 46c1ef65..00000000 --- a/ncs/app_envelope.yaml.jinja2 +++ /dev/null @@ -1,148 +0,0 @@ -{%- set mpi_app_vendor_name = app['config']['CONFIG_SUIT_MPI_APP_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_app_class_name = app['config']['CONFIG_SUIT_MPI_APP_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_app') %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - - - MEM - - {{ app['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(app['dt'].chosen_nodes['zephyr,code-partition']) }} - - {{ app['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }} - - - CAND_IMG - - 0 -{%- if flash_companion_subimage is defined %} - - - MEM - - {{ flash_companion_subimage['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(flash_companion_subimage['dt'].chosen_nodes['zephyr,code-partition']) }} - - {{ flash_companion_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }} -{%- endif %} - suit-shared-sequence: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_app_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_app_vendor_name }} - name: {{ mpi_app_class_name }} - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ app['binary'] }} - suit-parameter-image-size: - file: {{ app['binary'] }} - - suit-condition-vendor-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-condition-class-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ app['binary'] }} -{%- if flash_companion_subimage is defined %} - - suit-directive-set-component-index: 2 - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ flash_companion_subimage['binary'] }} -{%- endif %} - suit-validate: - - suit-directive-set-component-index: 0 - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-invoke: - - suit-directive-set-component-index: 0 - - suit-directive-invoke: - - suit-send-record-failure - suit-install: -{%- if flash_companion_subimage is defined %} - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ flash_companion_subimage['name'] }}' - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ flash_companion_subimage['binary'] }} - - suit-directive-fetch: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 2 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-invoke: - - suit-send-record-failure -{%- endif %} - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ app['name'] }}' - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ app['binary'] }} - - suit-directive-fetch: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-text: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: {{ mpi_app_vendor_name }} - name: {{ mpi_app_class_name }} - suit-text: - en: - '["MEM", {{ app['dt'].label2node['cpu'].unit_addr }}, {{ get_absolute_address(app['dt'].chosen_nodes['zephyr,code-partition']) }}, {{ app['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }}]': - suit-text-vendor-name: Nordic Semiconductor ASA - suit-text-model-name: nRF54H20_cpuapp - suit-text-vendor-domain: nordicsemi.com - suit-text-model-info: The nRF54H20 application core - suit-text-component-description: Sample application core FW - suit-text-component-version: v1.0.0 - suit-integrated-payloads: - '#{{ app['name'] }}': {{ app['binary'] }} -{%- if flash_companion_subimage is defined %} - '#{{ flash_companion_subimage['name'] }}': {{ flash_companion_subimage['binary'] }} -{%- endif %} diff --git a/ncs/basic_kms.py b/ncs/basic_kms.py new file mode 100644 index 00000000..95456051 --- /dev/null +++ b/ncs/basic_kms.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""A basic KMS based on keys stored in files on the local drive.""" + +import os + +from pathlib import Path +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from suit_generator.suit_kms_base import SuitKMSBase +import json + + +class SuitKMS(SuitKMSBase): + """Implementation of the KMS.""" + + def parse_context(self, context): + """Parse the provided context string.""" + if context is None: + self.keys_directory = Path(__file__).parent + return None + + # Check if context is a valid path + context_path = Path(context) + if context_path.is_dir(): + self.keys_directory = context_path + return + + try: + context_loaded = json.loads(context) + except json.JSONDecodeError: + raise ValueError(f"The provided context '{context}' is neither a valid path nor a valid JSON string.") + + try: + self.keys_directory = Path(context_loaded["keys_directory"]) + except KeyError: + raise ValueError(f"The provided json context '{context}' does not contain the 'keys_directory' key.") + + def init_kms(self, context) -> None: + """ + Initialize the KMS. + + :param context: The context to be used + """ + self.parse_context(context) + + def encrypt(self, plaintext, key_name, context, aad) -> tuple[bytes, bytes, bytes]: + """ + 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. + :param context: The context to be used + If it is passed, it is used to point to the directory where the keys are stored. + It can either be a path or a JSON string in the format '{ "keys_directory":"" }'. + :param aad: The additional authenticated data to be used. + :return: The nonce, tag and ciphertext. + :rtype: tuple[bytes, bytes, bytes] + """ + key_file_name = key_name + ".bin" + key_file = self.keys_directory / key_file_name + + with open(key_file, "rb") as f: + key_data = f.read() + aesgcm = AESGCM(key_data) + nonce = os.urandom(12) + ciphertext_response = aesgcm.encrypt(nonce, plaintext, aad) + ciphertext = ciphertext_response[:-16] + tag = ciphertext_response[-16:] + + return nonce, tag, ciphertext + + +def suit_kms_factory(): + """Get a KMS object.""" + return SuitKMS() diff --git a/ncs/build.py b/ncs/build.py index faf29fbf..329a7ce8 100755 --- a/ncs/build.py +++ b/ncs/build.py @@ -15,10 +15,12 @@ from jinja2 import Template from argparse import ArgumentParser +from configparser import ConfigParser sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) from suit_generator.cmd_image import ImageCreator # noqa: E402 +from build_configuration.configuration import BuildConfiguration # noqa: E402 TEMPLATE_CMD = "template" STORAGE_CMD = "storage" @@ -27,66 +29,132 @@ dir_path = pathlib.Path(__file__).parent.absolute() -class BuildConfiguration(dict): - """Represents a build system configuration, providing access to KConfig values. - - This class reads configuration data from a specified file and parses it. - Configuration data is accessible as a dictionary. - """ - - config_value_pattern = re.compile(r"(?P[A-Za-z0-9_]+)=(?P.*)") - - def __init__(self, input_file: str = ".config") -> None: - """Initialize a BuildConfiguration object.""" - super().__init__() - try: - with open(input_file, "r") as fh: - self._config_data = fh.readlines() - except FileNotFoundError as e: - raise SystemExit(e) - self._parse() - - def _parse(self) -> None: - """Parse input .config file and populate the configuration dictionary.""" - for config_line in self._config_data: - if re_result := self.config_value_pattern.match(config_line): - kconfig_name = re_result.group("kconfig_name") - kconfig_value = re_result.group("kconfig_value") - if kconfig_value == "y": - # boolean value - kconfig_value = True - elif kconfig_value.startswith("0x"): - # hexadecimal value - kconfig_value = int(kconfig_value, base=16) - elif kconfig_value.startswith('"') and kconfig_value.endswith('"'): - # string value - kconfig_value = kconfig_value[1:-1] - elif kconfig_value.isdecimal(): - # int value - kconfig_value = int(kconfig_value, base=10) - super().__setitem__(kconfig_name, kconfig_value) - - def read_configurations(configurations): """Read configuration stored in the pickled devicetree.""" data = {} for config in configurations: - name, binary, edt = config.split(",") - kconfig = pathlib.Path(edt).parent / ".config" - with open(edt, "rb") as edt_handler: - edt = pickle.load(edt_handler) - # add prefix _ to the names starting with digits, for example: - # 802154_rpmsg_subimage will be available in the templates as _802154_rpmsg_subimage - data[f"_{name}" if re.match("^[0-9].*]", name) else name] = { - "name": name, - "config": BuildConfiguration(kconfig), - "dt": edt, - "binary": binary, - } + args = config.split(",") + if len(args) < 4: + raise ValueError("Invalid number of input arguments") + + # Parse obligatory arguments + name, binary, edt, kconfig = args[:4] + + edt_data = None + if edt: + with open(edt, "rb") as edt_handler: + edt_data = pickle.load(edt_handler) + # add prefix _ to the names starting with digits, for example: + # 802154_rpmsg_subimage will be available in the templates as _802154_rpmsg_subimage + image_name = f"_{name}" if re.match("^[0-9].*]", name) else name + + if image_name in data: + existing_binary = data[image_name]["binary"] + raise ValueError( + "Two images have the same CONFIG_SUIT_ENVELOPE_TARGET value: " f"{binary} and {existing_binary}" + ) + + data[image_name] = { + "name": name, + "config": BuildConfiguration(kconfig), + } + if edt_data: + data[image_name]["dt"] = edt_data + if binary: + data[image_name]["filename"] = pathlib.Path(binary).name + data[image_name]["binary"] = binary data["get_absolute_address"] = get_absolute_address return data +def append_default_version_values(cfg): + """Generate DEFAULT_SEQ_NUM and DEFAULT_VERSION variables.""" + extraversion_re = r"^(alpha|beta|rc)[\.]{0,1}([0-9]+){0,1}$" + version = cfg["VERSION"] + + if "APP_ROOT_VERSION" in version: + default_version = version["APP_ROOT_VERSION"] + elif ("VERSION_MAJOR" in version) and ("VERSION_MINOR" in version) and ("PATCHLEVEL" in version): + default_version = version["VERSION_MAJOR"] + "." + version["VERSION_MINOR"] + "." + version["PATCHLEVEL"] + if "EXTRAVERSION" in version: + extra = re.match(extraversion_re, version["EXTRAVERSION"]) + if extra is not None: + default_version += "-" + ".".join([v for v in extra.groups() if v is not None]) + elif len(version["EXTRAVERSION"]) > 0: + # Use the least important pre-release tag for unsupported values + default_version += "-alpha" + else: + default_version = None + + if "APP_ROOT_SEQ_NUM" in version: + default_seq_num = version["APP_ROOT_SEQ_NUM"] + elif ("VERSION_MAJOR" in version) and ("VERSION_MINOR" in version) and ("PATCHLEVEL" in version): + default_seq_num = ( + (int(version["VERSION_MAJOR"]) << 24) + + (int(version["VERSION_MINOR"]) << 16) + + (int(version["PATCHLEVEL"]) << 8) + ) + if "VERSION_TWEAK" in version: + default_seq_num += int(version["VERSION_TWEAK"]) + else: + default_seq_num = 1 + + if "DEFAULT_VERSION" not in version: + if default_version is not None: + cfg["VERSION"]["DEFAULT_VERSION"] = default_version + if "DEFAULT_SEQ_NUM" not in version: + cfg["VERSION"]["DEFAULT_SEQ_NUM"] = f"{default_seq_num}" + + # Handle SCFW versioning schema - it is customized by overwriting the Zephyr's version.cmake file. + if ( + ("SYSCTRL_VERSION_MAJOR" in version) + and ("SYSCTRL_VERSION_MINOR" in version) + and ("SYSCTRL_VERSION_PATCH" in version) + ): + default_scfw_version = ( + version["SYSCTRL_VERSION_MAJOR"] + + "." + + version["SYSCTRL_VERSION_MINOR"] + + "." + + version["SYSCTRL_VERSION_PATCH"] + ) + if "SYSCTRL_VERSION_EXTRA" in version: + extra = re.match(extraversion_re, version["SYSCTRL_VERSION_EXTRA"]) + if extra is not None: + default_scfw_version += "-" + ".".join([v for v in extra.groups() if v is not None]) + elif len(version["SYSCTRL_VERSION_EXTRA"]) > 0: + # Use the least important pre-release tag for unsupported values + default_scfw_version += "-alpha" + + default_scfw_seq_num = ( + (int(version["SYSCTRL_VERSION_MAJOR"]) << 24) + + (int(version["SYSCTRL_VERSION_MINOR"]) << 16) + + (int(version["SYSCTRL_VERSION_PATCH"]) << 8) + ) + if "SYSCTRL_VERSION_TWEAK" in version: + default_scfw_seq_num += int(version["SYSCTRL_VERSION_TWEAK"]) + else: + default_scfw_version = None + default_scfw_seq_num = 1 + + if "SCFW_VERSION" not in version: + if default_scfw_version is not None: + cfg["VERSION"]["SCFW_VERSION"] = default_scfw_version + if "SCFW_SEQ_NUM" not in version: + cfg["VERSION"]["SCFW_SEQ_NUM"] = f"{default_scfw_seq_num}" + + +def read_version_file(version_file): + """Read values from the VERSION configuration file.""" + with open(version_file, "r") as ver_values: + cfg = ConfigParser() + cfg.optionxform = lambda option: option + cfg.read_string("[VERSION]\n" + ver_values.read()) + append_default_version_values(cfg) + return cfg.items("VERSION") + return {} + + def render_template(template_location, data): """Render template using passed data.""" with open(template_location) as template_file: @@ -96,10 +164,8 @@ def render_template(template_location, data): def get_absolute_address(node, use_offset: bool = True): """Get absolute address of passed node.""" - # fixme: hardcoded value for parent node due to bug in DTS - # return node.parent.parent.regs[0].addr + node.regs[0].addr if use_offset: - return 0xE000000 + node.regs[0].addr + return node.parent.parent.regs[0].addr + node.regs[0].addr return node.regs[0].addr @@ -134,6 +200,9 @@ def get_absolute_address(node, use_offset: bool = True): cmd_template_arg_parser.add_argument("--artifacts-folder", required=True, help="Output artifact folder.") cmd_template_arg_parser.add_argument("--template-suit", required=True, help="Input SUIT jinja2 template.") cmd_template_arg_parser.add_argument("--output-suit", required=True, help="Output SUIT configuration.") + cmd_template_arg_parser.add_argument( + "--version_file", required=False, default=None, help="Path to the VERSION file to use." + ) cmd_storage_arg_parser.add_argument( "--input-envelope", required=True, action="append", help="Location of input envelope(s)." @@ -141,11 +210,45 @@ def get_absolute_address(node, use_offset: bool = True): cmd_storage_arg_parser.add_argument( "--storage-output-directory", required=True, help="Directory path to store hex files with SUIT storage contents" ) + cmd_storage_arg_parser.add_argument( + "--storage-address", + required=False, + type=lambda x: int(x, 0), + default=ImageCreator.default_storage_address, + help="Absolute address of the SUIT storage area", + ) + cmd_storage_arg_parser.add_argument( + "--config-file", + required=False, + default=None, + help="Path to KConfig file", + ) + cmd_storage_arg_parser.add_argument( + "--soc", + required=False, + type=str, + default="nrf54h20", + help="SoC device (nrf54h20 or nrf9280)", + ) cmd_update_arg_parser = subparsers.add_parser( UPDATE_CMD, help="Generate files needed for Secure Domain update", parents=[parent_parser] ) + cmd_update_arg_parser.add_argument( + "--update-candidate-info-address", + required=False, + type=lambda x: int(x, 0), + default=ImageCreator.default_update_candidate_info_address, + help="Address of SUIT storage update candidate info.", + ) + cmd_update_arg_parser.add_argument( + "--dfu-max-caches", + required=False, + type=int, + default=ImageCreator.default_dfu_max_caches, + help="Maximum number of caches, allowed to be passed inside update candidate info.", + ) cmd_update_arg_parser.add_argument("--input-file", required=True, help="SUIT envelope in binary format") cmd_update_arg_parser.add_argument( "--storage-output-file", required=True, help="SUIT storage output file in HEX format" @@ -166,6 +269,8 @@ def get_absolute_address(node, use_offset: bool = True): configuration = read_configurations(arguments.core) if arguments.command == TEMPLATE_CMD: + if arguments.version_file is not None: + configuration.update(read_version_file(arguments.version_file)) configuration["output_envelope"] = arguments.output_suit configuration["artifacts_folder"] = arguments.artifacts_folder output_suit_content = render_template(arguments.template_suit, configuration) @@ -173,22 +278,19 @@ def get_absolute_address(node, use_offset: bool = True): output_file.write(output_suit_content) elif arguments.command == STORAGE_CMD: - # fixme: envelope_address, update_candidate_info_address and dfu_max_caches shall be extracted from DTS ImageCreator.create_files_for_boot( input_files=arguments.input_envelope, storage_output_directory=arguments.storage_output_directory, - envelope_address=ImageCreator.default_envelope_address, - envelope_slot_size=ImageCreator.default_envelope_slot_size, - envelope_slot_count=ImageCreator.default_envelope_slot_count, - update_candidate_info_address=ImageCreator.default_update_candidate_info_address, - dfu_max_caches=ImageCreator.default_dfu_max_caches, + storage_address=arguments.storage_address, + config_file=arguments.config_file, + soc=arguments.soc, ) elif arguments.command == UPDATE_CMD: ImageCreator.create_files_for_update( input_file=arguments.input_file, storage_output_file=arguments.storage_output_file, dfu_partition_output_file=arguments.dfu_partition_output_file, - update_candidate_info_address=ImageCreator.default_update_candidate_info_address, + update_candidate_info_address=arguments.update_candidate_info_address, dfu_partition_address=arguments.dfu_partition_address, - dfu_max_caches=ImageCreator.default_dfu_max_caches, + dfu_max_caches=arguments.dfu_max_caches, ) diff --git a/ncs/encrypt_script.py b/ncs/encrypt_script.py new file mode 100644 index 00000000..e200dff5 --- /dev/null +++ b/ncs/encrypt_script.py @@ -0,0 +1,379 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""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 + + +@unique +class SuitAlgorithms(Enum): + """Suit algorithms.""" + + COSE_ALG_AES_GCM_128 = 1 + COSE_ALG_AES_GCM_192 = 2 + COSE_ALG_AES_GCM_256 = 3 + COSE_ALG_A128KW = -3 + COSE_ALG_A192KW = -4 + COSE_ALG_A256KW = -5 + COSE_ALG_DIRECT = -6 + + +class SuitIds(Enum): + """Suit elements identifiers.""" + + COSE_ALG = 1 + COSE_KEY_ID = 4 + 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) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +class DigestGenerator: + """Class to generate digests for plaintext files using specified hash algorithms.""" + + _hash_func = { + SuitDigestAlgorithms.SHA_256.value: hashes.SHA256(), + SuitDigestAlgorithms.SHAKE128.value: hashes.SHAKE128(16), + SuitDigestAlgorithms.SHA_384.value: hashes.SHA384(), + SuitDigestAlgorithms.SHA_512.value: hashes.SHA512(), + SuitDigestAlgorithms.SHAKE256.value: hashes.SHAKE256(32), + } + + def __init__(self, hash_name: str): + """Initialize object.""" + if hash_name not in self._hash_func: + 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() + + 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))) + + +class Encryptor: + """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" + kms_module = _import_module_from_path(module_name, kms_script) + 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(context) + + def generate_kms_artifacts(self, plaintext_file_path: Path, 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, + and returns the encrypted asset and encrypted content encryption key (CEK). + + """ + # Enc structure: + # { + # "context": "Encrypt", + # "protected": {"suit-cose-algorithm-id": "cose-alg-aes-gcm-256"}, + # "external_aad": "", + # } + # bytes(hex): 8367456e637279707443a1010340 + enc_structure_encoded = bytes( + [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: + raise ValueError("AES Key Wrap 256 is not supported yet") + elif self.cose_kw_alg == SuitAlgorithms.COSE_ALG_DIRECT.value: + nonce, tag, ciphertext = self.kms.encrypt( + plaintext=asset_plaintext, + key_name=key_name, + context=context, + aad=enc_structure_encoded, + ) + + encrypted_asset = nonce + tag + ciphertext + + return encrypted_asset, encrypted_cek + + def parse_encrypted_assets(self, asset_bytes): + """Parse the encrypted assets to extract initialization vector, tag, and encrypted content.""" + # Encrypted data is returned in format nonce|tag|encrypted_data + init_vector = asset_bytes[:12] + tag = asset_bytes[12 : 12 + 16] + encrypted_content = asset_bytes[12 + 16 :] + + return init_vector, tag, encrypted_content + + def generate_encrypted_payload(self, encrypted_content, tag, output_directory: Path): + """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) + + def generate_suit_encryption_info(self, iv, encrypted_cek, string_key_id, output_directory: Path): + """Generate the SUIT encryption information file. + + This method creates a CBOR-encoded SUIT encryption information structure and writes it to a binary file. + """ + Cose_Encrypt = [ + # protected + cbor2.dumps( + { + SuitIds.COSE_ALG.value: SuitAlgorithms.COSE_ALG_AES_GCM_256.value, + } + ), + # unprotected + { + SuitIds.COSE_IV.value: bytes(iv), + }, + # ciphertext + None, + # recipients + [ + [ + # protected + b"", + # unprotected + { + SuitIds.COSE_ALG.value: self.cose_kw_alg, + SuitIds.COSE_KEY_ID.value: cbor2.dumps(KEY_IDS[string_key_id]), + }, + # ciphertext + encrypted_cek, + ] + ], + ] + + 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) + + 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. + + 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 + ) + + 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 + ) diff --git a/ncs/multiprotocol_rad_envelope.yaml.jinja2 b/ncs/multiprotocol_rad_envelope.yaml.jinja2 deleted file mode 100644 index 7fd756fa..00000000 --- a/ncs/multiprotocol_rad_envelope.yaml.jinja2 +++ /dev/null @@ -1,99 +0,0 @@ -{%- set mpi_rad_vendor_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_rad_class_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_rad') %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - - - MEM - - {{ multiprotocol_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(multiprotocol_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }} - - {{ multiprotocol_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }} - - - CAND_IMG - - 0 - suit-shared-sequence: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_rad_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ multiprotocol_rpmsg_subimage['binary'] }} - suit-parameter-image-size: - file: {{ multiprotocol_rpmsg_subimage['binary'] }} - - suit-condition-vendor-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-condition-class-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ multiprotocol_rpmsg_subimage['binary'] }} - suit-validate: - - suit-directive-set-component-index: 0 - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-invoke: - - suit-directive-set-component-index: 0 - - suit-directive-invoke: - - suit-send-record-failure - suit-install: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ multiprotocol_rpmsg_subimage['name'] }}' - - suit-directive-fetch: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-text: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-text: - en: - '["MEM", {{ multiprotocol_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }}, {{ get_absolute_address(multiprotocol_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }}, {{ multiprotocol_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }}]': - suit-text-vendor-name: Nordic Semiconductor ASA - suit-text-model-name: nRF54H20_cpurad - suit-text-vendor-domain: nordicsemi.com - suit-text-model-info: The nRF54H20 radio core - suit-text-component-description: Sample radio core FW - suit-text-component-version: v1.0.0 - suit-integrated-payloads: - '#{{ multiprotocol_rpmsg_subimage['name'] }}': {{ multiprotocol_rpmsg_subimage['binary'] }} \ No newline at end of file diff --git a/ncs/nordic_top_envelope.yaml.jinja2 b/ncs/nordic_top_envelope.yaml.jinja2 index 4f208bbf..fe64e43a 100644 --- a/ncs/nordic_top_envelope.yaml.jinja2 +++ b/ncs/nordic_top_envelope.yaml.jinja2 @@ -1,11 +1,16 @@ -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} SUIT_Envelope_Tagged: suit-authentication-wrapper: SuitDigest: suit-digest-algorithm-id: cose-alg-sha-256 suit-manifest: suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} +{%- if NORDIC_TOP_SEQ_NUM is defined %} + suit-manifest-sequence-number: {{ NORDIC_TOP_SEQ_NUM }} +{%- elif DEFAULT_SEQ_NUM is defined %} + suit-manifest-sequence-number: {{ DEFAULT_SEQ_NUM }} +{%- else %} + suit-manifest-sequence-number: 1 +{%- endif %} suit-common: suit-components: - - CAND_MFST @@ -19,20 +24,12 @@ SUIT_Envelope_Tagged: namespace: nordicsemi.com name: nRF54H20_sys suit-shared-sequence: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_sec - - suit-directive-set-component-index: 2 + - suit-directive-set-component-index: [1,2] - suit-directive-override-parameters: suit-parameter-class-identifier: RFC4122_UUID: namespace: nordicsemi.com - name: nRF54H20_sys - - suit-directive-set-component-index: [1,2] - - suit-directive-override-parameters: + name: nRF54H20_nordic_top suit-parameter-vendor-identifier: RFC4122_UUID: nordicsemi.com - suit-condition-vendor-identifier: @@ -96,8 +93,46 @@ SUIT_Envelope_Tagged: - suit-send-record-failure - suit-send-sysinfo-success - suit-send-sysinfo-failure + +{%- if NORDIC_TOP_VERSION is defined %} + suit-current-version: {{ NORDIC_TOP_VERSION }} +{%- elif DEFAULT_VERSION is defined %} + suit-current-version: {{ DEFAULT_VERSION }} +{%- endif %} + suit-install: - suit-directive-set-component-index: 0 + - suit-directive-override-parameters: + suit-parameter-uri: '#{{ secdom['name'] }}' + - suit-directive-fetch: + - suit-send-record-failure + - suit-condition-dependency-integrity: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-process-dependency: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-override-parameters: + suit-parameter-uri: '#{{ sysctrl['name'] }}' + - suit-directive-fetch: + - suit-send-record-failure + - suit-condition-dependency-integrity: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-process-dependency: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + + suit-candidate-verification: + - suit-directive-set-component-index: 0 - suit-directive-override-parameters: suit-parameter-uri: '#{{ secdom['name'] }}' suit-parameter-image-digest: @@ -144,6 +179,7 @@ SUIT_Envelope_Tagged: - suit-send-record-failure - suit-send-sysinfo-success - suit-send-sysinfo-failure + suit-manifest-component-id: - INSTLD_MFST - RFC4122_UUID: diff --git a/ncs/rad_envelope.yaml.jinja2 b/ncs/rad_envelope.yaml.jinja2 deleted file mode 100644 index 800d8fbc..00000000 --- a/ncs/rad_envelope.yaml.jinja2 +++ /dev/null @@ -1,99 +0,0 @@ -{%- set mpi_rad_vendor_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_rad_class_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_rad') %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - - - MEM - - {{ hci_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(hci_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }} - - {{ hci_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }} - - - CAND_IMG - - 0 - suit-shared-sequence: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_rad_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ hci_rpmsg_subimage['binary'] }} - suit-parameter-image-size: - file: {{ hci_rpmsg_subimage['binary'] }} - - suit-condition-vendor-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-condition-class-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ hci_rpmsg_subimage['binary'] }} - suit-validate: - - suit-directive-set-component-index: 0 - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-invoke: - - suit-directive-set-component-index: 0 - - suit-directive-invoke: - - suit-send-record-failure - suit-install: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ hci_rpmsg_subimage['name'] }}' - - suit-directive-fetch: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure - - suit-condition-image-match: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - suit-text: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} - suit-text: - en: - '["MEM", {{ hci_rpmsg_subimage['dt'].label2node['cpu'].unit_addr }}, {{ get_absolute_address(hci_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition']) }}, {{ hci_rpmsg_subimage['dt'].chosen_nodes['zephyr,code-partition'].regs[0].size }}]': - suit-text-vendor-name: Nordic Semiconductor ASA - suit-text-model-name: nRF54H20_cpurad - suit-text-vendor-domain: nordicsemi.com - suit-text-model-info: The nRF54H20 radio core - suit-text-component-description: Sample radio core FW - suit-text-component-version: v1.0.0 - suit-integrated-payloads: - '#{{ hci_rpmsg_subimage['name'] }}': {{ hci_rpmsg_subimage['binary'] }} \ No newline at end of file diff --git a/ncs/root_with_nordic_top_envelope.yaml.jinja2 b/ncs/root_with_nordic_top_envelope.yaml.jinja2 index dfbc26de..f385e8b5 100644 --- a/ncs/root_with_nordic_top_envelope.yaml.jinja2 +++ b/ncs/root_with_nordic_top_envelope.yaml.jinja2 @@ -1,12 +1,11 @@ {%- set component_index = 0 %} {%- set component_list = [] %} -{%- set mpi_root_vendor_name = app['config']['CONFIG_SUIT_MPI_ROOT_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_root_class_name = app['config']['CONFIG_SUIT_MPI_ROOT_CLASS_NAME']|default('nRF54H20_sample_root') %} -{%- set mpi_app_vendor_name = app['config']['CONFIG_SUIT_MPI_APP_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_app_class_name = app['config']['CONFIG_SUIT_MPI_APP_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_app') %} -{%- set mpi_rad_vendor_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} -{%- set mpi_rad_class_name = app['config']['CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_rad') %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} +{%- set mpi_root_vendor_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_ROOT_VENDOR_NAME']|default('nordicsemi.com') %} +{%- set mpi_root_class_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_ROOT_CLASS_NAME']|default('nRF54H20_sample_root') %} +{%- set mpi_app_vendor_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_APP_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} +{%- set mpi_app_class_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_APP_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_app') %} +{%- set mpi_rad_vendor_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME']|default('nordicsemi.com') %} +{%- set mpi_rad_class_name = sysbuild['config']['SB_CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME']|default('nRF54H20_sample_rad') %} {%- if hci_rpmsg_subimage is defined %} {% set rad = hci_rpmsg_subimage %} {%- elif _802154_rpmsg_subimage is defined %} @@ -20,12 +19,18 @@ SUIT_Envelope_Tagged: suit-digest-algorithm-id: cose-alg-sha-256 suit-manifest: suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} +{%- if APP_ROOT_SEQ_NUM is defined %} + suit-manifest-sequence-number: {{ APP_ROOT_SEQ_NUM }} +{%- elif DEFAULT_SEQ_NUM is defined %} + suit-manifest-sequence-number: {{ DEFAULT_SEQ_NUM }} +{%- else %} + suit-manifest-sequence-number: 1 +{%- endif %} suit-common: suit-components: - - CAND_MFST - 0 -{%- if rad is defined %} +{%- if radio is defined %} {%- set component_index = component_index + 1 %} {%- set rad_component_index = component_index %} {{- component_list.append( rad_component_index ) or ""}} @@ -34,7 +39,7 @@ SUIT_Envelope_Tagged: namespace: {{ mpi_rad_vendor_name }} name: {{ mpi_rad_class_name }} {%- endif %} -{%- if app is defined %} +{%- if application is defined %} {%- set component_index = component_index + 1 %} {%- set app_component_index = component_index %} {{- component_list.append( app_component_index ) or ""}} @@ -56,39 +61,14 @@ SUIT_Envelope_Tagged: {%- endif %} suit-shared-sequence: -{%- if rad is defined %} - - suit-directive-set-component-index: {{ rad_component_index }} - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_rad_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_rad_vendor_name }} - name: {{ mpi_rad_class_name }} -{%- endif %} -{%- if app is defined %} - - suit-directive-set-component-index: {{ app_component_index }} - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: {{ mpi_app_vendor_name }} - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: {{ mpi_app_vendor_name }} - name: {{ mpi_app_class_name }} -{%- endif %} - -{%- if top is defined %} - - suit-directive-set-component-index: {{ top_component_index }} + - suit-directive-set-component-index: [{{ component_list|join(',') }}] - suit-directive-override-parameters: suit-parameter-vendor-identifier: - RFC4122_UUID: nordicsemi.com + RFC4122_UUID: {{ mpi_root_vendor_name }} suit-parameter-class-identifier: RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_nordic_top -{%- endif %} - - - suit-directive-set-component-index: [{{ component_list|join(',') }}] + namespace: {{ mpi_root_vendor_name }} + name: {{ mpi_root_class_name }} - suit-condition-vendor-identifier: - suit-send-record-success - suit-send-record-failure @@ -131,15 +111,72 @@ SUIT_Envelope_Tagged: - suit-send-sysinfo-success - suit-send-sysinfo-failure +{%- if APP_ROOT_VERSION is defined %} + suit-current-version: {{ APP_ROOT_VERSION }} +{%- elif DEFAULT_VERSION is defined %} + suit-current-version: {{ DEFAULT_VERSION }} +{%- endif %} + suit-install: - suit-directive-set-component-index: 0 -{%- if rad is defined %} +{%- if radio is defined %} + - suit-directive-override-parameters: + suit-parameter-uri: '#{{ radio['name'] }}' + - suit-directive-fetch: + - suit-send-record-failure + - suit-condition-dependency-integrity: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-process-dependency: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure +{%- endif %} +{%- if application is defined %} + - suit-directive-override-parameters: + suit-parameter-uri: '#{{ application['name'] }}' + - suit-directive-fetch: + - suit-send-record-failure + - suit-condition-dependency-integrity: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-process-dependency: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure +{%- endif %} +{%- if top is defined %} + - suit-directive-override-parameters: + suit-parameter-uri: '#{{ top['name'] }}' + - suit-directive-fetch: + - suit-send-record-failure + - suit-condition-dependency-integrity: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure + - suit-directive-process-dependency: + - suit-send-record-success + - suit-send-record-failure + - suit-send-sysinfo-success + - suit-send-sysinfo-failure +{%- endif %} + + suit-candidate-verification: + - suit-directive-set-component-index: 0 +{%- if radio is defined %} - suit-directive-override-parameters: - suit-parameter-uri: '#{{ rad['name'] }}' + suit-parameter-uri: '#{{ radio['name'] }}' suit-parameter-image-digest: suit-digest-algorithm-id: cose-alg-sha-256 suit-digest-bytes: - envelope: {{ artifacts_folder ~ rad['name'] }}.suit + envelope: {{ artifacts_folder ~ radio['name'] }}.suit - suit-directive-fetch: - suit-send-record-failure - suit-condition-image-match: @@ -158,13 +195,13 @@ SUIT_Envelope_Tagged: - suit-send-sysinfo-success - suit-send-sysinfo-failure {%- endif %} -{%- if app is defined %} +{%- if application is defined %} - suit-directive-override-parameters: - suit-parameter-uri: '#{{ app['name'] }}' + suit-parameter-uri: '#{{ application['name'] }}' suit-parameter-image-digest: suit-digest-algorithm-id: cose-alg-sha-256 suit-digest-bytes: - envelope: {{ artifacts_folder ~ app['name'] }}.suit + envelope: {{ artifacts_folder ~ application['name'] }}.suit - suit-directive-fetch: - suit-send-record-failure - suit-condition-image-match: @@ -208,17 +245,18 @@ SUIT_Envelope_Tagged: - suit-send-sysinfo-success - suit-send-sysinfo-failure {%- endif %} + suit-manifest-component-id: - INSTLD_MFST - RFC4122_UUID: namespace: {{ mpi_root_vendor_name }} name: {{ mpi_root_class_name }} suit-integrated-dependencies: -{%- if rad is defined %} - '#{{ rad['name'] }}': {{ artifacts_folder ~ rad['name'] }}.suit +{%- if radio is defined %} + '#{{ radio['name'] }}': {{ artifacts_folder ~ radio['name'] }}.suit {%- endif %} -{%- if app is defined %} - '#{{ app['name'] }}': {{ artifacts_folder ~ app['name'] }}.suit +{%- if application is defined %} + '#{{ application['name'] }}': {{ artifacts_folder ~ application['name'] }}.suit {%- endif %} {%- if top is defined %} '#{{ top['name'] }}': {{ artifacts_folder ~ top['name'] }}.suit diff --git a/ncs/secdom_update_envelope.yaml.jinja2 b/ncs/secdom_update_envelope.yaml.jinja2 deleted file mode 100644 index 4125045f..00000000 --- a/ncs/secdom_update_envelope.yaml.jinja2 +++ /dev/null @@ -1,60 +0,0 @@ -{%- if secdom is not defined %} - {# secure domain build as main application #} - {%- set secdom = app %} -{%- endif %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - - - SOC_SPEC - - 1 - - - CAND_IMG - - 0 - suit-shared-sequence: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-vendor-identifier: - RFC4122_UUID: - name: nordicsemi.com - suit-parameter-class-identifier: - RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_sec - - suit-condition-vendor-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure - - suit-condition-class-identifier: - - suit-send-record-success - - suit-send-record-failure - - suit-send-sysinfo-success - - suit-send-sysinfo-failure -{%- if 'CONFIG_HW_REVISION_SOC1' in app['config'] %} - suit-install: [] -{%- else %} - suit-install: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ secdom['name'] }}' - - suit-directive-fetch: - - suit-send-record-failure - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: - - suit-send-record-failure -{%- endif %} - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_sec - suit-integrated-payloads: - '#{{ secdom['name'] }}': {{ secdom['binary'] }} \ No newline at end of file diff --git a/ncs/sign_script.py b/ncs/sign_script.py index 5203ba12..92ea6820 100644 --- a/ncs/sign_script.py +++ b/ncs/sign_script.py @@ -22,15 +22,25 @@ from argparse import ArgumentParser from pathlib import Path from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.serialization import load_pem_private_key +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 -PRIVATE_KEY = Path(__file__).parent / "key_private.pem" +# +# 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", +} @unique @@ -58,9 +68,13 @@ class SuitIds(Enum): SUIT_MANIFEST_COMPONENT_ID = 5 -DEFAULT_KEY_ID = 0x7FFFFFE0 +DEFAULT_KEY_ID = 0x40000000 -KEY_IDS = {"nRF54H20_sample_root": 0x7FFFFFE0, "nRF54H20_sample_app": 0x7FFFFFE0, "nRF54H20_sample_rad": 0x7FFFFFE0} +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" @@ -118,7 +132,7 @@ 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): + 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") @@ -129,7 +143,7 @@ def _algorithm_name(self) -> str: 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): + 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") @@ -160,10 +174,26 @@ def _get_manifest_class_id(self): def _get_key_id_for_manifest_class(self): return self._key_ids[self._get_manifest_class_id()] - def sign(self, private_key_path: Path) -> None: + 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 = load_pem_private_key(private_key.read(), None) + self._key = loader(private_key.read(), None) sign_method = self._get_sign_method() protected = { SuitIds.COSE_ALG.value: SuitAlgorithms[self._algorithm_name].value, @@ -183,5 +213,5 @@ def sign(self, private_key_path: Path) -> None: signer = Signer() signer.load_envelope(arguments.input_file) - signer.sign(PRIVATE_KEY) + signer.sign() signer.save_envelope(arguments.output_file) diff --git a/ncs/sysctrl_envelope.yaml.jinja2 b/ncs/sysctrl_envelope.yaml.jinja2 deleted file mode 100644 index 699d2a6b..00000000 --- a/ncs/sysctrl_envelope.yaml.jinja2 +++ /dev/null @@ -1,82 +0,0 @@ -{# example template - need to be updated #} -{%- if sysctrl is not defined %} - {# sysctrl domain build as main application #} - {%- set sysctrl = app %} -{%- endif %} -{%- set sequence_number = app['config']['CONFIG_SUIT_ENVELOPE_SEQUENCE_NUM'] %} -SUIT_Envelope_Tagged: - suit-authentication-wrapper: - SuitDigest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-manifest: - suit-manifest-version: 1 - suit-manifest-sequence-number: {{ sequence_number }} - suit-common: - suit-components: - # [0], RAM - - - MEM - - {{ sysctrl['dt'].label2node['cpu'].unit_addr }} - - {{ get_absolute_address(sysctrl['dt'].chosen_nodes['zephyr,flash'], use_offset=False) }} - - {{ sysctrl['dt'].chosen_nodes['zephyr,flash'].regs[0].size }} - # [1], MRAM - - - MEM - - -1 - - {{ get_absolute_address(sysctrl['dt'].label2node['sysctrl_mram0'], use_offset=False) }} - - {{ sysctrl['dt'].label2node['sysctrl_mram0'].regs[0].size }} - # [2], Pseudo-component, used to verify the integrity of the incoming image. - # It is used as a target component for all fetch operations that download firmware. - - - CAND_IMG - - 0 - suit-shared-sequence: - - suit-directive-set-component-index: [0, 1, 2] - - suit-directive-override-parameters: - suit-parameter-image-digest: - suit-digest-algorithm-id: cose-alg-sha-256 - suit-digest-bytes: - file: {{ sysctrl['binary'] }} - suit-validate: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-size: - file: {{ sysctrl['binary'] }} - - suit-condition-image-match: [] - suit-load: - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-image-size: - file: {{ sysctrl['binary'] }} - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-source-component: 1 - - suit-directive-copy: [] - - suit-condition-image-match: [] - suit-invoke: - - suit-directive-set-component-index: 0 - - suit-directive-override-parameters: - suit-parameter-image-size: - file: {{ sysctrl['binary'] }} - - suit-condition-image-match: [] - - suit-directive-invoke: [] - suit-payload-fetch: - - suit-directive-set-component-index: 2 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ sysctrl['name'] }}' - - suit-directive-fetch: [] - - suit-condition-image-match: [] - suit-install: - - suit-directive-set-component-index: 2 - - suit-directive-override-parameters: - suit-parameter-uri: '#{{ sysctrl['name'] }}' - - suit-directive-fetch: [] - - suit-condition-image-match: [] - - suit-directive-set-component-index: 1 - - suit-directive-override-parameters: - suit-parameter-source-component: 2 - - suit-directive-copy: [] - suit-manifest-component-id: - - INSTLD_MFST - - RFC4122_UUID: - namespace: nordicsemi.com - name: nRF54H20_sys - suit-integrated-payloads: - '#{{ sysctrl['name'] }}': {{ sysctrl['binary'] }} diff --git a/pyproject.toml b/pyproject.toml index 70454349..6133895d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dynamic = ["version","dependencies"] dependencies = {file = ["requirements.txt"]} [tool.setuptools] -packages=["suit_generator"] +packages=["suit_generator","build_configuration"] [project.scripts] suit-generator = "suit_generator.cli:main" diff --git a/suit_generator/__init__.py b/suit_generator/__init__.py index f302f4e2..12e7f21e 100644 --- a/suit_generator/__init__.py +++ b/suit_generator/__init__.py @@ -23,19 +23,3 @@ Version of tool is autogenerated using setuptools_scm. To create new version just add git tag v.X.Y.Z """ -import logging -from logging.config import dictConfig - -import yaml -import os -import pathlib - -dir_path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) - -with open(dir_path / "logging.yaml", "r") as stream: - config = yaml.load(stream, Loader=yaml.FullLoader) - -logging.config.dictConfig(config) - -logger = logging.getLogger(__name__) -logger.debug("suit-generator initialized and logging configuration loaded") diff --git a/suit_generator/args.py b/suit_generator/args.py index 4c765765..26be64f2 100644 --- a/suit_generator/args.py +++ b/suit_generator/args.py @@ -15,10 +15,14 @@ from suit_generator.cmd_image import add_arguments as image_args from suit_generator.cmd_convert import add_arguments as convert_args 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 def _parser() -> ArgumentParser: parser = ArgumentParser() + parser.add_argument("--log-filename", default=None, help="Log file path (it will override cli defaults).") + subparsers = parser.add_subparsers(dest="command", required=True, help="Choose subcommand:") create_args(subparsers) parse_args(subparsers) @@ -26,6 +30,8 @@ def _parser() -> ArgumentParser: image_args(subparsers) convert_args(subparsers) mpi_args(subparsers) + cache_create_args(subparsers) + payload_extract_args(subparsers) return parser @@ -35,13 +41,15 @@ def parse_arguments() -> Tuple: Parse passed CLI parameters and return argparse.Namespace - :return: Tuple contains command and it's parameters + :return: Tuple contains command, it's parameters and log filename """ parser = _parser() arguments = parser.parse_args() cmd = str(arguments.command) + log_filename = arguments.log_filename # remove unnecessary arguments to simplify command calling del arguments.command + del arguments.log_filename - return cmd, arguments + return cmd, arguments, log_filename diff --git a/suit_generator/cli.py b/suit_generator/cli.py index 5527b407..6518e945 100644 --- a/suit_generator/cli.py +++ b/suit_generator/cli.py @@ -11,10 +11,24 @@ sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) -from suit_generator import cmd_parse, cmd_keys, cmd_convert, cmd_create, cmd_image, cmd_mpi, args +from suit_generator import ( + cmd_parse, + cmd_keys, + cmd_convert, + cmd_create, + cmd_image, + cmd_mpi, + cmd_cache_create, + cmd_payload_extract, + args, +) from suit_generator.exceptions import GeneratorError, SUITError import logging +import logging.config +import yaml + +from pathlib import Path logger = logging.getLogger(__name__) @@ -25,12 +39,41 @@ cmd_convert.CONVERT_CMD: cmd_convert.main, cmd_image.ImageCreator.IMAGE_CMD: cmd_image.main, 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, } +def configure_cli_logging(log_file_name: str = None): + """ + Configure logging for CLI. + + :param log_file_name: log file name to be used (override default log file name from logging.yaml) + """ + dir_path = Path(__file__).resolve().parent + + with open(dir_path / "logging.yaml", "r") as stream: + config = yaml.load(stream, Loader=yaml.FullLoader) + + # override log file name if passed as argument + if log_file_name: + config["handlers"]["file"]["filename"] = log_file_name + + logging.config.dictConfig(config) + + # any logger initialized before call to 'configure_logging' will be disabled because logging.yaml + # contains entry 'disable_existing_loggers: true', if yaml has no explicit configuration for it + # so we need to defer logger creation (call to 'getLogger') after 'configure_logging' call or enable it manually + logger.disabled = False + logger.debug("*** suit-generator initialized and logging configuration loaded") + + def main() -> None: """Parse input arguments and call passed CMD executor.""" - command, arguments = args.parse_arguments() + command, arguments, log_file = args.parse_arguments() + + configure_cli_logging(log_file) + # passing arguments as kwargs used to simplify commands calling, improve documentation and error handling try: COMMAND_EXECUTORS[command](**vars(arguments)) diff --git a/suit_generator/cmd_cache_create.py b/suit_generator/cmd_cache_create.py new file mode 100644 index 00000000..2713ca6f --- /dev/null +++ b/suit_generator/cmd_cache_create.py @@ -0,0 +1,328 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""CMD_CACHE_CREATE CLI command entry point.""" + +import logging +import math +import cbor2 +import re + +from suit_generator.exceptions import GeneratorError + +CACHE_CREATE_CMD = "cache_create" +CACHE_CREATE_FROM_PAYLOADS_CMD = "from_payloads" +CACHE_CREATE_FROM_ENVELOPE_CMD = "from_envelope" +CACHE_MERGE_CMD = "merge" + +log = logging.getLogger(__name__) + + +def add_arguments(parser): + """Add additional arguments to the passed parser.""" + cmd_cache_create = parser.add_parser(CACHE_CREATE_CMD, help="Create raw cache structure.") + + cmd_cache_create_subparsers = cmd_cache_create.add_subparsers( + dest="cache_create_subcommand", required=True, help="Choose cache_create subcommand" + ) + cmd_cache_create_from_payloads = cmd_cache_create_subparsers.add_parser( + CACHE_CREATE_FROM_PAYLOADS_CMD, + help="Create a cache partition from the provided binaries containing raw payloads.", + ) + + cmd_cache_create_from_payloads.add_argument("--output-file", required=True, help="Output raw SUIT DFU cache file.") + cmd_cache_create_from_payloads.add_argument( + "--eb-size", type=int, help="Erase block size in bytes (used for padding).", default=16 + ) + + cmd_cache_create_from_payloads.add_argument( + "--input", + required=True, + action="append", + help="Input binary with corresponding URI, passed in format ,." + + "Multiple inputs can be passed.", + ) + + cmd_cache_create_from_envelope = cmd_cache_create_subparsers.add_parser( + CACHE_CREATE_FROM_ENVELOPE_CMD, help="Create a cache partition from the payloads inside the provided envelope." + ) + + cmd_cache_create_from_envelope.add_argument("--output-file", required=True, help="Output raw SUIT DFU cache file.") + cmd_cache_create_from_envelope.add_argument( + "--eb-size", type=int, help="Erase block size in bytes (used for padding).", default=16 + ) + + cmd_cache_create_from_envelope.add_argument("--input-envelope", required=True, help="Input envelope file path.") + + cmd_cache_create_from_envelope.add_argument( + "--output-envelope", required=True, help="Output envelope file path (envelope with removed extracted payloads)." + ) + + cmd_cache_create_from_envelope.add_argument( + "--omit-payload-regex", + help="Integrated payloads matching the regular expression will not be extracted to the cache.", + ) + + cmd_cache_create_from_envelope.add_argument( + "--dependency-regex", + help="Integrated payloads matching the regular expression will be treated as dependency" + + "envelopes and parsed hierarchically. " + + "The payloads extracted from the dependency envelopes will be added to the cache.", + ) + + cmd_cache_merge = cmd_cache_create_subparsers.add_parser( + CACHE_MERGE_CMD, help="Merge multiple cache partitions into a single cache partition." + ) + + cmd_cache_merge.add_argument("--input", required=True, action="append", help="Input raw SUIT DFU cache file.") + cmd_cache_merge.add_argument("--output-file", required=True, help="Output raw SUIT DFU cache file.") + cmd_cache_merge.add_argument( + "--eb-size", type=int, help="Erase block size in bytes (used for padding).", default=16 + ) + + +class CachePartition: + """Class generating SUIT DFU Cache Partition.""" + + def __init__(self, eb_size: int): + """Initialize a CachePartition object.""" + self.first_slot = True + self.cache_data = bytes() + self.eb_size = eb_size + self.uris = [] + + def add_padding(self, data: bytes) -> bytes: + """ + Add padding to the given data to align it to the specified erase block size. + + This method ensures that the data is padded to a size that is a multiple of the erase block size (self.eb_size). + The padding is done by appending a CBOR key-value pair with empty URI as the key and + byte-string-encoded zeros as the value. + + :param data: The input data to be padded. + :type data: bytes + :return: The padded data. + """ + rounded_up_size = math.ceil(len(data) / self.eb_size) * self.eb_size + padding_size = rounded_up_size - len(data) + padded_data = data + + # minimum padding size is 2 bytes + if padding_size == 1: + padding_size += self.eb_size + rounded_up_size += self.eb_size + + if padding_size == 0: + return data + + padded_data += bytes([0x60]) + + if padding_size <= 23: + header_len = 2 + padded_data += bytes([0x40 + (padding_size - header_len)]) + elif padding_size <= 0xFFFF: + header_len = 4 + padded_data += bytes([0x59]) + (padding_size - header_len).to_bytes(2, byteorder="big") + else: + raise ValueError("Number of required padding bytes exceeds assumed max size 0xFFFF") + + return padded_data.ljust(rounded_up_size, b"\x00") + + def add_cache_slot(self, uri: str, data: bytes): + """ + Add a cache slot to the cache from the given URI and data. + + This function creates a cache slot from the given URI and data, and pads the data to align with the specified + erase block size (eb_size). The first slot in the cache is created with indefinite length CBOR map. + + :param uri: The URI associated with the data. + :type uri: str + :param data: The data to be included in the cache slot. + :type data: bytes + """ + slot_data = bytes() + if self.first_slot: + # Open the cache - it is an indefinite length CBOR map (0xBF) + slot_data = bytes([0xBF]) + self.first_slot = False + + if uri in self.uris: + raise ValueError(f"URI {uri} already exists in the cache!") + self.uris.append(uri) + + # uri as key + slot_data += cbor2.dumps(uri) + + # Size must be encoded in 4 bytes, thus cannot use cbor2.dumps + slot_data += bytes([0x5A]) + len(data).to_bytes(4, byteorder="big") + data + # Add padding for single slot + slot_data = self.add_padding(slot_data) + + self.cache_data += slot_data + + def close_and_save_cache(self, output_file: str): + """ + Close the cache and save it to the specified output file. + + This function closes the cache by adding the end-of-map byte (0xFF) and saves the cache to the specified output + file. + + :param output_file: Path to the output raw SUIT DFU cache file. + :type output_file: str + """ + self.cache_data += bytes([0xFF]) + with open(output_file, "wb") as f: + f.write(self.cache_data) + + def merge_single_cache_file(self, cache_input_file: str): + """ + Merge the contents of the provided single cache file into the current cache. + + :param cache_input_file: Path to the input raw SUIT DFU cache file. + """ + with open(cache_input_file, "rb") as f: + data = f.read() + + cache_dict = cbor2.loads(data) + + for k in cache_dict.keys(): + if len(k) == 0: + continue # Empty key means padding - skip + self.add_cache_slot(k, cache_dict[k]) + + +class CacheFromPayloads: + """Class generating SUIT DFU Cache Partition from payloads.""" + + def fill_cache_from_payloads(cache: CachePartition, input: list[str]) -> None: + """ + Process list of input binaries, each associated with a URI, and fill the SUIT DFU cache with the data. + + :param cache: CachePartition object to fill with the data + :param input: List of input binaries with corresponding URIs, passed in the format , + """ + for single_input in input: + args = single_input.split(",") + if len(args) < 2: + raise ValueError("Invalid number of input arguments: " + single_input) + uri, input_file = args + + with open(input_file, "rb") as f: + data = f.read() + + cache.add_cache_slot(uri, data) + + +class CacheFromEnvelope: + """Class generating SUIT DFU Cache Partition from envelope.""" + + def fill_cache_from_envelope_data( + cache: CachePartition, envelope_data: bytes, omit_payload_regex: str, dependency_regex: str + ) -> bytes: + """ + Fill the cache partition with data from the payloads inside the provided envelope binary data. + + This function is called recursively for dependency envelopes. + :param cache: CachePartition object to fill with the data + :param envelope_data: Binary data of the envelope to extract the payloads from + :param omit_payload_regex: Integrated payloads matching the regular expression will not be extracted to the + cache + :param dependency_regex: Integrated payloads matching the regular expression will be treated as dependency + envelopes + """ + try: + envelope = cbor2.loads(envelope_data) + except Exception: + raise GeneratorError("The provided envelope/dependency envelope is not a valid envelope!") + + if isinstance(envelope, cbor2.CBORTag) and isinstance(envelope.value, dict): + integrated = [k for k in envelope.value.keys() if isinstance(k, str)] + else: + raise GeneratorError("The provided envelope/dependency envelope is not a valid envelope!") + + if dependency_regex is not None: + integrated_dependencies = [k for k in integrated if not re.fullmatch(dependency_regex, k) is None] + for dep in integrated_dependencies: + integrated.remove(dep) + else: + integrated_dependencies = [] + + if omit_payload_regex is None: + payloads_to_extract = integrated + else: + payloads_to_extract = [k for k in integrated if re.fullmatch(omit_payload_regex, k) is None] + + for payload in payloads_to_extract: + cache.add_cache_slot(payload, envelope.value.pop(payload)) + + for dependency in integrated_dependencies: + try: + new_dependency_data = CacheFromEnvelope.fill_cache_from_envelope_data( + cache, envelope.value[dependency], omit_payload_regex, dependency_regex + ) + except GeneratorError as e: + log.log(logging.ERROR, "Failed to extract payloads from dependency %s: %s", dependency, repr(e)) + raise GeneratorError("Failed to extract payloads from envelope!") + + envelope.value[dependency] = new_dependency_data + + return cbor2.dumps(envelope) + + def fill_cache_from_envelope( + cache: CachePartition, input_envelope: str, output_envelope: str, omit_payload_regex: str, dependency_regex: str + ) -> None: + """ + Extract the payloads from the provided envelope to the cache partition file. + + param cache: CachePartition object to fill with the data + param input_envelope: Path to the input envelope file + param output_envelope: Path to the output envelope file (envelope with removed extracted payloads) + param omit_payload_regex: Integrated payloads matching the regular expression will not be extracted to the cache + param dependency_regex: Integrated payloads matching the regular expression will be treated as dependency + envelopes + """ + with open(input_envelope, "rb") as fh: + data = fh.read() + output_envelope_data = CacheFromEnvelope.fill_cache_from_envelope_data( + cache, data, omit_payload_regex, dependency_regex + ) + with open(output_envelope, "wb") as fh: + fh.write(output_envelope_data) + + +class CacheMerge: + """Class merging SUIT DFU Cache Partitions.""" + + def merge_cache_files(cache: CachePartition, input: list[str]) -> None: + """ + Merge the contents of the provided cache files into the cache partition. + + :param cache: CachePartition object to merge the cache files into + :param input: List of paths to the input raw SUIT DFU cache files + """ + for single_input in input: + cache.merge_single_cache_file(single_input) + + +def main(**kwargs) -> None: + """Create a raw SUIT DFU cache file.""" + cache = CachePartition(kwargs["eb_size"]) + + if kwargs["cache_create_subcommand"] == CACHE_CREATE_FROM_PAYLOADS_CMD: + CacheFromPayloads.fill_cache_from_payloads(cache, kwargs["input"]) + elif kwargs["cache_create_subcommand"] == CACHE_CREATE_FROM_ENVELOPE_CMD: + CacheFromEnvelope.fill_cache_from_envelope( + cache, + kwargs["input_envelope"], + kwargs["output_envelope"], + kwargs["omit_payload_regex"], + kwargs["dependency_regex"], + ) + elif kwargs["cache_create_subcommand"] == CACHE_MERGE_CMD: + CacheMerge.merge_cache_files(cache, kwargs["input"]) + else: + raise GeneratorError(f"Invalid 'cache_create' subcommand: {kwargs['cache_create_subcommand']}") + + cache.close_and_save_cache(kwargs["output_file"]) diff --git a/suit_generator/cmd_convert.py b/suit_generator/cmd_convert.py index 24ff6bb9..678a6720 100644 --- a/suit_generator/cmd_convert.py +++ b/suit_generator/cmd_convert.py @@ -250,17 +250,29 @@ def _get_public_key_data(self) -> bytes: # TODO: Consider adding support for keys protected by password private_key = serialization.load_pem_private_key(data=fd.read(), password=None) - public_key_numbers = private_key.public_key().public_numbers() + public_key_bytes = None - # Make sure that if bit length is not aligned to 8, full bytes will be used - x_byte_length = (public_key_numbers.x.bit_length() + 7) // 8 - y_byte_length = (public_key_numbers.y.bit_length() + 7) // 8 + try: + public_key_numbers = private_key.public_key().public_numbers() - # Convert the numbers into bytes - x_bytes = public_key_numbers.x.to_bytes(length=x_byte_length, byteorder="big") - y_bytes = public_key_numbers.y.to_bytes(length=y_byte_length, byteorder="big") + # Make sure that if bit length is not aligned to 8, full bytes will be used + x_byte_length = (public_key_numbers.x.bit_length() + 7) // 8 + y_byte_length = (public_key_numbers.y.bit_length() + 7) // 8 - return x_bytes + y_bytes + # Convert the numbers into bytes + x_bytes = public_key_numbers.x.to_bytes(length=x_byte_length, byteorder="big") + y_bytes = public_key_numbers.y.to_bytes(length=y_byte_length, byteorder="big") + + public_key_bytes = x_bytes + y_bytes + except AttributeError: + # Depending on the key type, some attributes may not be available + # Thus, the method of getting the public key data is different + public_key_bytes = private_key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + + return public_key_bytes def _split_bytes_per_row(self, data: bytes) -> list[bytes]: return [data[i : i + self._columns_count] for i in range(0, len(data), self._columns_count)] diff --git a/suit_generator/cmd_image.py b/suit_generator/cmd_image.py index 0bee664a..dea42305 100644 --- a/suit_generator/cmd_image.py +++ b/suit_generator/cmd_image.py @@ -10,6 +10,7 @@ import os import struct import uuid +import re from enum import Enum from cbor2 import dumps as cbor_dumps @@ -19,6 +20,7 @@ from suit_generator.exceptions import GeneratorError from suit_generator.exceptions import SUITError from suit_generator.suit.manifest import SuitManifest +from build_configuration.configuration import BuildConfiguration def add_arguments(parser): @@ -43,40 +45,17 @@ def add_arguments(parser): "--storage-output-directory", required=True, help="Output hex file with SUIT storage contents" ) cmd_image_boot.add_argument( - "--update-candidate-info-address", - required=False, - type=lambda x: int(x, 0), - default=ImageCreator.default_update_candidate_info_address, - help=f"Address of SUIT storage update candidate info. " - f"Default: 0x{ImageCreator.default_update_candidate_info_address:08X}", - ) - cmd_image_boot.add_argument( - "--envelope-address", + "--storage-address", required=False, type=lambda x: int(x, 0), - default=ImageCreator.default_envelope_address, - help=f"Address of installed envelope in SUIT storage. Default: 0x{ImageCreator.default_envelope_address:08X}", + default=ImageCreator.default_storage_address, + help=f"Address of SUIT storage. Default: 0x{ImageCreator.default_storage_address:08X}", ) cmd_image_boot.add_argument( - "--envelope-slot-size", + "--config-file", required=False, - type=lambda x: int(x, 0), - default=ImageCreator.default_envelope_slot_size, - help=f"Envelope slot size in SUIT storage. Default: 0x{ImageCreator.default_envelope_slot_size:08X}", - ) - cmd_image_boot.add_argument( - "--envelope-slot-count", - required=False, - type=lambda x: int(x, 0), - default=ImageCreator.default_envelope_slot_count, - help=f"Max number of envelope slots in SUIT storage. Default: {ImageCreator.default_envelope_slot_count}", - ) - cmd_image_boot.add_argument( - "--dfu-max-caches", - required=False, - type=int, - default=ImageCreator.default_dfu_max_caches, - help=f"Max number of DFU caches. Default: {ImageCreator.default_dfu_max_caches}", + default=None, + help="Path to KConfig file", ) cmd_image_update.add_argument("--input-file", required=True, help="Input SUIT file; an envelope") @@ -137,51 +116,14 @@ class ManifestDomain(Enum): class EnvelopeStorage: - """Class generating SUIT storage binary in legacy format.""" + """Base class for generating SUIT storage binary.""" ENVELOPE_SLOT_VERSION = 1 ENVELOPE_SLOT_VERSION_KEY = 0 ENVELOPE_SLOT_CLASS_ID_OFFSET_KEY = 1 ENVELOPE_SLOT_ENVELOPE_BSTR_KEY = 2 - _LAYOUT = [ - { - "role": ManifestRole.APP_ROOT, - "offset": 2048 * 0, - "size": 2048, - "domain": ManifestDomain.APPLICATION, - }, - { - "role": ManifestRole.APP_LOCAL_1, - "offset": 2048 * 1, - "size": 2048, - "domain": ManifestDomain.APPLICATION, - }, - { - "role": ManifestRole.RAD_LOCAL_1, - "offset": 2048 * 2, - "size": 2048, - "domain": ManifestDomain.RADIO, - }, - { - "role": ManifestRole.SEC_TOP, - "offset": 2048 * 3, - "size": 2048, - "domain": ManifestDomain.SECURE, - }, - { - "role": ManifestRole.SEC_SDFW, - "offset": 2048 * 4, - "size": 2048, - "domain": ManifestDomain.SECURE, - }, - { - "role": ManifestRole.SEC_SYSCTRL, - "offset": 2048 * 5, - "size": 2048, - "domain": ManifestDomain.SECURE, - }, - ] + _LAYOUT = [] # Default manifest role assignments _CLASS_ROLE_ASSIGNMENTS = [ @@ -195,11 +137,21 @@ class EnvelopeStorage: "class_name": "nRF54H20_sample_app", "role": ManifestRole.APP_LOCAL_1, }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF54H20_sample_app_recovery", + "role": ManifestRole.APP_RECOVERY, + }, { "vendor_name": "nordicsemi.com", "class_name": "nRF54H20_sample_rad", "role": ManifestRole.RAD_LOCAL_1, }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF54H20_sample_rad_recovery", + "role": ManifestRole.RAD_RECOVERY, + }, { "vendor_name": "nordicsemi.com", "class_name": "nRF54H20_nordic_top", @@ -217,9 +169,9 @@ class EnvelopeStorage: }, ] - def __init__(self, base_address: int, load_defaults=True): + def __init__(self, base_address: int, load_defaults=True, kconfig=None): """Create object generating binary SUIT storage.""" - self._assignments = [] + self._assignments = {} self._base_address = base_address self._envelopes = {} @@ -227,28 +179,52 @@ def __init__(self, base_address: int, load_defaults=True): for entry in self._CLASS_ROLE_ASSIGNMENTS: self.assign_role(entry["vendor_name"], entry["class_name"], entry["role"]) + if kconfig: + for entry in self._get_role_assignments_from_kconfig(kconfig): + self.assign_role(entry["vendor_name"], entry["class_name"], entry["role"]) + + @staticmethod + def _get_role_assignments_from_kconfig(kconfig: str) -> list: + config = BuildConfiguration(input_file=kconfig) + kconfig_assignments = [] + for key, value in config.items(): + if re_value := re.match(r"^SB_CONFIG_SUIT_MPI_(?P[A-Z1-9_]+)_VENDOR_NAME$", key): + manifest = re_value.group("manifest") + # ensure that the same combination of vid/cid has not been set for different role + for item in kconfig_assignments: + if ( + item["vendor_name"] == config[f"SB_CONFIG_SUIT_MPI_{manifest}_VENDOR_NAME"] + and item["class_name"] == config[f"SB_CONFIG_SUIT_MPI_{manifest}_CLASS_NAME"] + ): + raise GeneratorError( + "Duplicate vid/cid combination for different roles detected in the KConfig file." + ) + data = { + "vendor_name": config[f"SB_CONFIG_SUIT_MPI_{manifest}_VENDOR_NAME"], + "class_name": config[f"SB_CONFIG_SUIT_MPI_{manifest}_CLASS_NAME"], + "role": ManifestRole[f"APP_{manifest}" if manifest == "ROOT" else manifest], + } + kconfig_assignments.append(data) + return kconfig_assignments + def assign_role(self, vendor_name: str, class_name: str, role: ManifestRole): """Assign role to envelope, identified by vendor and class name.""" vid = uuid.uuid5(uuid.NAMESPACE_DNS, vendor_name) - self._assignments.append( - { - "vendor_id": vid.bytes, - "class_id": uuid.uuid5(vid, class_name).bytes, - "role": role, - } - ) + cid = uuid.uuid5(vid, class_name) + self._assignments[cid.hex] = { + "vendor_id": vid.bytes, + "class_id": cid.bytes, + "role": role, + } - def _find_class(self, role: ManifestRole) -> bytes: - for entry in self._assignments: + def _find_class(self, role: ManifestRole) -> bytes | None: + for entry in self._assignments.values(): if entry["role"] == role: return entry["class_id"] return None - def _find_role(self, class_id: bytes) -> ManifestRole: - for entry in self._assignments: - if entry["class_id"].hex() == class_id.hex(): - return entry["role"] - return None + def _find_role(self, class_id: bytes) -> ManifestRole | None: + return self._assignments[class_id.hex()]["role"] if class_id.hex() in self._assignments else None def _find_slot(self, class_id: bytes) -> (int, int): role = self._find_role(class_id) @@ -297,7 +273,9 @@ def add_envelope(self, envelope: SuitEnvelope): raise GeneratorError(f"Unable to find slot for manifest with class id {class_id.hex()}") if slot[1] < len(envelope_bytes): - raise GeneratorError(f"Unable to fit manifest with class id ({len(class_id.hex())} > {slot})") + raise GeneratorError( + f"Unable to fit manifest with class id {class_id.hex()} ({len(envelope_bytes)} > {slot[1]})" + ) if role in self._envelopes.keys(): raise GeneratorError(f"Manifest with role {role} already added") @@ -325,7 +303,7 @@ def as_intelhex(self, storage_domain: ManifestDomain = None): envelope_bytes = self._envelopes[role].ljust(max_size, b"\xFF") envelope_count += 1 else: - envelope_bytes = b"\xFF" * max_size + continue envelope_hex = IntelHex() envelope_hex.frombytes(envelope_bytes, self._base_address + entry["offset"]) @@ -411,14 +389,128 @@ class EnvelopeStorageNrf54h20(EnvelopeStorage): ] +class EnvelopeStorageNrf9280(EnvelopeStorage): + """Class generating SUIT storage binary in upcoming format.""" + + _CLASS_ROLE_ASSIGNMENTS = [ + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sample_root", + "role": ManifestRole.APP_ROOT, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sample_app", + "role": ManifestRole.APP_LOCAL_1, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sample_app_recovery", + "role": ManifestRole.APP_RECOVERY, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sample_rad", + "role": ManifestRole.RAD_LOCAL_1, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sample_rad_recovery", + "role": ManifestRole.RAD_RECOVERY, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_nordic_top", + "role": ManifestRole.SEC_TOP, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sec", + "role": ManifestRole.SEC_SDFW, + }, + { + "vendor_name": "nordicsemi.com", + "class_name": "nRF9280_sys", + "role": ManifestRole.SEC_SYSCTRL, + }, + ] + + _LAYOUT = [ + { + "role": ManifestRole.SEC_TOP, + "offset": 4096, + "size": 1536, + "domain": ManifestDomain.SECURE, + }, + { + "role": ManifestRole.SEC_SDFW, + "offset": 2048, + "size": 1024, + "domain": ManifestDomain.SECURE, + }, + { + "role": ManifestRole.SEC_SYSCTRL, + "offset": 3072, + "size": 1024, + "domain": ManifestDomain.SECURE, + }, + { + "role": ManifestRole.RAD_RECOVERY, + "offset": 8192 + 1024 * 1, + "size": 1024, + "domain": ManifestDomain.RADIO, + }, + { + "role": ManifestRole.RAD_LOCAL_1, + "offset": 8192 + 1024 * 2, + "size": 1024, + "domain": ManifestDomain.RADIO, + }, + { + "role": ManifestRole.RAD_LOCAL_2, + "offset": 8192 + 1024 * 3, + "size": 1024, + "domain": ManifestDomain.RADIO, + }, + { + "role": ManifestRole.APP_ROOT, + "offset": 12288 + 1024 * 1, + "size": 2048, + "domain": ManifestDomain.APPLICATION, + }, + { + "role": ManifestRole.APP_RECOVERY, + "offset": 12288 + 1024 * 3, + "size": 2048, + "domain": ManifestDomain.APPLICATION, + }, + { + "role": ManifestRole.APP_LOCAL_1, + "offset": 12288 + 1024 * 5, + "size": 1024, + "domain": ManifestDomain.APPLICATION, + }, + { + "role": ManifestRole.APP_LOCAL_2, + "offset": 12288 + 1024 * 6, + "size": 1024, + "domain": ManifestDomain.APPLICATION, + }, + { + "role": ManifestRole.APP_LOCAL_3, + "offset": 12288 + 1024 * 7, + "size": 1024, + "domain": ManifestDomain.APPLICATION, + }, + ] + + class ImageCreator: """Helper class for extracting data from SUIT envelope and creating hex files.""" - default_update_candidate_info_address = 0x0E1E9340 - default_envelope_address = 0x0E1E7000 - default_envelope_slot_size = 2048 - default_envelope_slot_count = 8 - default_dfu_partition_address = 0x0E155000 + default_update_candidate_info_address = 0x0E1EF340 + default_storage_address = 0x0E1ED000 + default_dfu_partition_address = 0x0E100000 default_dfu_max_caches = 6 UPDATE_MAGIC_VALUE_AVAILABLE = 0x55AA55AA @@ -435,21 +527,6 @@ def _prepare_suit_storage_struct_format(dfu_max_caches: int) -> str: # (void*, size_t) for each cache return "<" + "IIII" + dfu_max_caches * "II" - @staticmethod - def _prepare_update_candidate_info_for_boot(dfu_max_caches: int) -> bytes: - uci = struct.Struct(ImageCreator._prepare_suit_storage_struct_format(dfu_max_caches)) - - all_cache_values = dfu_max_caches * [0, 0] # address, size - struct_values = [ - ImageCreator.UPDATE_MAGIC_VALUE_AVAILABLE, # Update candidate info magic - 0, # Nb of memory regions - 0, # SUIT envelope address - 0, # SUIT envelope size - *all_cache_values, # Values for all the caches - ] - - return uci.pack(*struct_values) - @staticmethod def _prepare_update_candidate_info_for_update( dfu_partition_address: int, candidate_size: int, dfu_max_caches: int @@ -472,69 +549,39 @@ def _create_single_domain_storage_file_for_boot( storage: EnvelopeStorage, domain: ManifestDomain, dir_name: str, - additional_hex, ) -> None: combined_hex = IntelHex() - if additional_hex is not None: - combined_hex = IntelHex(additional_hex) envelopes_hex = storage.as_intelhex(domain) if envelopes_hex is not None: combined_hex.merge(envelopes_hex) - combined_hex.write_hex_file(dir_name + "/storage_" + domain.name.lower() + ".hex") - - def _create_suit_storage_file_for_boot_legacy( - envelopes: list[SuitEnvelope], - update_candidate_info_address: int, - installed_envelope_address: int, - envelope_slot_size: int, - envelope_slot_count: int, - dir_name: str, - dfu_max_caches: int, - ) -> None: - # Update candidate info - # In the boot path it is used to inform no update candidate is pending. - uci_hex = IntelHex() - uci_hex.frombytes( - ImageCreator._prepare_update_candidate_info_for_boot(dfu_max_caches), update_candidate_info_address - ) - - combined_hex = IntelHex(uci_hex) - - storage = EnvelopeStorageNrf54h20(installed_envelope_address) - for envelope in envelopes: - storage.add_envelope(envelope) - combined_hex.merge(storage.as_intelhex()) - - combined_hex.write_hex_file(dir_name + "/storage.hex") + combined_hex.write_hex_file(dir_name + "/suit_installed_envelopes_" + domain.name.lower() + "_merged.hex") @staticmethod def _create_suit_storage_files_for_boot( envelopes: list[SuitEnvelope], - update_candidate_info_address: int, - installed_envelope_address: int, - envelope_slot_size: int, - envelope_slot_count: int, + storage_address: int, dir_name: str, - dfu_max_caches: int, + config_file: str, + soc: str = "nrf54h20", ) -> None: - # Update candidate info - # In the boot path it is used to inform no update candidate is pending. - uci_hex = IntelHex() - uci_hex.frombytes( - ImageCreator._prepare_update_candidate_info_for_boot(dfu_max_caches), update_candidate_info_address - ) + if soc == "nrf54h20": + storage = EnvelopeStorageNrf54h20(storage_address, kconfig=config_file) + elif soc == "nrf9280": + storage = EnvelopeStorageNrf9280(storage_address, kconfig=config_file) + else: + raise GeneratorError(f"Unknown soc: {soc}") - storage = EnvelopeStorageNrf54h20(installed_envelope_address) for envelope in envelopes: storage.add_envelope(envelope) for domain in ManifestDomain: - additional_hex = None - if domain == ManifestDomain.APPLICATION: - additional_hex = uci_hex - ImageCreator._create_single_domain_storage_file_for_boot(storage, domain, dir_name, additional_hex) + ImageCreator._create_single_domain_storage_file_for_boot( + storage, + domain, + dir_name, + ) @staticmethod def _create_suit_storage_file_for_update( @@ -563,21 +610,17 @@ def _create_dfu_partition_hex_file(input_file: str, dfu_partition_output_file: s def create_files_for_boot( input_files: list[str], storage_output_directory: str, - update_candidate_info_address: int, - envelope_address: int, - envelope_slot_size: int, - envelope_slot_count: int, - dfu_max_caches: int, + storage_address: int, + config_file: str | None, + soc: str = "nrf54h20", ) -> None: """Create storage and payload hex files allowing boot execution path. - :param input_file: file path to SUIT envelope + :param input_files: file paths to SUIT envelope :param storage_output_directory: directory path to store hex files with SUIT storage contents - :param update_candidate_info_address: address of SUIT storage update candidate info - :param envelope_address: address of installed envelope in SUIT storage - :param envelope_slot_size: number of bytes, reserved to store a single envelope, - :param envelope_slot_count: number of envelope slots in SUIT storage, - :param dfu_max_caches: maximum number of caches, allowed to be passed inside update candidate info, + :param storage_address: address of SUIT storage + :param config_file: path to KConfig file containing MPI settings + :param soc: soc in use, nrf54h20 or nrf9280 """ try: envelopes = [] @@ -588,23 +631,12 @@ def create_files_for_boot( envelope.sever() envelopes.append(envelope) - ImageCreator._create_suit_storage_file_for_boot_legacy( - envelopes, - update_candidate_info_address, - envelope_address, - envelope_slot_size, - envelope_slot_count, - storage_output_directory, - dfu_max_caches, - ) ImageCreator._create_suit_storage_files_for_boot( envelopes, - update_candidate_info_address, - envelope_address, - envelope_slot_size, - envelope_slot_count, + storage_address, storage_output_directory, - dfu_max_caches, + config_file, + soc, ) except FileNotFoundError as error: raise GeneratorError(error) @@ -627,6 +659,7 @@ def create_files_for_update( :param dfu_partition_output_file: file path to hex file with DFU partition contents (the SUIT envelope) :param update_candidate_info_address: address of SUIT storage update candidate info :param dfu_partition_address: address of partition where DFU update candidate is stored + :param dfu_max_caches: maximum number of caches """ try: ImageCreator._create_suit_storage_file_for_update( @@ -647,22 +680,22 @@ def main(**kwargs) -> None: :Keyword Arguments: * **image** - subcommand to be executed * **input_file** - file path to SUIT envelope - * **storage_output_file** - file path to hex file with SUIT storage contents - for "update" command * **storage_output_directory** - directory path to store hex files with storage contents - for "boot" command - * **update_candidate_info_address** - address of SUIT storage update candidate info - * **envelope_address** - address of installed envelope in SUIT storage - * **dfu_partition_output_file** - file path to hex file with DFU partition contents (the SUIT envelope) - * **dfu_partition_address** - address of partition where DFU update candidate is stored + * **storage_address** - address of SUIT storage - for "boot" command + * **storage_output_file** - file path to hex file with SUIT storage contents - for "update" command + * **update_candidate_info_address** - address of SUIT storage update candidate info - for "update" command + * **dfu_partition_output_file** - file path to hex file with DFU partition contents (the SUIT envelope), + for "update" command + * **dfu_partition_address** - address of partition where DFU update candidate is stored - for "update" command + * **dfu_max_caches** - maximum number of caches, allowed to be passed inside update candidate info, + for "update" command """ if kwargs["image"] == ImageCreator.IMAGE_CMD_BOOT: ImageCreator.create_files_for_boot( kwargs["input_file"], kwargs["storage_output_directory"], - kwargs["update_candidate_info_address"], - kwargs["envelope_address"], - kwargs["envelope_slot_size"], - kwargs["envelope_slot_count"], - kwargs["dfu_max_caches"], + kwargs["storage_address"], + kwargs["config_file"], ) elif kwargs["image"] == ImageCreator.IMAGE_CMD_UPDATE: ImageCreator.create_files_for_update( diff --git a/suit_generator/cmd_mpi.py b/suit_generator/cmd_mpi.py index f3e4c0a5..d51610ee 100644 --- a/suit_generator/cmd_mpi.py +++ b/suit_generator/cmd_mpi.py @@ -90,7 +90,7 @@ def add_arguments(parser): class MpiGenerator: - """Class geenrating SUIT Manifest Provisioning Information.""" + """Class generating SUIT Manifest Provisioning Information.""" BYTE_ORDER = "little" diff --git a/suit_generator/cmd_payload_extract.py b/suit_generator/cmd_payload_extract.py new file mode 100644 index 00000000..6b9fa9b9 --- /dev/null +++ b/suit_generator/cmd_payload_extract.py @@ -0,0 +1,67 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""CMD_PAYLOAD_EXTRACT CLI command entry point.""" + +import cbor2 +import logging + +log = logging.getLogger(__name__) + +PAYLOAD_EXTRACT_CMD = "payload_extract" + + +def add_arguments(parser): + """Add additional arguments to the passed parser.""" + cmd_payload_extract_arg_parser = parser.add_parser(PAYLOAD_EXTRACT_CMD, help="Create raw cache structure.") + + cmd_payload_extract_arg_parser.add_argument("--input-envelope", required=True, help="Input envelope file path.") + cmd_payload_extract_arg_parser.add_argument("--output-envelope", required=True, help="Output envelope file path.") + cmd_payload_extract_arg_parser.add_argument( + "--payload-name", required=True, help="Name of the integrated payload to extract." + ) + cmd_payload_extract_arg_parser.add_argument( + "--output-payload-file", + required=False, + help="Output payload file path to store the extracted payload." + + "If not provided, the payload will not be stored to a file.", + ) + + cmd_payload_extract_arg_parser.add_argument( + "--payload-replace-path", + help="Path to the integrated payload to replace the extracted payload with." + + "If not provided, the payload will be removed from the envelope.", + ) + + +def main( + input_envelope: str, output_envelope: str, payload_name: str, output_payload_file: str, payload_replace_path: str +) -> None: + """Extract an integrated payload from a SUIT envelope. + + :param input_envelope: input envelope file path + :param output_envelope: output envelope file path + :param payload_name: name of the integrated payload to extract + :param output_payload_file: output file path to store the extracted payload + None if the payload should not be stored to a file + :param payload_replace_path: Path to the integrated payload to replace the extracted payload with. + None if the payload should be removed from the envelope. + """ + with open(input_envelope, "rb") as fh: + envelope = cbor2.load(fh) + extracted_payload = envelope.value.pop(payload_name, None) + + if extracted_payload is None: + log.log(logging.ERROR, 'Payload "%s" not found in envelope', payload_name) + + if payload_replace_path is not None: + with open(payload_replace_path, "rb") as fh: + envelope.value[payload_name] = fh.read() + + with open(output_envelope, "wb") as fh: + cbor2.dump(envelope, fh) + if output_payload_file is not None: + with open(output_payload_file, "wb") as fh: + fh.write(extracted_payload) diff --git a/suit_generator/logger.py b/suit_generator/logger.py index b692675f..1bb67b97 100644 --- a/suit_generator/logger.py +++ b/suit_generator/logger.py @@ -4,13 +4,21 @@ # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause # """Logger helper methods.""" +from __future__ import annotations + import functools import inspect import logging +import logging.config + +from pathlib import Path logger = logging.getLogger(__name__) +DEFAULT_LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" +DEFAULT_LOG_FILE_PATH: Path = "suit-generator.log" + def log_call(func): """Decorate function or method if call shall be logged.""" @@ -23,8 +31,8 @@ def inner_func(*args, **kwargs): info = inspect.getframeinfo(frame) logger.debug(f"{info.filename}:{info.function}:{info.lineno}:{func.__name__}({args=},{kwargs=})") return func(*args, **kwargs) - except Exception as e: - logger.warning(f"{info.filename}:{info.function}:{info.lineno}:{func.__name__}({args=},{kwargs=}):\n{e}") + except Exception: + logger.debug(f"Unable to parse data: {args=},{kwargs=}") raise return inner_func diff --git a/suit_generator/logging.yaml b/suit_generator/logging.yaml index d7ef563d..6f06bb55 100644 --- a/suit_generator/logging.yaml +++ b/suit_generator/logging.yaml @@ -18,15 +18,10 @@ handlers: backupCount: 10 # max 10 files maxBytes: 10485760 # max ten mega bytes (1024*1024*10) loggers: - suit_generator.suit.types.common: + suit_generator: level: DEBUG - handlers: [file] - suit_generator.logger: - level: DEBUG - handlers: [file] ncs: level: DEBUG - handler: [file] -root: - level: DEBUG - handlers: [file, console] \ No newline at end of file + root: + level: DEBUG + handlers: [file, console] diff --git a/suit_generator/suit/envelope.py b/suit_generator/suit/envelope.py index d7601abb..c446bd89 100644 --- a/suit_generator/suit/envelope.py +++ b/suit_generator/suit/envelope.py @@ -14,7 +14,7 @@ from suit_generator.suit.payloads import SuitIntegratedPayloadMap from suit_generator.suit.types.common import SuitKeyValue, SuitTag, Tag, Metadata, SuitBstr, cbstr -from suit_generator.suit.authentication import SuitDelegationChain, SuitAuthentication, SuitHash +from suit_generator.suit.security import SuitDelegationChain, SuitAuthentication, SuitHash from suit_generator.suit.manifest import SuitManifest, SuitCommandSequence, SuitTextMap from suit_generator.suit.types.keys import ( suit_manifest, @@ -24,6 +24,7 @@ suit_candidate_verification, suit_payload_fetch, suit_install, + suit_install_legacy, suit_text, suit_integrated_payloads, suit_integrated_dependencies, @@ -42,6 +43,7 @@ class SuitEnvelopeSimplified(SuitKeyValue): suit_payload_fetch: SuitBstr, suit_candidate_verification: SuitBstr, suit_install: SuitBstr, + suit_install_legacy: SuitBstr, suit_text: SuitBstr, suit_integrated_payloads: SuitIntegratedPayloadMap, suit_integrated_dependencies: SuitIntegratedPayloadMap, @@ -62,6 +64,7 @@ class SuitEnvelope(SuitKeyValue): suit_payload_fetch: cbstr(SuitCommandSequence), suit_candidate_verification: cbstr(SuitCommandSequence), suit_install: cbstr(SuitCommandSequence), + suit_install_legacy: cbstr(SuitCommandSequence), suit_text: cbstr(SuitTextMap), suit_integrated_payloads: SuitIntegratedPayloadMap, suit_integrated_dependencies: SuitIntegratedPayloadMap, @@ -78,17 +81,24 @@ def update_digest(self): alg = ( self.value.value.value[suit_authentication_wrapper].SuitAuthentication[0].SuitDigest.SuitDigestRaw[0].value ) - manifest = self.value.value.value[suit_manifest].to_cbor() - hash_func = SuitHash(alg) - new_digest = binascii.a2b_hex(hash_func.hash(manifest)) self.value.value.value[suit_authentication_wrapper].SuitAuthentication[0].SuitDigest.SuitDigestRaw[ 1 - ].SuitDigestBytes = new_digest + ].SuitDigestBytes = self.get_manifest_digest(alg) def get_digest(self): """Return digest from parsed envelope.""" return self.value.value.value[suit_authentication_wrapper].SuitAuthentication[0].SuitDigest + def get_manifest(self): + """Return manifest from parsed envelope.""" + return self.value.value.value[suit_manifest] + + def get_manifest_digest(self, alg): + """Return digest from parsed envelope.""" + manifest = self.get_manifest().to_cbor() + hash_func = SuitHash(alg) + return binascii.a2b_hex(hash_func.hash(manifest)) + def update_severable_digests(self): """Update digest in the envelope for severed elements.""" severable_elements = [ @@ -97,6 +107,7 @@ def update_severable_digests(self): suit_payload_fetch, suit_candidate_verification, suit_install, + suit_install_legacy, ] for severable_element in severable_elements: if severable_element in self.SuitEnvelopeTagged.value.SuitEnvelope[suit_manifest].SuitManifest and hasattr( diff --git a/suit_generator/suit/manifest.py b/suit_generator/suit/manifest.py index 38f3dcf0..61868c63 100644 --- a/suit_generator/suit/manifest.py +++ b/suit_generator/suit/manifest.py @@ -7,8 +7,10 @@ Code inspired by/based on https://github.com/tomchy/suit-composer. """ +from enum import Enum import uuid from os.path import getsize +from typing import List, Union from suit_generator.suit.types.common import ( SuitInt, @@ -28,7 +30,7 @@ SuitBchar, cbstr, ) -from suit_generator.suit.authentication import SuitDigest +from suit_generator.suit.security import SuitDigest, SuitEncryptionInfo from suit_generator.suit.types.keys import ( suit_parameter_vendor_identifier, suit_parameter_class_identifier, @@ -38,13 +40,12 @@ suit_parameter_soft_failure, suit_parameter_image_size, suit_parameter_content, + suit_parameter_encryption_info, suit_parameter_uri, suit_parameter_source_component, suit_parameter_invoke_args, suit_parameter_device_identifier, suit_parameter_version, - suit_parameter_version_comparison_type, - suit_parameter_version_comparison_value, suit_directive_set_component_index, suit_directive_try_each, suit_directive_write, @@ -91,6 +92,7 @@ suit_invoke, suit_payload_fetch, suit_install, + suit_install_legacy, suit_text_manifest_description, suit_text_manifest_json_source, suit_text_manifest_yaml_source, @@ -105,7 +107,10 @@ suit_candidate_verification, suit_uninstall, suit_text, + suit_synchronous_invoke, + suit_timeout, ) +from suit_generator.logger import log_call class SuitIndex(SuitUnion): @@ -204,41 +209,81 @@ def from_obj(cls, obj: dict) -> SuitUint: binary_data = SuitEnvelopeTagged.return_processed_binary_data(obj["envelope"]) return super().from_obj(len(binary_data)) + elif "file_direct" in obj.keys(): + img_size = 0 + with open(obj["file_direct"], "r") as file: + img_size = int(file.read()) + return super().from_obj(img_size) else: raise ValueError(f"Unable to parse image size: {obj}") -class SuitConditionVersionComparisonType(SuitEnum): - """Representation of available SUIT condition version comparison types.""" - - _metadata = Metadata( - children=[ - suit_condition_version_comparison_greater, - suit_condition_version_comparison_greater_equal, - suit_condition_version_comparison_equal, - suit_condition_version_comparison_lesser_equal, - suit_condition_version_comparison_lesser, - ] - ) - - class SuitComponentVersion(SuitList): """Representation of a single component version.""" _metadata = Metadata(children=[SuitInt]) + @staticmethod + def _convert_version_part(part): + class PrereleaseType(Enum): + alpha = -3 + beta = -2 + rc = -1 + + if isinstance(part, str): + if part.isnumeric(): + return int(part) + else: + try: + prerelease = getattr(PrereleaseType, part) + return prerelease.value + except AttributeError: + raise ValueError(f"Unsupported prerelease type: {part}") + elif isinstance(part, int): + return part + else: + raise ValueError(f"Unsupported version part: {part}") + + @classmethod + @log_call + def from_obj(cls, obj: Union[List[int], str]) -> SuitList: + """Restore SUIT representation from passed object.""" + if isinstance(obj, str): + obj = [cls._convert_version_part(part) for part in obj.replace("-", ".").split(".")] + return super().from_obj(obj) + -class SuitParameterVersion(SuitKeyValue): +class SuitParameterVersion(SuitKeyValueTuple): """Representation of SUIT version parameter.""" _metadata = Metadata( map={ - suit_parameter_version_comparison_type: SuitConditionVersionComparisonType, - suit_parameter_version_comparison_value: SuitComponentVersion, + suit_condition_version_comparison_greater: SuitComponentVersion, + suit_condition_version_comparison_greater_equal: SuitComponentVersion, + suit_condition_version_comparison_equal: SuitComponentVersion, + suit_condition_version_comparison_lesser_equal: SuitComponentVersion, + suit_condition_version_comparison_lesser: SuitComponentVersion, } ) +class SuitParameterInvokeArgs(SuitKeyValue): + """Representation of SUIT version parameter.""" + + _metadata = Metadata( + map={ + suit_synchronous_invoke: SuitBool, + suit_timeout: SuitUint, + } + ) + + +class SuitParameterContent(SuitUnion): + """Abstract element to define possible sub-elements.""" + + _metadata = Metadata(children=[cbstr(SuitUint), SuitBstr]) + + class SuitParameters(SuitKeyValue): """Representation of SUIT parameters.""" @@ -251,12 +296,13 @@ class SuitParameters(SuitKeyValue): suit_parameter_strict_order: SuitBool, suit_parameter_soft_failure: SuitBool, suit_parameter_image_size: SuitImageSize, - suit_parameter_content: SuitBstr, + suit_parameter_content: SuitParameterContent, + suit_parameter_encryption_info: SuitEncryptionInfo, suit_parameter_uri: SuitTstr, suit_parameter_source_component: SuitUint, - suit_parameter_invoke_args: SuitBstr, + suit_parameter_invoke_args: cbstr(SuitParameterInvokeArgs), suit_parameter_device_identifier: SuitUUID, - suit_parameter_version: SuitParameterVersion, + suit_parameter_version: cbstr(SuitParameterVersion), } ) @@ -442,6 +488,7 @@ class SuitManifest(SuitKeyValue): suit_invoke: cbstr(SuitCommandSequence), suit_payload_fetch: SuitSeverableCommandSequence, suit_install: SuitSeverableCommandSequence, + suit_install_legacy: SuitSeverableCommandSequence, suit_text: SuitSeverableText, suit_dependency_resolution: SuitSeverableCommandSequence, suit_candidate_verification: SuitSeverableCommandSequence, diff --git a/suit_generator/suit/authentication.py b/suit_generator/suit/security.py similarity index 59% rename from suit_generator/suit/authentication.py rename to suit_generator/suit/security.py index 417596ad..dd8687bf 100644 --- a/suit_generator/suit/authentication.py +++ b/suit_generator/suit/security.py @@ -19,6 +19,7 @@ SuitKeyValue, SuitList, SuitBstr, + SuitEmptyBstr, SuitTag, Tag, cbstr, @@ -29,6 +30,7 @@ from suit_generator.suit.types.keys import ( suit_cose_algorithm_id, suit_cose_key_id, + suit_cose_iv, suit_issuer, suit_subject, suit_audience, @@ -45,6 +47,13 @@ cose_alg_es_384, cose_alg_es_521, cose_alg_eddsa, + cose_alg_aes_gcm_128, + cose_alg_aes_gcm_192, + cose_alg_aes_gcm_256, + cose_alg_a256kw, + cose_alg_a192kw, + cose_alg_a128kw, + cose_alg_direct, suit_digest_algorithm_id, suit_digest_bytes, ) @@ -147,9 +156,12 @@ def from_obj(cls, obj: dict) -> SuitDigestRaw: sub_envelope = SuitEnvelopeTagged.from_cbor(fh.read()) sub_envelope.update_severable_digests() sub_envelope.update_digest() - obj[suit_digest_bytes.name] = sub_envelope.get_digest().SuitDigestRaw[1].SuitDigestBytes.hex() + obj[suit_digest_bytes.name] = sub_envelope.get_manifest_digest(obj[suit_digest_algorithm_id.name]).hex() elif "raw" in digest_dict.keys(): obj[suit_digest_bytes.name] = digest_dict["raw"] + elif "file_direct" in digest_dict.keys(): + with open(digest_dict["file_direct"], "rb") as fd: + obj[suit_digest_bytes.name] = fd.read().hex() else: raise ValueError(f"Unable to calculate digest from: {digest_dict}") @@ -162,10 +174,35 @@ class SuitDigest(SuitUnion): _metadata = Metadata(children=[SuitDigestRaw, SuitDigestExt]) -class SuitcoseSignAlg(SuitEnum): +class SuitcoseAlg(SuitEnum): """Representation of SUIT COSE sign algorithm.""" - _metadata = Metadata(children=[cose_alg_es_256, cose_alg_es_384, cose_alg_es_521, cose_alg_eddsa]) + _metadata = Metadata( + children=[ + cose_alg_es_256, + cose_alg_es_384, + cose_alg_es_521, + cose_alg_eddsa, + cose_alg_aes_gcm_128, + cose_alg_aes_gcm_192, + cose_alg_aes_gcm_256, + cose_alg_a256kw, + cose_alg_a192kw, + cose_alg_a128kw, + cose_alg_direct, + ] + ) + + +class SuitcoseKeyId(SuitUnion): + """Representation of a KEY ID item.""" + + _metadata = Metadata( + children=[ + cbstr(SuitInt), + SuitBstr, + ] + ) class SuitHeaderMap(SuitKeyValue): @@ -173,12 +210,37 @@ class SuitHeaderMap(SuitKeyValue): _metadata = Metadata( map={ - suit_cose_algorithm_id: SuitcoseSignAlg, - suit_cose_key_id: cbstr(SuitInt), + suit_cose_algorithm_id: SuitcoseAlg, + suit_cose_key_id: SuitcoseKeyId, + suit_cose_iv: SuitHex, } ) +class SuitHeaderMapOptional(SuitUnion): + """Representation of COSE_Encrypt_ciphertext item.""" + + _metadata = Metadata( + children=[ + SuitHeaderMap, + SuitEmptyBstr, + ] + ) + + @classmethod + def from_obj(cls, obj) -> SuitUnion: + """Restore SUIT representation from passed object.""" + value = None + if isinstance(obj, dict): + value = SuitEmptyBstr.from_obj("") if obj == {} else SuitHeaderMap.from_obj(obj) + elif obj == "" or obj == b"": + value = SuitEmptyBstr.from_obj("") + else: + raise ValueError(f"Expected dict empty string or empty sequence of bytes received: {obj}") + + return cls(value) + + class SuitHeaderData(SuitUnion): """Abstract element to define possible sub-elements.""" @@ -271,3 +333,114 @@ class SuitDelegationChain(SuitList): """Representation of SUIT delegation chain.""" _metadata = Metadata(children=[SuitDelegation]) + + +# Encryption + + +class SuitCiphertextBytes(SuitHex): + """Representation of SUIT ciphertext bytes.""" + + pass + + +class CoseEncryptCiphertext(SuitUnion): + """Representation of COSE_Encrypt_ciphertext item.""" + + _metadata = Metadata( + children=[ + SuitNull, + SuitCiphertextBytes, + ] + ) + + +class CoseRecipient(SuitTupleNamed): + """Representation of COSE_Recipient item.""" + + _metadata = Metadata( + map={ + "protected": cbstr(SuitHeaderMapOptional), + "unprotected": SuitHeaderData, + "ciphertext": CoseEncryptCiphertext, + "recipients*": SuitList, + } + ) + + +class CoseRecipientList(SuitList): + """Representation of a list of COSE_Recipient items.""" + + _metadata = Metadata(children=[CoseRecipient]) + + +# Fix cyclic dependencies between types +CoseRecipient._metadata.map["recipients*"] = CoseRecipientList + + +class CoseEncrypt(SuitTupleNamed): + """Representation of COSE_Encrypt item.""" + + _metadata = Metadata( + map={ + "protected": cbstr(SuitHeaderMap), + "unprotected": SuitHeaderData, + "ciphertext": CoseEncryptCiphertext, + "recipients": CoseRecipientList, + } + ) + + +class CoseEncryptTagged(SuitTag): + """Representation of COSE_Encrypt_Tagged item.""" + + _metadata = Metadata(children=[CoseEncrypt], tag=Tag(96, "CoseEncryptTagged")) + + +class CoseEncStructure(SuitTupleNamed): + """Representation of COSE Enc_structure.""" + + _metadata = Metadata( + map={ + "context": SuitTstr, + "protected": cbstr(SuitHeaderMapOptional), + "external_aad": SuitBstr, + } + ) + + +class SuitEncryptionInfoExt(SuitBstr): + """Representation of SUIT encryption info ext.""" + + @classmethod + def to_obj(self) -> dict: + """Dump SUIT representation to object.""" + raise ValueError("Encryption info should be expanded to full structure by to_obj method") + + @classmethod + def from_obj(cls, obj: dict) -> SuitBstr: + """Restore SUIT representation from passed object.""" + if not isinstance(obj, dict): + raise ValueError(f"Expected dict, received: {obj}") + enc_info_bytes = b"" + if "raw" in obj.keys(): + enc_info_bytes = bytes.fromhex(obj["raw"]) # TODO: check this + elif "file" in obj.keys(): + with open(obj["file"], "rb") as fd: + enc_info_bytes = fd.read() + else: + raise ValueError(f"Unable to parse encryption info: {obj}") + # the value in enc_info_bytes is already bstr wrapped - we have + # to deserialize it, so that the SuitBstr to_cbor method returns the correct value + return super().from_cbor(super().deserialize_cbor(enc_info_bytes)) + + @classmethod + def from_cbor(self) -> dict: + """Restore SUIT representation from passed CBOR string.""" + raise ValueError("Encryption info should be created as serialized CoseEncryptTagged object from cbor") + + +class SuitEncryptionInfo(SuitUnion): + """Representation of SUIT digest.""" + + _metadata = Metadata(children=[cbstr(CoseEncryptTagged), SuitEncryptionInfoExt]) diff --git a/suit_generator/suit/types/common.py b/suit_generator/suit/types/common.py index 114c1bdc..6b7ae807 100644 --- a/suit_generator/suit/types/common.py +++ b/suit_generator/suit/types/common.py @@ -134,6 +134,8 @@ def validate_cbor(cbstr: bytes) -> None: does not contain data which will cause that cbor2 library will request huge amount of memory. """ requested_memory_len = None + if len(cbstr) < 1: + raise ValueError("The cbstr parsed object is empty") cbor_item_type = cbstr[0] >> 5 cbor_item_count = cbstr[0] & 31 # fixme: do not validate CBORTag length @@ -658,6 +660,21 @@ def to_obj(self) -> str: return self.value.hex() +class SuitEmptyBstr(SuitBstr): + """Representation of empty byte string.""" + + @classmethod + def from_cbor(cls, cbstr: bytes) -> SuitBstr: + """Restore SUIT representation from passed CBOR.""" + if len(cbstr) > 0: + raise ValueError(f"{cbstr} is not empty") + return cls(cbstr) + + def to_cbor(self) -> bytes: + """Dump SUIT representation to cbor encoded bytes.""" + return b"" + + class SuitHex(SuitBstr): """Representation of hex type.""" diff --git a/suit_generator/suit/types/keys.py b/suit_generator/suit/types/keys.py index 6c18c1df..e98721d6 100644 --- a/suit_generator/suit/types/keys.py +++ b/suit_generator/suit/types/keys.py @@ -134,20 +134,6 @@ class suit_parameter_version(suit_key): name = "suit-parameter-version" -class suit_parameter_version_comparison_type(suit_key): - """suit-parameter-version-comparison-type metadata.""" - - id = 1 - name = "suit-parameter-version-comparison-type" - - -class suit_parameter_version_comparison_value(suit_key): - """suit-parameter-version-comparison-value metadata.""" - - id = 2 - name = "suit-parameter-version-comparison-value" - - class suit_parameter_vendor_identifier(suit_key): """suit-parameter-vendor-identifier metadata.""" @@ -204,6 +190,13 @@ class suit_parameter_content(suit_key): name = "suit-parameter-content" +class suit_parameter_encryption_info(suit_key): + """suit-parameter-encryption-info metadata.""" + + id = 19 + name = "suit-parameter-encryption-info" + + class suit_parameter_uri(suit_key): """suit-parameter-uri metadata.""" @@ -309,10 +302,17 @@ class suit_payload_fetch(suit_key): name = "suit-payload-fetch" +class suit_install_legacy(suit_key): + """suit-install metadata (before v27).""" + + id = 17 + name = "suit-install-legacy" + + class suit_install(suit_key): """suit-install metadata.""" - id = 17 + id = 20 name = "suit-install" @@ -575,6 +575,13 @@ class suit_cose_key_id(suit_key): name = "suit-cose-key-id" +class suit_cose_iv(suit_key): + """suit-cose-iv metadata.""" + + id = 5 + name = "suit-cose-iv" + + class suit_issuer(suit_key): """CWT Issuer metadata.""" @@ -687,6 +694,55 @@ class cose_alg_eddsa(suit_key): name = "cose-alg-eddsa" +class cose_alg_aes_gcm_128(suit_key): + """Cose algorithm metadata.""" + + id = 1 + name = "cose-alg-aes-gcm-128" + + +class cose_alg_aes_gcm_192(suit_key): + """Cose algorithm metadata.""" + + id = 2 + name = "cose-alg-aes-gcm-192" + + +class cose_alg_aes_gcm_256(suit_key): + """Cose algorithm metadata.""" + + id = 3 + name = "cose-alg-aes-gcm-256" + + +class cose_alg_a256kw(suit_key): + """Cose algorithm metadata.""" + + id = -5 + name = "cose-alg-a256kw" + + +class cose_alg_a192kw(suit_key): + """Cose algorithm metadata.""" + + id = -4 + name = "cose-alg-a192kw" + + +class cose_alg_a128kw(suit_key): + """Cose algorithm metadata.""" + + id = -3 + name = "cose-alg-a128kw" + + +class cose_alg_direct(suit_key): + """Cose algorithm metadata.""" + + id = -6 + name = "cose-alg-direct" + + class suit_send_record_success(suit_key): """Reporting policy bit.""" @@ -713,3 +769,17 @@ class suit_send_sysinfo_failure(suit_key): id = 8 name = "suit-send-sysinfo-failure" + + +class suit_synchronous_invoke(suit_key): + """Synchronous invoke argument.""" + + id = 1 + name = "suit-synchronous-invoke" + + +class suit_timeout(suit_key): + """Timeout invoke argument.""" + + id = 2 + name = "suit-timeout" diff --git a/suit_generator/suit_kms_base.py b/suit_generator/suit_kms_base.py new file mode 100644 index 00000000..d6632f86 --- /dev/null +++ b/suit_generator/suit_kms_base.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""A base abstract class for any KMS implementations used by the SUIT encrypt/sign scripts.""" + +from abc import ABC, abstractmethod + + +class SuitKMSBase(ABC): + """Base abstract class for the KMS implementations.""" + + @abstractmethod + def init_kms(self, context) -> None: + """ + Initialize the KMS. + + :param context: The context to be used + """ + pass + + @abstractmethod + def encrypt(self, plaintext, key_name, context, aad) -> tuple[bytes, bytes, bytes]: + """ + Encrypt the plainext with an AES key. + + :param plaintext: The plaintext to be encrypted. + :param key_name: The name of the key to be used. + :param context: The context to be used + :param aad: The additional authenticated data to be used. + :return: The nonce, tag and ciphertext. + :rtype: tuple[bytes, bytes, bytes] + """ + pass diff --git a/tests/test_args.py b/tests/test_args.py index ec51f58d..136b3c56 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -14,13 +14,16 @@ @mock.patch( "argparse.ArgumentParser.parse_args", - return_value=argparse.Namespace(command="create", input_file="test1.json", output_file="test2.suit"), + return_value=argparse.Namespace( + command="create", input_file="test1.json", output_file="test2.suit", log_filename="test.log" + ), ) def test_create_cmd_mode_auto(mock_args): """Test arguments parsing.""" args = parse_arguments() assert args[0] == "create" assert vars(args[1]) == {"input_file": "test1.json", "output_file": "test2.suit"} + assert args[2] == "test.log" @pytest.mark.parametrize( diff --git a/tests/test_cli.py b/tests/test_cli.py index 602688c0..4e5094d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -20,19 +20,19 @@ def __init__(self, **kwargs): def monkey_patched_parse_arguments(cmd): """Monkey patched argument parser.""" args = Namespace(output_file="test1", input_file="test2", input_format="json") - return cmd, args + return cmd, args, None def monkey_patched_create_arguments(cmd): """Monkey patched argument parser.""" args = Namespace(input_file="test1", output_format="test2", output_file="test3") - return cmd, args + return cmd, args, None def monkey_patched_keys_arguments(cmd): """Monkey patched argument parser.""" args = Namespace(output_file="test1", key_type="test2") - return cmd, args + return cmd, args, None def monkey_patched_main_create(input_file: str, output_format: str, output_file: str): diff --git a/tests/test_cmd_cache_create.py b/tests/test_cmd_cache_create.py new file mode 100644 index 00000000..ded25748 --- /dev/null +++ b/tests/test_cmd_cache_create.py @@ -0,0 +1,208 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""Unit tests for cmd_cache_create.py implementation.""" + +import pytest +import pathlib +import os +import cbor2 +from suit_generator.cmd_cache_create import main as cmd_cache_create_main +from suit_generator.cmd_create import main as cmd_create_main + +TEMP_DIRECTORY = pathlib.Path("test_test_data") + +BINARY_CONTENT_1 = bytes([0x01, 0x02, 0x03, 0x04]) +BINARY_CONTENT_2 = bytes([0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11]) +BINARY_CONTENT_3 = bytes( + [ + 0x11, + 0x22, + 0x33, + 0x44, + 0x55, + ] +) + +URI1 = "#first" # [0x23, 0x66, 0x69, 0x72, 0x73, 0x74] +URI2 = "#second" # [0x23, 0x73, 0x65, 0x63, 0x6F, 0x6E, 0x64] +URI3 = "#third" # [0x23, 0x74, 0x68, 0x69, 0x72, 0x64] +ENVELOPE_ROOT_YAML = """SUIT_Envelope_Tagged: + suit-authentication-wrapper: + SuitDigest: + suit-digest-algorithm-id: cose-alg-sha-256 + suit-digest-bytes: 60198229d4c07c866094a3d19c2d8b15b5dd552cd5bba5cf8f78e492ccbb3327 + suit-manifest: + suit-manifest-component-id: + - raw: aa + suit-integrated-dependencies: + 'dependency_manifest': dep.suit + '#first': first.bin +""" + +ENVELOPE_DEP_YAML = """SUIT_Envelope_Tagged: + suit-authentication-wrapper: + SuitDigest: + suit-digest-algorithm-id: cose-alg-sha-256 + suit-digest-bytes: b2afeba5d8172371661b7ab5d7242c2ba6797ae27e713114619d90663ab6a2ec + suit-manifest: + suit-manifest-component-id: + - raw: bb + suit-integrated-dependencies: + '#second': second.bin + '#third': third.bin +""" + +# fmt: off +EXPECTED_CACHE_EB_8 = bytes([0xBF, + 0x66, 0x23, 0x66, 0x69, 0x72, 0x73, 0x74, # tstr "#first" + 0x5A, 0x00, 0x00, 0x00, 0x04, # bstr size 4 + 0x01, 0x02, 0x03, 0x04, # BINARY_CONTENT_1 + 0x60, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, # padding + 0x67, 0x23, 0x73, 0x65, 0x63, 0x6F, 0x6E, 0x64, # tstr "#second" + 0x5A, 0x00, 0x00, 0x00, 0x0a, # bstr size 10 + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, # BINARY_CONTENT_2 + 0x60, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # padding + 0xFF]) + +EXPECTED_CACHE_EB_64 = bytes([0xBF, + 0x66, 0x23, 0x66, 0x69, 0x72, 0x73, 0x74, # tstr "#first" + 0x5A, 0x00, 0x00, 0x00, 0x04, # bstr size 4 + 0x01, 0x02, 0x03, 0x04, # BINARY_CONTENT_1 + 0x60, 0x59, 0x00, 0x2B, # padding 47 bytes (4 bytes header + 43 bytes padding) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x67, 0x23, 0x73, 0x65, 0x63, 0x6F, 0x6E, 0x64, # tstr "#second" + 0x5A, 0x00, 0x00, 0x00, 0x0a, # bstr size 10 + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, # BINARY_CONTENT_2 + 0x60, 0x59, 0x00, 0x25, # padding 41 bytes (4 bytes header + 37 bytes padding) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF]) +# fmt: on + + +@pytest.fixture +def setup_and_teardown(tmp_path_factory): + """Create and cleanup environment.""" + # Setup environment + # - create temp directory + # - input files + start_directory = os.getcwd() + path = tmp_path_factory.mktemp(TEMP_DIRECTORY) + os.chdir(path) + with open("first.bin", "wb") as fh: + fh.write(BINARY_CONTENT_1) + with open("second.bin", "wb") as fh: + fh.write(BINARY_CONTENT_2) + with open("third.bin", "wb") as fh: + fh.write(BINARY_CONTENT_3) + yield + # Cleanup environment + # - remove temp directory + os.chdir(start_directory) + + +@pytest.mark.parametrize( + "eb_size, output_content", + [ + (8, EXPECTED_CACHE_EB_8), + (64, EXPECTED_CACHE_EB_64), + ], +) +def test_cache_create_from_payloads(setup_and_teardown, eb_size, output_content): + """Verify if the cache is correctly created from payloads.""" + + cmd_cache_create_main( + cache_create_subcommand="from_payloads", + input=[f"{URI1},first.bin", f"{URI2},second.bin"], + output_file="test_cache.bin", + eb_size=eb_size, + ) + + with open("test_cache.bin", "rb") as f: + assert f.read() == output_content + + +def test_cache_create_merge(setup_and_teardown): + """Verify if the cache is correctly created from two other cache partitions.""" + + cmd_cache_create_main( + cache_create_subcommand="from_payloads", + input=[f"{URI1},first.bin", f"{URI2},second.bin"], + output_file="test_cache1.bin", + eb_size=8, + ) + cmd_cache_create_main( + cache_create_subcommand="from_payloads", input=[f"{URI3},third.bin"], output_file="test_cache2.bin", eb_size=4 + ) + + cmd_cache_create_main( + cache_create_subcommand="from_payloads", + input=[f"{URI1},first.bin", f"{URI2},second.bin", f"{URI3},third.bin"], + output_file="test_cache_merged_expected.bin", + eb_size=16, + ) + + cmd_cache_create_main( + cache_create_subcommand="merge", + input=["test_cache1.bin", "test_cache2.bin"], + output_file="test_cache_merged.bin", + eb_size=16, + ) + + with open("test_cache_merged.bin", "rb") as f: + result = f.read() + + with open("test_cache_merged_expected.bin", "rb") as f: + expected = f.read() + + # Assert that cache resulting from merging two caches is the same as if the cache was created from the payloads + assert result == expected + + +def test_cache_create_merge_from_envelope(setup_and_teardown): + # Prepare envelope files + with open("root.yaml", "w") as fh: + fh.write(ENVELOPE_ROOT_YAML) + with open("dep.yaml", "w") as fh: + fh.write(ENVELOPE_DEP_YAML) + cmd_create_main(input_file="dep.yaml", output_file="dep.suit", input_format="AUTO") + cmd_create_main(input_file="root.yaml", output_file="root.suit", input_format="AUTO") + + cmd_cache_create_main( + cache_create_subcommand="from_envelope", + input_envelope="root.suit", + output_envelope="root_stripped.suit", + output_file="test_cache_from_envelope.bin", + eb_size=8, + omit_payload_regex=".*third", + dependency_regex="dep.*", + ) + + with open("test_cache_from_envelope.bin", "rb") as f: + assert f.read() == EXPECTED_CACHE_EB_8 + + with open("root_stripped.suit", "rb") as fh: + envelope_stripped = cbor2.load(fh) + + assert "#first" not in envelope_stripped.value.keys() + + dependency = envelope_stripped.value.pop("dependency_manifest", None) + assert dependency is not None + dependency = cbor2.loads(dependency).value + + assert "#second" not in dependency.keys() + + not_extracted = dependency.pop("#third", None) + assert not_extracted is not None + assert not_extracted == BINARY_CONTENT_3 diff --git a/tests/test_cmd_image.py b/tests/test_cmd_image.py index 45664f94..c2b76acc 100644 --- a/tests/test_cmd_image.py +++ b/tests/test_cmd_image.py @@ -5,13 +5,20 @@ """Unit tests for cmd_image.py implementation.""" import pytest +import pathlib +import os -from suit_generator.cmd_image import ImageCreator, EnvelopeStorage +import yaml + +from suit_generator.cmd_image import ImageCreator, EnvelopeStorageNrf54h20 from suit_generator.cmd_image import main as cmd_image_main from suit_generator.exceptions import GeneratorError, SUITError +from suit_generator.suit.envelope import SuitEnvelopeTagged from unittest.mock import _Call +TEMP_DIRECTORY = pathlib.Path("test_test_data") + MAX_CACHE_COUNT = 16 addresses = {0x00000000: b"\x00\x00\x00\x00", 0xDEADBEEF: b"\xEF\xBE\xAD\xDE", 0xFFFFFFFF: b"\xFF\xFF\xFF\xFF"} @@ -61,271 +68,8 @@ malformed_envelope_input = b"\x00" expected_boot_storage = ( - # Empty update candidate info (0x0E1E9340) - ":020000040E1ECE\n" - ":10934000AA55AA550000000000000000000000001F\n" - ":10935000000000000000000000000000000000000D\n" - ":1093600000000000000000000000000000000000FD\n" - ":1093700000000000000000000000000000000000ED\n" + # Empty update candidate info (0x0E1E9340 - 0x0E1E9380) # Uninitialized NVV area (0x0E1E9380 - 0x0E1E9400) - # Empty root manifest slot (0x0E1E9400) - ":10940000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C\n" - ":10941000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C\n" - ":10942000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C\n" - ":10943000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C\n" - ":10944000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C\n" - ":10945000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C\n" - ":10946000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C\n" - ":10947000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC\n" - ":10948000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC\n" - ":10949000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC\n" - ":1094A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC\n" - ":1094B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC\n" - ":1094C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC\n" - ":1094D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C\n" - ":1094E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C\n" - ":1094F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C\n" - ":10950000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B\n" - ":10951000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B\n" - ":10952000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B\n" - ":10953000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B\n" - ":10954000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B\n" - ":10955000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B\n" - ":10956000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B\n" - ":10957000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB\n" - ":10958000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB\n" - ":10959000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB\n" - ":1095A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB\n" - ":1095B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB\n" - ":1095C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB\n" - ":1095D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B\n" - ":1095E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B\n" - ":1095F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B\n" - ":10960000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A\n" - ":10961000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A\n" - ":10962000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A\n" - ":10963000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A\n" - ":10964000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A\n" - ":10965000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A\n" - ":10966000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A\n" - ":10967000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA\n" - ":10968000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA\n" - ":10969000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA\n" - ":1096A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA\n" - ":1096B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA\n" - ":1096C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA\n" - ":1096D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A\n" - ":1096E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A\n" - ":1096F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A\n" - ":10970000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69\n" - ":10971000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59\n" - ":10972000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49\n" - ":10973000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39\n" - ":10974000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29\n" - ":10975000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19\n" - ":10976000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09\n" - ":10977000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9\n" - ":10978000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9\n" - ":10979000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9\n" - ":1097A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9\n" - ":1097B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9\n" - ":1097C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9\n" - ":1097D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99\n" - ":1097E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89\n" - ":1097F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79\n" - ":10980000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68\n" - ":10981000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58\n" - ":10982000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48\n" - ":10983000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38\n" - ":10984000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28\n" - ":10985000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18\n" - ":10986000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08\n" - ":10987000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8\n" - ":10988000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8\n" - ":10989000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8\n" - ":1098A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8\n" - ":1098B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8\n" - ":1098C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8\n" - ":1098D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98\n" - ":1098E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88\n" - ":1098F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78\n" - ":10990000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67\n" - ":10991000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57\n" - ":10992000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47\n" - ":10993000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37\n" - ":10994000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27\n" - ":10995000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17\n" - ":10996000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07\n" - ":10997000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7\n" - ":10998000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7\n" - ":10999000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7\n" - ":1099A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7\n" - ":1099B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7\n" - ":1099C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7\n" - ":1099D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97\n" - ":1099E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87\n" - ":1099F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77\n" - ":109A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66\n" - ":109A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56\n" - ":109A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46\n" - ":109A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36\n" - ":109A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26\n" - ":109A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16\n" - ":109A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06\n" - ":109A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6\n" - ":109A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6\n" - ":109A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6\n" - ":109AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6\n" - ":109AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6\n" - ":109AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6\n" - ":109AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96\n" - ":109AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86\n" - ":109AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76\n" - ":109B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65\n" - ":109B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55\n" - ":109B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45\n" - ":109B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35\n" - ":109B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25\n" - ":109B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15\n" - ":109B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05\n" - ":109B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5\n" - ":109B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5\n" - ":109B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5\n" - ":109BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5\n" - ":109BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5\n" - ":109BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5\n" - ":109BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95\n" - ":109BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85\n" - ":109BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75\n" - # Application recovery manifest slot (0x0E1E9C00) - ":109C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64\n" - ":109C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54\n" - ":109C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44\n" - ":109C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34\n" - ":109C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24\n" - ":109C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14\n" - ":109C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04\n" - ":109C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4\n" - ":109C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4\n" - ":109C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4\n" - ":109CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4\n" - ":109CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4\n" - ":109CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4\n" - ":109CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94\n" - ":109CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84\n" - ":109CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74\n" - ":109D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63\n" - ":109D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53\n" - ":109D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43\n" - ":109D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33\n" - ":109D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23\n" - ":109D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13\n" - ":109D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03\n" - ":109D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3\n" - ":109D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3\n" - ":109D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3\n" - ":109DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3\n" - ":109DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3\n" - ":109DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3\n" - ":109DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93\n" - ":109DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83\n" - ":109DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73\n" - ":109E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62\n" - ":109E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52\n" - ":109E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42\n" - ":109E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32\n" - ":109E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22\n" - ":109E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12\n" - ":109E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02\n" - ":109E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2\n" - ":109E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2\n" - ":109E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2\n" - ":109EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2\n" - ":109EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2\n" - ":109EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2\n" - ":109ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92\n" - ":109EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82\n" - ":109EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72\n" - ":109F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61\n" - ":109F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51\n" - ":109F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41\n" - ":109F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31\n" - ":109F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21\n" - ":109F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11\n" - ":109F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01\n" - ":109F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1\n" - ":109F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1\n" - ":109F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1\n" - ":109FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1\n" - ":109FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1\n" - ":109FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1\n" - ":109FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91\n" - ":109FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81\n" - ":109FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71\n" - ":10A00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60\n" - ":10A01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50\n" - ":10A02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40\n" - ":10A03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30\n" - ":10A04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20\n" - ":10A05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10\n" - ":10A06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00\n" - ":10A07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0\n" - ":10A08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0\n" - ":10A09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0\n" - ":10A0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0\n" - ":10A0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0\n" - ":10A0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0\n" - ":10A0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90\n" - ":10A0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80\n" - ":10A0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70\n" - ":10A10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F\n" - ":10A11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F\n" - ":10A12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F\n" - ":10A13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F\n" - ":10A14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F\n" - ":10A15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F\n" - ":10A16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\n" - ":10A17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF\n" - ":10A18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF\n" - ":10A19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF\n" - ":10A1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF\n" - ":10A1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF\n" - ":10A1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F\n" - ":10A1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F\n" - ":10A1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F\n" - ":10A1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F\n" - ":10A20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E\n" - ":10A21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E\n" - ":10A22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E\n" - ":10A23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E\n" - ":10A24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E\n" - ":10A25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E\n" - ":10A26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE\n" - ":10A27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE\n" - ":10A28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE\n" - ":10A29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE\n" - ":10A2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE\n" - ":10A2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE\n" - ":10A2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E\n" - ":10A2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E\n" - ":10A2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E\n" - ":10A2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E\n" - ":10A30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D\n" - ":10A31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D\n" - ":10A32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D\n" - ":10A33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D\n" - ":10A34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D\n" - ":10A35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D\n" - ":10A36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD\n" - ":10A37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED\n" - ":10A38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD\n" - ":10A39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD\n" - ":10A3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD\n" - ":10A3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD\n" - ":10A3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D\n" - ":10A3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D\n" - ":10A3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D\n" - ":10A3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D\n" # Application local 1 manifest slot (0x0E1EA400) ":10A40000A300010118FB0259010BD86BA2025827C7\n" ":10A41000815824822F58203045487BD1B451ABF568\n" @@ -391,136 +135,6 @@ ":10A7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89\n" ":10A7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79\n" ":10A7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69\n" - # Application local 2 manifest slot (0x0E1EA800) - ":10A80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58\n" - ":10A81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48\n" - ":10A82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38\n" - ":10A83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28\n" - ":10A84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18\n" - ":10A85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08\n" - ":10A86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8\n" - ":10A87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8\n" - ":10A88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8\n" - ":10A89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8\n" - ":10A8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8\n" - ":10A8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8\n" - ":10A8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98\n" - ":10A8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88\n" - ":10A8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78\n" - ":10A8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68\n" - ":10A90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57\n" - ":10A91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47\n" - ":10A92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37\n" - ":10A93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27\n" - ":10A94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17\n" - ":10A95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07\n" - ":10A96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7\n" - ":10A97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7\n" - ":10A98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7\n" - ":10A99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7\n" - ":10A9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7\n" - ":10A9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7\n" - ":10A9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97\n" - ":10A9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87\n" - ":10A9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77\n" - ":10A9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67\n" - ":10AA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56\n" - ":10AA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46\n" - ":10AA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36\n" - ":10AA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26\n" - ":10AA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16\n" - ":10AA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06\n" - ":10AA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6\n" - ":10AA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6\n" - ":10AA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6\n" - ":10AA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6\n" - ":10AAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6\n" - ":10AAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6\n" - ":10AAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96\n" - ":10AAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86\n" - ":10AAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76\n" - ":10AAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66\n" - ":10AB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55\n" - ":10AB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45\n" - ":10AB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35\n" - ":10AB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25\n" - ":10AB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15\n" - ":10AB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05\n" - ":10AB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5\n" - ":10AB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5\n" - ":10AB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5\n" - ":10AB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5\n" - ":10ABA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5\n" - ":10ABB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5\n" - ":10ABC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95\n" - ":10ABD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85\n" - ":10ABE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75\n" - ":10ABF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65\n" - # Application local 3 manifest slot (0x0E1EAC00) - ":10AC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54\n" - ":10AC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44\n" - ":10AC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34\n" - ":10AC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24\n" - ":10AC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14\n" - ":10AC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04\n" - ":10AC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4\n" - ":10AC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4\n" - ":10AC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4\n" - ":10AC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4\n" - ":10ACA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4\n" - ":10ACB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4\n" - ":10ACC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94\n" - ":10ACD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84\n" - ":10ACE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74\n" - ":10ACF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64\n" - ":10AD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53\n" - ":10AD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43\n" - ":10AD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33\n" - ":10AD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23\n" - ":10AD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13\n" - ":10AD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03\n" - ":10AD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3\n" - ":10AD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3\n" - ":10AD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3\n" - ":10AD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3\n" - ":10ADA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3\n" - ":10ADB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3\n" - ":10ADC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93\n" - ":10ADD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83\n" - ":10ADE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73\n" - ":10ADF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63\n" - ":10AE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52\n" - ":10AE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42\n" - ":10AE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32\n" - ":10AE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22\n" - ":10AE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12\n" - ":10AE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02\n" - ":10AE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2\n" - ":10AE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2\n" - ":10AE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2\n" - ":10AE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2\n" - ":10AEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2\n" - ":10AEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2\n" - ":10AEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92\n" - ":10AED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82\n" - ":10AEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72\n" - ":10AEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62\n" - ":10AF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51\n" - ":10AF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41\n" - ":10AF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31\n" - ":10AF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21\n" - ":10AF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11\n" - ":10AF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01\n" - ":10AF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1\n" - ":10AF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1\n" - ":10AF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1\n" - ":10AF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1\n" - ":10AFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1\n" - ":10AFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1\n" - ":10AFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91\n" - ":10AFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81\n" - ":10AFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71\n" - ":10AFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61\n" # Application area end (0x0E1EB000) ":00000001FF\n" ) @@ -583,6 +197,113 @@ ":00000001FF\n" ) +MPI_KCONFIG_TEMPLATE = """ +SB_CONFIG_SUIT_MPI_ROOT_VENDOR_NAME="{root_vendor_name}" +SB_CONFIG_SUIT_MPI_ROOT_CLASS_NAME="{root_class_name}" +SB_CONFIG_SUIT_MPI_APP_LOCAL_1=y +SB_CONFIG_SUIT_MPI_APP_LOCAL_1_VENDOR_NAME="{app_local_1_vendor_name}" +SB_CONFIG_SUIT_MPI_APP_LOCAL_1_CLASS_NAME="{app_local_1_class_name}" +SB_CONFIG_SUIT_MPI_APP_LOCAL_2 is not set +SB_CONFIG_SUIT_MPI_APP_LOCAL_3 is not set +SB_CONFIG_SUIT_MPI_RAD_RECOVERY is not set +SB_CONFIG_SUIT_MPI_RAD_LOCAL_1=y +SB_CONFIG_SUIT_MPI_RAD_LOCAL_1_VENDOR_NAME="{rad_local_1_vendor_name}" +SB_CONFIG_SUIT_MPI_RAD_LOCAL_1_CLASS_NAME="{rad_local_1_class_name}" +SB_CONFIG_SUIT_MPI_RAD_LOCAL_2 is not set +""" + +INPUT_ENVELOPE_YAML = """SUIT_Envelope_Tagged: + suit-authentication-wrapper: + SuitDigest: + suit-digest-algorithm-id: cose-alg-sha-256 + suit-digest-bytes: abe742c95d30b5d0dcc33e03cc939e563b41673cd9c6d0c6d06a5300c9af182e + suit-manifest: + suit-manifest-version: 1 + suit-manifest-sequence-number: 1 + suit-common: + suit-components: + - - TEST + - 1 + - 2 + - 3 + suit-manifest-component-id: + - INSTLD_MFST + - RFC4122_UUID: + namespace: {component_id_namespace} + name: {component_id_name} +""" + + +@pytest.fixture +def setup_and_teardown(tmp_path_factory): + """Create and cleanup environment.""" + # Setup environment + # - create temp directory + # - input files + start_directory = os.getcwd() + path = tmp_path_factory.mktemp(TEMP_DIRECTORY) + os.chdir(path) + with open(".config", "w") as fh: + fh.write( + MPI_KCONFIG_TEMPLATE.format( + root_vendor_name="root_custom_vendor", + root_class_name="root_custom_class", + app_local_1_vendor_name="app_local_1_custom_vendor", + app_local_1_class_name="app_local_1_custom_class", + rad_local_1_vendor_name="rad_local_1_custom_vendor", + rad_local_1_class_name="rad_local_1_custom_class", + ) + ) + with open(".config_with_defaults", "w") as fh: + fh.write( + MPI_KCONFIG_TEMPLATE.format( + root_vendor_name="nordicsemi.com", + root_class_name="nRF54H20_sample_root", + app_local_1_vendor_name="nordicsemi.com", + app_local_1_class_name="nRF54H20_sample_app", + rad_local_1_vendor_name="rad_local_1_custom_vendor", + rad_local_1_class_name="rad_local_1_custom_class", + ) + ) + with open(".config_duplicates", "w") as fh: + fh.write( + MPI_KCONFIG_TEMPLATE.format( + root_vendor_name="nordicsemi.com", + root_class_name="nRF54H20_sample_root", + app_local_1_vendor_name="nordicsemi.com", + app_local_1_class_name="nRF54H20_sample_app", + rad_local_1_vendor_name="nordicsemi.com", + rad_local_1_class_name="nRF54H20_sample_app", + ) + ) + with open(".config_role_exchanged", "w") as fh: + fh.write( + MPI_KCONFIG_TEMPLATE.format( + root_vendor_name="nordicsemi.com", + root_class_name="nRF54H20_sample_root", + app_local_1_vendor_name="nordicsemi.com", + app_local_1_class_name="nRF54H20_sample_rad", + rad_local_1_vendor_name="nordicsemi.com", + rad_local_1_class_name="nRF54H20_sample_app", + ) + ) + for item_name in ["root", "app_local_1", "rad_local_1"]: + with open(f"custom_{item_name}_component_id.suit", "wb") as fh: + envelope_data = SuitEnvelopeTagged.from_obj( + yaml.load( + INPUT_ENVELOPE_YAML.format( + component_id_namespace=f"{item_name}_custom_vendor", + component_id_name=f"{item_name}_custom_class", + ), + Loader=yaml.FullLoader, + ) + ).to_cbor() + fh.write(envelope_data) + yield + # Cleanup environment + # - remove temp directory + os.chdir(start_directory) + def prepare_calls(data): """Split data by lines and wrap each line using _Call object for easy assertions; get rid of last newline""" @@ -596,17 +317,6 @@ def test_struct_format(nb_of_caches): assert ImageCreator._prepare_suit_storage_struct_format(nb_of_caches) == format -@pytest.mark.parametrize("nb_of_caches", range(MAX_CACHE_COUNT + 1)) -def test_update_candidate_info_for_boot(nb_of_caches): - suit_storage_bytes = b"\xAA\x55\xAA\x55\x00\x00\x00\x00" - envelope_address_size_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" - caches_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * nb_of_caches - - expected_bytes = suit_storage_bytes + envelope_address_size_bytes + caches_bytes - - assert ImageCreator._prepare_update_candidate_info_for_boot(nb_of_caches) == expected_bytes - - def test_update_candidate_info_verify_class_id_offset(): from suit_generator.envelope import SuitEnvelope from suit_generator.suit.envelope import SuitEnvelopeTagged @@ -617,14 +327,14 @@ def test_update_candidate_info_verify_class_id_offset(): envelope._envelope = SuitEnvelopeTagged.from_cbor(signed_envelope_input).to_obj() # Generate storage envelope slot for the envelope - storage = EnvelopeStorage(0) + storage = EnvelopeStorageNrf54h20(0) storage.add_envelope(envelope) (envelope_role, envelope_cbor) = storage._envelopes.popitem() # Extract the class ID, based on the offset and minified envelope storage_dict = cbor_loads(envelope_cbor) - offset = storage_dict[EnvelopeStorage.ENVELOPE_SLOT_CLASS_ID_OFFSET_KEY] - envelope_bstr = storage_dict[EnvelopeStorage.ENVELOPE_SLOT_ENVELOPE_BSTR_KEY] + offset = storage_dict[EnvelopeStorageNrf54h20.ENVELOPE_SLOT_CLASS_ID_OFFSET_KEY] + envelope_bstr = storage_dict[EnvelopeStorageNrf54h20.ENVELOPE_SLOT_ENVELOPE_BSTR_KEY] # RFC4122 uuid5(nordic_vid, 'nRF54H20_sample_app') exp_class_id = b"\x08\xc1\xb5\x99\x55\xe8\x5f\xbc\x9e\x76\x7b\xc2\x9c\xe1\xb0\x4d" @@ -657,7 +367,7 @@ def test_unsupported_image_subcommand(): input_file="", storage_output_file="", update_candidate_info_address=0, - envelope_address=0, + storage_address=0, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=0, @@ -671,12 +381,13 @@ def test_boot_subcommand_nonexisting_input_file(): input_file=["nonexisting"], storage_output_directory="", update_candidate_info_address=0, - envelope_address=0, + storage_address=0, envelope_slot_size=2048, envelope_slot_count=8, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=0, + config_file=None, ) @@ -690,12 +401,13 @@ def test_boot_subcommand_manifest_without_component_id(mocker): input_file=["some_input"], storage_output_directory="some_output", update_candidate_info_address=0x0E1EEC00, - envelope_address=0x0E1EED80, + storage_address=0x0E1EED80, envelope_slot_size=2048, envelope_slot_count=8, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=4, + config_file=None, ) @@ -708,12 +420,13 @@ def test_boot_subcommand_success(mocker): input_file=["some_input"], storage_output_directory="some_output", update_candidate_info_address=0x0E1E9340, - envelope_address=0x0E1E7000, + storage_address=0x0E1E7000, envelope_slot_size=2048, envelope_slot_count=1, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=6, + config_file=None, ) io_mock().read.assert_called_once() @@ -727,7 +440,7 @@ def test_update_subcommand_nonexisting_input_file(): input_file="nonexisting", storage_output_file="", update_candidate_info_address=0, - envelope_address=0, + storage_address=0, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=0, @@ -748,7 +461,7 @@ def test_update_subcommand_success(mocker): input_file="some_input", storage_output_file="some_storage_output", update_candidate_info_address=0x0E1EEC00, - envelope_address=0x0E1EED80, + storage_address=0x0E1EED80, dfu_partition_output_file="some_dfu_partition_output", dfu_partition_address=0x0E100000, dfu_max_caches=4, @@ -770,12 +483,13 @@ def test_malformed_envelope(mocker): input_file=["some_input"], storage_output_directory="some_output", update_candidate_info_address=0x0E1FE000, - envelope_address=0x0E1FF000, + storage_address=0x0E1FF000, envelope_slot_size=2048, envelope_slot_count=8, dfu_partition_output_file="", dfu_partition_address=0, dfu_max_caches=0, + config_file=None, ) @@ -803,8 +517,101 @@ def bin2hex_mock(*args, **kwargs): input_file="some_input", storage_output_file="some_storage_output", update_candidate_info_address=0x0E1EEC00, - envelope_address=0x0E1EED80, + storage_address=0x0E1EED80, dfu_partition_output_file="some_dfu_partition_output", dfu_partition_address=0x0E100000, dfu_max_caches=4, ) + + +def test_nrf54_storage_no_defaults(): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=False, kconfig=None) + assert storage._assignments == {} + + +def test_nrf54_storage_with_defaults(): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=True, kconfig=None) + assert len(storage._assignments) == 8 + + +def test_nrf54_storage_custom_config_defaults(setup_and_teardown): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=True, kconfig=".config") + assert len(storage._assignments) == 11 + + +def test_nrf54_storage_custom_config_with_defaults_overwrite(setup_and_teardown): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=True, kconfig=".config_with_defaults") + # 8 default assignments + 3 custom assignments: 2 default values + one custom value, + # 2 custom assignments with default values are overwritten so final result is 8 + 1 = 9 + assert len(storage._assignments) == 9 + + +def test_nrf54_storage_custom_config_with_role_change_duplicates(setup_and_teardown): + with pytest.raises(GeneratorError): + # GeneratorError shall be raised due to duplicated assignments + EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=True, kconfig=".config_duplicates") + + +def test_nrf54_storage_custom_config_with_role_change_no_duplicates(setup_and_teardown): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=True, kconfig=".config_role_exchanged") + # 8 default assignments + 3 custom assignment: root uses default value, APP and RAD have exchanged roles. + assert len(storage._assignments) == 8 + + +def test_nrf54_storage_custom_config_no_defaults(setup_and_teardown): + storage = EnvelopeStorageNrf54h20(base_address=0xFF, load_defaults=False, kconfig=".config") + assert len(storage._assignments) == 3 + + +def test_generate_boot_images_for_default_vid_cid(): + pass + + +@pytest.mark.parametrize( + "input_envelope, expected_storage", + [ + ("custom_app_local_1_component_id.suit", "suit_installed_envelopes_application_merged.hex"), + ("custom_rad_local_1_component_id.suit", "suit_installed_envelopes_radio_merged.hex"), + ("custom_root_component_id.suit", "suit_installed_envelopes_application_merged.hex"), + ], +) +def test_generate_boot_images_for_custom_vid_cid_separately(setup_and_teardown, input_envelope, expected_storage): + """Test generating boot images for custom VID/CID separately.""" + ImageCreator.create_files_for_boot( + input_files=[input_envelope], + storage_output_directory="./", + storage_address=0, + config_file=".config", + ) + assert pathlib.Path(expected_storage).is_file() + + +def test_generate_boot_images_for_custom_vid_cid_all_envelopes_in_one_request(setup_and_teardown): + """Test generating boot images for custom VID/CID in one request.""" + ImageCreator.create_files_for_boot( + input_files=[ + "custom_app_local_1_component_id.suit", + "custom_rad_local_1_component_id.suit", + "custom_root_component_id.suit", + ], + storage_output_directory="./", + storage_address=0, + config_file=".config", + ) + assert pathlib.Path("suit_installed_envelopes_application_merged.hex").is_file() + assert pathlib.Path("suit_installed_envelopes_radio_merged.hex").is_file() + + +def test_generate_update_images_for_custom_non_defined_vid_cid(setup_and_teardown): + """Test generating update images for custom VID/CID when VID/CID is not known.""" + with pytest.raises(GeneratorError): + ImageCreator.create_files_for_boot( + input_files=[ + "custom_app_local_1_component_id.suit", + "custom_rad_local_1_component_id.suit", + "custom_root_component_id.suit", + ], + storage_output_directory="./", + storage_address=0, + config_file=None, + ) diff --git a/tests/test_envelope.py b/tests/test_envelope.py index a7d64dac..19be64d5 100644 --- a/tests/test_envelope.py +++ b/tests/test_envelope.py @@ -15,7 +15,7 @@ from suit_generator.envelope import SuitEnvelope from suit_generator.exceptions import GeneratorError from suit_generator.input_output import FileTypeException -from suit_generator.suit.authentication import SuitDigest, SuitAuthenticationBlock +from suit_generator.suit.security import SuitDigest, SuitAuthenticationBlock from suit_generator.suit.types.common import Metadata, cbstr TEMP_DIRECTORY = pathlib.Path("test_test_data") @@ -681,7 +681,7 @@ def test_envelope_signed_twice_parsing(setup_and_teardown): @patch( - "suit_generator.suit.authentication.SuitAuthentication._metadata", + "suit_generator.suit.security.SuitAuthentication._metadata", Metadata(map={"SuitDigest*": cbstr(SuitDigest), "SuitAuthentication*": SuitAuthenticationBlock}), ) def test_envelope_parsing_wrong_internal_structure_dynamic_element_twice(setup_and_teardown): @@ -692,7 +692,7 @@ def test_envelope_parsing_wrong_internal_structure_dynamic_element_twice(setup_a @patch( - "suit_generator.suit.authentication.SuitAuthentication._metadata", + "suit_generator.suit.security.SuitAuthentication._metadata", Metadata(map={"SuitDigest*": cbstr(SuitDigest), "SuitAuthentication": SuitAuthenticationBlock}), ) def test_envelope_parsing_wrong_internal_structure_dynamic_element_at_the_beginning(setup_and_teardown): diff --git a/tests/test_envelope_api.py b/tests/test_envelope_api.py index bf54bf72..8a20a96d 100644 --- a/tests/test_envelope_api.py +++ b/tests/test_envelope_api.py @@ -99,7 +99,7 @@ CoseSign1Tagged: protected: suit-cose-algorithm-id: cose-alg-es-256 - suit-cose-key-id: 0x7fffffe0 + suit-cose-key-id: 0x40000000 unprotected: {} payload: None signature: DEADBEEF diff --git a/tests/test_ncs_sign_script.py b/tests/test_ncs_sign_script.py index abd393b9..1cf6bf5d 100644 --- a/tests/test_ncs_sign_script.py +++ b/tests/test_ncs_sign_script.py @@ -22,7 +22,7 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature -from suit_generator.suit.authentication import CoseSigStructure +from suit_generator.suit.security import CoseSigStructure from suit_generator.suit.envelope import SuitEnvelopeTagged from suit_generator.suit.types.keys import ( @@ -190,7 +190,7 @@ 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(f"key_private_{private_key}.pem") + 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: @@ -233,7 +233,7 @@ def test_envelope_sign_and_verify(setup_and_teardown, input_data, amount_of_payl signer = Signer() signer.load_envelope("test_envelope.suit") - signer.sign("key_private_es_256.pem") + 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: @@ -260,7 +260,7 @@ def test_envelope_sign_and_verify(setup_and_teardown, input_data, amount_of_payl cose_structure = CoseSigStructure.from_obj( { "context": "Signature1", - "body_protected": {"suit-cose-algorithm-id": algorithm_name, "suit-cose-key-id": 0x7FFFFFE0}, + "body_protected": {"suit-cose-algorithm-id": algorithm_name, "suit-cose-key-id": 0x40000000}, "external_add": "", "payload": digest_object, } @@ -276,7 +276,7 @@ def test_ncs_signing_unsupported(setup_and_teardown): signer = Signer() signer.load_envelope("test_envelope.suit") with pytest.raises(SignerError): - signer.sign("key_private_rs2048.pem") + signer.sign(pathlib.Path("key_private_rs2048.pem")) @patch("ncs.sign_script.DEFAULT_KEY_ID", 0x0C0FFE) @@ -291,7 +291,7 @@ def test_ncs_signing_manifest_component_id_known_default_key_used(setup_and_tear assert parsed_manifest_id == expected_manifest_id - signer.sign("key_private_es_256.pem") + 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: @@ -309,7 +309,7 @@ def test_ncs_signing_manifest_component_id_known_default_key_used(setup_and_tear .SuitAuthentication[1] .SuitAuthenticationBlock.CoseSign1Tagged.value.CoseSign1[0] .SuitHeaderMap[suit_cose_key_id] - .value + .value.value == 0x0C0FFE ) @@ -320,7 +320,7 @@ def test_ncs_signing_manifest_component_id_known_non_default(setup_and_teardown) signer = Signer() signer.load_envelope("test_envelope_manifest_component_id.suit") - signer.sign("key_private_es_256.pem") + 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: @@ -331,7 +331,7 @@ def test_ncs_signing_manifest_component_id_known_non_default(setup_and_teardown) .SuitAuthentication[1] .SuitAuthenticationBlock.CoseSign1Tagged.value.CoseSign1[0] .SuitHeaderMap[suit_cose_key_id] - .value + .value.value == 0xFFEEDDBB ) @@ -342,7 +342,7 @@ def test_ncs_signing_manifest_component_id_unknown(setup_and_teardown): signer = Signer() signer.load_envelope("test_envelope_manifest_component_id.suit") - signer.sign("key_private_es_256.pem") + 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: @@ -353,7 +353,7 @@ def test_ncs_signing_manifest_component_id_unknown(setup_and_teardown): .SuitAuthentication[1] .SuitAuthenticationBlock.CoseSign1Tagged.value.CoseSign1[0] .SuitHeaderMap[suit_cose_key_id] - .value + .value.value == 0xDEADBEEF ) diff --git a/tests/test_suit_authentication.py b/tests/test_suit_authentication.py index f36d91c8..11c66fe0 100644 --- a/tests/test_suit_authentication.py +++ b/tests/test_suit_authentication.py @@ -6,7 +6,7 @@ """Unit tests for suit authentication parsing.""" import binascii import pytest -from suit_generator.suit.authentication import SuitAuthentication, CoseSigStructure +from suit_generator.suit.security import SuitAuthentication, CoseSigStructure from suit_generator.suit.types.keys import suit_cose_algorithm_id TEST_DATA = { @@ -136,7 +136,7 @@ def test_sig_structure(): assert hasattr(structure, "CoseSigStructure") assert len(structure.CoseSigStructure) == 4 assert structure.CoseSigStructure[0].value == "Signature1" - assert structure.CoseSigStructure[1].SuitHeaderMap[suit_cose_algorithm_id].SuitcoseSignAlg == "cose-alg-es-256" + assert structure.CoseSigStructure[1].SuitHeaderMap[suit_cose_algorithm_id].SuitcoseAlg == "cose-alg-es-256" assert structure.CoseSigStructure[2].SuitHex == b"" assert structure.CoseSigStructure[3].to_cbor().hex().upper() == "4C822F49AAABBBCCCDDDEEEFFF" assert hex_value is not None diff --git a/tests/test_suit_encryption.py b/tests/test_suit_encryption.py new file mode 100644 index 00000000..9a25c62e --- /dev/null +++ b/tests/test_suit_encryption.py @@ -0,0 +1,97 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +"""Unit tests for suit encryption parsing.""" +import binascii +import pytest + +from suit_generator.suit.security import CoseEncryptTagged + +TEST_DATA = { + "COSE_ENCRYPT_FROM_SPEC": ( + "D8608443A10101A10550F14AAB9D81D51F7AD943FE87AF4F70CDF6818340" + "A2012204456B69642D31581875603FFC9518D794713C8CA8A115A7FB3256" + "5A6D59534D62" + ), +} + +TEST_DATA_OBJECT = { + "COSE_ENCRYPT_FROM_SPEC": { + "CoseEncryptTagged": { + "protected": { + "suit-cose-algorithm-id": "cose-alg-aes-gcm-128", + }, + "unprotected": {"suit-cose-iv": "f14aab9d81d51f7ad943fe87af4f70cd"}, + "ciphertext": None, + "recipients": [ + { + "protected": {}, + "unprotected": { + "suit-cose-algorithm-id": "cose-alg-a128kw", + "suit-cose-key-id": "6b69642d31", # "kid-1" + }, + "ciphertext": "75603ffc9518d794713c8ca8a115a7fb32565a6d59534d62", + }, + ], + } + }, +} + + +@pytest.mark.parametrize( + "input_data", + [ + "COSE_ENCRYPT_FROM_SPEC", + ], +) +def test_suit_cose_encrypt_content_from_obj(input_data): + suit_obj = CoseEncryptTagged.from_obj(TEST_DATA_OBJECT[input_data]) + assert suit_obj.value is not None + + +@pytest.mark.parametrize( + "input_data", + [ + "COSE_ENCRYPT_FROM_SPEC", + ], +) +def test_suit_cose_encrypt_from_obj(input_data): + suit_obj = CoseEncryptTagged.from_obj(TEST_DATA_OBJECT[input_data]) + suit_binary = suit_obj.to_cbor() + assert suit_obj.value is not None + assert suit_binary.hex() == CoseEncryptTagged.from_cbor(suit_binary).to_cbor().hex() + + +@pytest.mark.parametrize( + "input_data", + [ + "COSE_ENCRYPT_FROM_SPEC", + ], +) +def test_suit_cose_encrypt_from_cbor(input_data): + suit_obj = CoseEncryptTagged.from_cbor(binascii.a2b_hex(TEST_DATA[input_data])) + assert suit_obj.value is not None + + +@pytest.mark.parametrize( + "input_data", + [ + "COSE_ENCRYPT_FROM_SPEC", + ], +) +def test_suit_cose_encrypt_from_cbor_parse_and_dump(input_data): + suit_obj = CoseEncryptTagged.from_cbor(binascii.a2b_hex(TEST_DATA[input_data])) + assert suit_obj.to_cbor().hex().upper() == TEST_DATA[input_data].upper() + + +@pytest.mark.parametrize( + "input_data", + [ + "COSE_ENCRYPT_FROM_SPEC", + ], +) +def test_suit_cose_encrypt_to_cbor_result(input_data): + suit_obj = CoseEncryptTagged.from_obj(TEST_DATA_OBJECT[input_data]) + assert suit_obj.to_cbor().hex().upper() == TEST_DATA[input_data].upper() diff --git a/tests/test_suit_envelope.py b/tests/test_suit_envelope.py index c9a40a57..26053d41 100644 --- a/tests/test_suit_envelope.py +++ b/tests/test_suit_envelope.py @@ -649,7 +649,7 @@ CoseSign1Tagged: protected: suit-cose-algorithm-id: cose-alg-es-256 - suit-cose-key-id: 0x7fffffe0 + suit-cose-key-id: 0x40000000 unprotected: {} payload: None, signature: DEADBEEF, diff --git a/tests/test_suit_envelope_severable.py b/tests/test_suit_envelope_severable.py index 1c003ba7..429709d4 100644 --- a/tests/test_suit_envelope_severable.py +++ b/tests/test_suit_envelope_severable.py @@ -376,9 +376,9 @@ "ENVELOPE_2_SEVERED_TEXT_FETCH_INSTALL": ( "d86ba602458143822f400358cea601010201035857a2028384414d4218ff451a0e054000451a0005600084414" "d410e451a2e054000451a000560008241444100045829840c0114a201507617daa571fd5a858f94e28d735ce9" - "f40250d622bafd4337518590bc6368cda7fbca11822f5820f0b65173c03a9c481a0fe3ea62ed744bcfb0bef21" + "f40250d622bafd4337518590bc6368cda7fbca14822f5820f0b65173c03a9c481a0fe3ea62ed744bcfb0bef21" "43d4380e52dd67d5ee4200d10822f5820814a3a9e09cf691e5fa77c315c0a69a9929ee86601d5dfdca8b24819" - "e0be745917822f5820aac171d7a184ecd31c01495ac6b656b2e60a5aa43de6f6d1c9acdac29bb540c211581e9" + "e0be745917822f5820aac171d7a184ecd31c01495ac6b656b2e60a5aa43de6f6d1c9acdac29bb540c214581e9" "00c0214a115692366696c652e62696e150003000c0114a11602160003001050840c0214a115692366696c652e" "62696e175896a162656ea184414d4102451a0e0aa000451a00056000a60178184e6f726469632053656d69636" "f6e647563746f7220415341026e6e5246353432305f637075617070036e6e6f7264696373656d692e636f6d04"