diff mbox series

[06/13] arm/uefi_capsule: Switch Capsule generation tool from U-Boot to EDK2

Message ID 20250730115327.3671160-7-harsimransingh.tungal@arm.com
State New
Headers show
Series Add PSA FWU support and capsule generation via EDK2 tooling | expand

Commit Message

Harsimran Singh Tungal July 30, 2025, 11:53 a.m. UTC
From: Ali Can Ozaslan <ali.oezaslan@arm.com>

This commit updates the uefi_capsule.bbclass to use the EDK2
GenerateCapsule tool instead of the mkeficapsule utility from U-Boot.

The switch was necessary because the mkeficapsule utility from U-Boot
does not support generating capsules with multiple payloads, whereas
the EDK2 GenerateCapsule tool provides native support for multi-image
capsule creation.

These changes allow building UEFI capsules with multiple firmware
binaries in one step, making the firmware update process
more flexible.

- Switching dependency from u-boot-tools-native to
  edk2-basetools-native
- Updating the actual capsule creation command to GenerateCapsule
  with the appropriate flags (including hardware instance, lowest
  supported version, and monotonic count)
    * CAPSULE_HARDWARE_INSTANCE defines which hardware instance
      the capsule update is intended for. This can be set
          to "1" or "0" indicating the first hardware module or SoC.
          For systems with multiple modules, subsequent instances
          could be numbered 2, 3, etc.
    * CAPSULE_LOWEST_SUPPORTED_VERSION enables roll-back protection
      by specifying the minimum firmware version that the platform
      accepts. Any firmware update below this version will be
      rejected.It should be set 0, 1, 2, etc according to your
      firmware security and versioning requirements.
- Combining certificates into the private key file as required
  by GenerateCapsule

- Add support for multiple firmware payloads
  This update refactors the capsule generation process to support
  multiple firmware binaries instead of a single payload.
  Key changes include:

- Integration of a JSON generator script to define multiple payloads
- Add default path for JSON config generator and prepare
  test infrastructure.
- Introduction of new variables
    * CAPSULE_ALL_COMPONENTS:  of all available components
    to be included in the capsule generation process.
    * CAPSULE_SELECTED_COMPONENTS: Subset of components from
    CAPSULE_ALL_COMPONENTS that should actually be included
    in the final capsule image.
- Replacement of direct GenerateCapsule arguments with JSON input
- Allow passing custom arguments to GenerateCapsule via
  `CAPSULE_EXTRA_ARGS` variable
- Cleanup of temporary files used in the capsule generation process

These changes align with EDK2's flexible capsule format and enable
component level filtering for more advanced firmware update scenarios.

Signed-off-by: Ali Can Ozaslan <ali.oezaslan@arm.com>
Signed-off-by: Harsimran Singh Tungal <harsimransingh.tungal@arm.com>
---
 meta-arm/classes/uefi_capsule.bbclass         |  93 +++++++++---
 meta-arm/conf/layer.conf                      |   3 +
 .../scripts/generate_capsule_json_multiple.py | 142 ++++++++++++++++++
 3 files changed, 213 insertions(+), 25 deletions(-)
 create mode 100644 meta-arm/scripts/generate_capsule_json_multiple.py
diff mbox series

Patch

diff --git a/meta-arm/classes/uefi_capsule.bbclass b/meta-arm/classes/uefi_capsule.bbclass
index a0709c0f..9d98959d 100644
--- a/meta-arm/classes/uefi_capsule.bbclass
+++ b/meta-arm/classes/uefi_capsule.bbclass
@@ -1,51 +1,94 @@ 
 # This class generates UEFI capsules
-# The current class supports generating a capsule with single firmware binary
+# The current class supports generating a capsule with multiple firmware binaries
 
 IMAGE_TYPES += "uefi_capsule"
 
-# u-boot-tools should be installed in the native sysroot directory
-do_image_uefi_capsule[depends] += "u-boot-tools-native:do_populate_sysroot"
+# edk2-basetools should be installed in the native sysroot directory
+do_image_uefi_capsule[depends] += "edk2-basetools-native:do_populate_sysroot"
 
 # By default the wic image is used to create a capsule
 CAPSULE_IMGTYPE ?= "wic"
 
 # IMGDEPLOYDIR is used as the default location of firmware binary for which the capsule needs to be created
