1
0
mirror of https://git.yoctoproject.org/meta-arm synced 2026-06-05 14:30:10 +00:00

arm/uefi_capsule: Switch Capsule generation tool from U-Boot to EDK2

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>
Signed-off-by: Jon Mason <jon.mason@arm.com>
This commit is contained in:
Ali Can Ozaslan
2025-07-30 12:53:20 +01:00
committed by Jon Mason
parent 5d481fd065
commit c68fedfbea
3 changed files with 213 additions and 25 deletions
+68 -25
View File
@@ -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
+3
View File
@@ -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}"
@@ -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()