-CAPSULE_IMGLOCATION ?= "${IMGDEPLOYDIR}"
+CAPSULE_IMG_LOCATION ?= "${IMGDEPLOYDIR}"
 
 # The generated capsule by default has uefi.capsule extension
 CAPSULE_EXTENSION ?= "uefi.capsule"
 
 # The generated capsule's name by default is the same as UEFI_FIRMWARE_BINARY
-CAPSULE_NAME ?= "${UEFI_FIRMWARE_BINARY}"
+CAPSULE_NAME ??= "${UEFI_FIRMWARE_BINARY}"
+
+# The generated capsule configuration file extension
+CAPSULE_CONFIG_FILE_EXTENSION ?= "json"
+
+# The generated capsule configuration file
+CAPSULE_CONFIG_FILE ?= "${IMGDEPLOYDIR}/${CAPSULE_NAME}.${CAPSULE_CONFIG_FILE_EXTENSION}"
+
+# Path to the script that generates the UEFI capsule payloads JSON
+UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT ?= "${META_ARM_LAYER_DIR}/scripts/generate_capsule_json_multiple.py"
+
+# Additional variables for capsule component filtering
+CAPSULE_ALL_COMPONENTS ?= ""
+CAPSULE_SELECTED_COMPONENTS ??= ""
+
+# Variables required by the EDK2 GenerateCapsule tool.
+CAPSULE_CERTIFICATE_PATHS ?= ""
+CAPSULE_FW_VERSIONS ?= ""
+CAPSULE_GUIDS ?= ""
+CAPSULE_INDEXES ?= ""
+CAPSULE_HARDWARE_INSTANCES ?= ""
+CAPSULE_LOWEST_SUPPORTED_VERSIONS ?= ""
+CAPSULE_MONOTONIC_COUNTS ?= ""
+CAPSULE_PRIVATE_KEY_PATHS ?= ""
+UEFI_FIRMWARE_BINARIES ?= ""
+PAYLOAD_CERTIFICATE_PATH ?= ""
+PAYLOAD_PRIVATE_KEY_PATH ?= ""
 
-# The following variables must be set to be able to generate a capsule update
-CAPSULE_CERTIFICATE_PATH ?= ""
-CAPSULE_FW_VERSION ?= ""
-CAPSULE_GUID ?= ""
-CAPSULE_INDEX ?= ""
-CAPSULE_MONOTONIC_COUNT ?= ""
-CAPSULE_PRIVATE_KEY_PATH ?= ""
-UEFI_FIRMWARE_BINARY ?= ""
 
 # Check if the required variables are set
 python() {
-    for var in ["CAPSULE_CERTIFICATE_PATH", "CAPSULE_FW_VERSION", \
-                "CAPSULE_GUID", "CAPSULE_INDEX", \
-                "CAPSULE_MONOTONIC_COUNT", "CAPSULE_PRIVATE_KEY_PATH", \
-                "UEFI_FIRMWARE_BINARY"]:
+    for var in ["CAPSULE_CERTIFICATE_PATHS", "CAPSULE_FW_VERSIONS", \
+                "CAPSULE_GUIDS", "CAPSULE_INDEXES", \
+                "CAPSULE_HARDWARE_INSTANCES", \
+                "CAPSULE_LOWEST_SUPPORTED_VERSIONS", \
+                "CAPSULE_MONOTONIC_COUNTS", "CAPSULE_PRIVATE_KEY_PATHS", \
+                "UEFI_FIRMWARE_BINARIES", \
+                "UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT", \
+                "CAPSULE_ALL_COMPONENTS", \
+                "CAPSULE_SELECTED_COMPONENTS", \
+                "PAYLOAD_CERTIFICATE_PATH", \
+                "PAYLOAD_PRIVATE_KEY_PATH"]:
         if not d.getVar(var):
             raise bb.parse.SkipRecipe(f"{var} not set")
 }
 
 IMAGE_CMD:uefi_capsule(){
-    mkeficapsule --certificate ${CAPSULE_CERTIFICATE_PATH} \
-                 --fw-version ${CAPSULE_FW_VERSION} \
-                 --guid ${CAPSULE_GUID} \
-                 --index ${CAPSULE_INDEX} \
-                 --monotonic-count ${CAPSULE_MONOTONIC_COUNT} \
-                 --private-key ${CAPSULE_PRIVATE_KEY_PATH} \
-                 ${UEFI_FIRMWARE_BINARY} \
-                 ${CAPSULE_IMGLOCATION}/${CAPSULE_NAME}.${CAPSULE_EXTENSION}
+    # Generates the UEFI capsule payloads JSON
+    ${PYTHON} ${UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT} \
+             --selected_components ${CAPSULE_SELECTED_COMPONENTS}\
+             --components ${CAPSULE_ALL_COMPONENTS}\
+             --fw_versions ${CAPSULE_FW_VERSIONS} \
+             --guids ${CAPSULE_GUIDS} \
+             --hardware_instances ${CAPSULE_HARDWARE_INSTANCES} \
+             --lowest_supported_versions ${CAPSULE_LOWEST_SUPPORTED_VERSIONS} \
+             --monotonic_counts ${CAPSULE_MONOTONIC_COUNTS} \
+             --payloads ${UEFI_FIRMWARE_BINARIES} \
+             --update_image_indexes ${CAPSULE_INDEXES} \
+             --private_keys ${CAPSULE_PRIVATE_KEY_PATHS} \
+             --certificates ${CAPSULE_CERTIFICATE_PATHS} \
+             --output ${CAPSULE_CONFIG_FILE}
+
+    # Force the GenerateCapsule script to use python3
+    export PYTHON_COMMAND=${PYTHON}
+
+    # Append the certificate to the private key to create a PEM bundle compatible with EDK2 tools
+    cat ${PAYLOAD_CERTIFICATE_PATH} >> ${PAYLOAD_PRIVATE_KEY_PATH}
+
+    # Generate the UEFI capsule image using the EDK2 GenerateCapsule tool
+    ${STAGING_BINDIR_NATIVE}/edk2-BaseTools/BinWrappers/PosixLike/GenerateCapsule \
+             -e -j ${CAPSULE_CONFIG_FILE} \
+             ${CAPSULE_EXTRA_ARGS} \
+             -o ${CAPSULE_IMG_LOCATION}/${CAPSULE_NAME}.${CAPSULE_EXTENSION}
 }
 
 # The firmware binary should be created before generating the capsule
diff --git a/meta-arm/conf/layer.conf b/meta-arm/conf/layer.conf
index 753f5259..8f7e1a43 100644
--- a/meta-arm/conf/layer.conf
+++ b/meta-arm/conf/layer.conf
@@ -21,3 +21,6 @@  HOSTTOOLS_NONFATAL += "telnet"
 addpylib ${LAYERDIR}/lib oeqa
 
 WARN_QA:append:layer-meta-arm = " patch-status"
+
+# Define base directory for meta-arm
+META_ARM_LAYER_DIR := "${LAYERDIR}"
diff --git a/meta-arm/scripts/generate_capsule_json_multiple.py b/meta-arm/scripts/generate_capsule_json_multiple.py
new file mode 100644
index 00000000..13425748
--- /dev/null
+++ b/meta-arm/scripts/generate_capsule_json_multiple.py
@@ -0,0 +1,142 @@ 
+# SPDX-FileCopyrightText: <text>Copyright 2025 Arm Limited and/or its
+# affiliates <open-source-office@arm.com></text>
+#
+# SPDX-License-Identifier: MIT
+
+"""
+Capsule Payload JSON Generator
+
+This script creates a JSON file that defines multiple capsule payloads.
+Each payload is constructed using command-line input and includes key
+metadata like firmware version, GUID, hardware instance, and more.
+
+Usage:
+    python generate_capsule_json_multiple.py \
+        --fw_versions 1 1 2 \
+        --guids guid1 guid2 guid3 \
+        --hardware_instances 1 1 1 \
+        --lowest_supported_versions 0 0 0 \
+        --monotonic_counts 1 1 1 \
+        --payloads bl2.bin initramfs.bin tfm_s.bin \
+        --update_image_indexes 1 4 2 \
+        --private_keys key.key key.key key.key \
+        --certificates cert.crt cert.crt cert.crt \
+        --components bl2 initramfs tfm_s \
+        --selected_components bl2 \
+        --output capsule_generation_config.json
+"""
+
+import json
+import argparse
+from typing import List
+
+
+def parse_arguments() -> argparse.Namespace:
+    """Parses command-line arguments."""
+    parser = argparse.ArgumentParser(
+        description="Generate a JSON file for multiple Capsule Payloads."
+    )
+
+    parser.add_argument(
+        "--selected_components", default=[], nargs="*", required=False,
+        help=(
+            "Filters the payloads to include only those for the selected "
+            "components (e.g., bl2, initramfs)."
+            "All components are included when not specified."
+        )
+    )
+
+    parser.add_argument(
+        "--output", default="capsule_payloads.json", help="Output JSON file name"
+    )
+
+    # Required arguments for each payload entry
+    required_args = {
+        "components": "List of components",
+        "fw_versions": "List of firmware versions",
+        "guids": "List of GUIDs",
+        "hardware_instances": "List of hardware instances",
+        "lowest_supported_versions": "List of lowest supported firmware versions",
+        "monotonic_counts": "List of monotonic counts",
+        "payloads": "List of payload file paths",
+        "update_image_indexes": "List of update image indexes",
+        "private_keys": "List of private key file paths",
+        "certificates": "List of certificate file paths",
+    }
+
+    for arg, desc in required_args.items():
+        parser.add_argument(f"--{arg}", nargs="+", required=True, help=desc)
+
+    return parser.parse_args()
+
+
+def validate_input_lengths(args: argparse.Namespace) -> None:
+    """Ensures all required input lists have the same length (excluding output and selected_components)."""
+    list_lengths = {
+        attr: len(getattr(args, attr))
+        for attr in vars(args)
+        if attr not in {"output", "selected_components"}  # Ignore optional fields
+    }
+
+    for attr, length in list_lengths.items():
+        if length == 0:
+            raise ValueError(f"Input list '{attr}' cannot be empty!")
+
+    if len(set(list_lengths.values())) != 1:
+        raise ValueError("All input lists must have the same length!")
+
+def create_payloads(args: argparse.Namespace) -> List[dict]:
+    """Generates the list of payload dictionaries to include in the final JSON."""
+    num_payloads = len(args.components)
+    selected_payloads = []
+
+    for i in range(num_payloads):
+
+        # If filtering is enabled, skip if not in the allowed components list
+        if  args.components[i] not in args.selected_components:
+            continue
+
+        payload = {
+            "Component": args.components[i],
+            "FwVersion": args.fw_versions[i],
+            "Guid": args.guids[i],
+            "HardwareInstance": args.hardware_instances[i],
+            "LowestSupportedVersion": args.lowest_supported_versions[i],
+            "MonotonicCount": args.monotonic_counts[i],
+            "Payload": args.payloads[i],
+            "UpdateImageIndex": args.update_image_indexes[i],
+            "OpenSslSignerPrivateCertFile": args.private_keys[i],
+            "OpenSslTrustedPublicCertFile": args.certificates[i],
+            "OpenSslOtherPublicCertFile": args.certificates[i],
+        }
+
+        selected_payloads.append(payload)
+
+    if not selected_payloads:
+        raise ValueError("None of the provided components match the selected_components list!")
+
+    return selected_payloads
+
+
+def write_json_file(output_path: str, payloads: List[dict]) -> None:
+    """Writes the list of payloads to a JSON file with basic error handling."""
+    try:
+        with open(output_path, "w", encoding="utf-8") as file:
+            json.dump({"Payloads": payloads}, file, indent=4)
+        print(f"JSON file created: {output_path}")
+    except (OSError) as e:
+        print(f"Failed to write JSON file to {output_path}: {e}")
+    except TypeError as e:
+        print(f"Invalid data format in payloads: {e}")
+
+
+def main() -> None:
+    """Main script entry point."""
+    args = parse_arguments()
+    validate_input_lengths(args)
+    payloads = create_payloads(args)
+    write_json_file(args.output, payloads)
+
+
+if __name__ == "__main__":
+    main()