Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion classes-recipe/image_types_qcom.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ create_qcomflash_pkg() {
fi

# uefi dtb
if [ -n "${QCOM_UEFI_DTB}" ] && \
# Prefer the OEM-cert-injected uefi_dtbs deployed by
# firmware-qcom-oem-cert when available. Mirrors the xbl_config
# substitution above, for SPI-NOR-boot targets (e.g. hamoa) that
# carry QcCapsuleRootCert in uefi_dtbs.elf rather than
# xbl_config.elf.
if [ -n "${QCOM_CAPSULE_FIRMWARE}" ] && \
[ -f "${DEPLOY_DIR_IMAGE}/uefi_dtbs-with-oem-cert.xz" ]; then
install -m 0644 "${DEPLOY_DIR_IMAGE}/uefi_dtbs-with-oem-cert.xz" spinor/uefi_dtbs.xz
elif [ -n "${QCOM_UEFI_DTB}" ] && \
[ -f "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}/spinor/${QCOM_UEFI_DTB}" ]; then
install -m 0644 "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}/spinor/${QCOM_UEFI_DTB}" spinor/uefi_dtbs.xz
fi
Expand Down
107 changes: 16 additions & 91 deletions classes-recipe/qcom-capsule.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,15 @@ CAPSULE_ROOT_PUB ?= ""
CAPSULE_SUB_PUB ?= ""

# ---------------------------------------------------------------------------
# XBLConfig DTB certificate injection
# OEM root certificate injection
# ---------------------------------------------------------------------------
# The class automatically detects the post-DDR DTB by parsing the output of
# xblconfig_parser.py dump (looks for the first entry matching post-ddr*.dtb).
# Both the filename and the section index are extracted from the dump output.
#
# XBLCONFIG_DTB overrides auto-detection when set to an explicit filename.
# XBLCONFIG_DTB_SECTION overrides the auto-detected section index.
#
# When a post-DDR DTB is found (auto or explicit), the class will:
# 1. dump XBLConfig sections
# 2. patch QcCapsuleRootCert in the DTB with the converted root cert
# 3. re-pack the updated DTB back into xbl_config.elf
XBLCONFIG_DTB ?= ""
XBLCONFIG_DTB_SECTION ?= ""
# QcCapsuleRootCert is injected into the boot config ELFs by
# firmware-qcom-oem-cert (classes-recipe/qcom-oem-cert.bbclass), not here.
# It has to happen there because the certificate must be in place before
# the config ELFs are signed, and this recipe runs after the boot firmware
# has already been deployed. This class consumes the result: the
# cert-bearing copies are staged over the pristine boot binaries so the
# capsule firmware volume is built from the same images the device runs.

# ---------------------------------------------------------------------------
# Boot binaries location
Expand Down Expand Up @@ -89,7 +83,7 @@ inherit python3native deploy
CAPSULE_DIR = "${WORKDIR}/capsule_gen"

do_compile[depends] += "cbsp-boot-utilities-native:do_populate_sysroot \
edk2-basetools-native:do_populate_sysroot"
firmware-qcom-oem-cert:do_deploy"
do_compile[dirs] = "${CAPSULE_DIR}"
do_compile[cleandirs] = "${CAPSULE_DIR}"

Expand Down Expand Up @@ -204,68 +198,8 @@ python generate_fvupdate() {

do_compile[prefuncs] += "generate_fvupdate"

# Inject the OEM root certificate into xbl_config.elf.
# Dumps the config sections, auto-detects the post-DDR DTB (or uses
# XBLCONFIG_DTB / XBLCONFIG_DTB_SECTION overrides), patches QcCapsuleRootCert
# in that DTB, and repacks the updated DTB back into xbl_config.elf in place.
# $1 - path to xbl_config.elf (modified in place on success)
patch_xblconfig_cert() {
local xbl_config="$1"
local staged_dir
staged_dir=$(dirname "${xbl_config}")

XBL_DUMP_LOG="${CAPSULE_DIR}/xbl_dump.log"
qcom-capsule-tool parse-config \
"${xbl_config}" dump \
--out-dir "${staged_dir}" | tee "${XBL_DUMP_LOG}"

DTB_PATCH="${XBLCONFIG_DTB}"
DTB_SECTION="${XBLCONFIG_DTB_SECTION}"
if [ -z "${DTB_PATCH}" ]; then
# Parse a line like:
# [+] config_item[6] -> PH# 8 -> './post-ddr-kodiak-1.0.dtb' (90280 bytes)
POST_DDR_LINE=$(grep -m1 "post-ddr.*\.dtb" "${XBL_DUMP_LOG}" || true)
if [ -n "${POST_DDR_LINE}" ]; then
DTB_PATCH=$(echo "${POST_DDR_LINE}" | sed "s|.* -> '||;s|'.*||" | xargs basename)
DTB_SECTION=$(echo "${POST_DDR_LINE}" | sed "s/.*PH# \([0-9]*\).*/\1/")
fi
fi

if [ -n "${DTB_PATCH}" ]; then
ORIG_DTB="${staged_dir}/${DTB_PATCH}"
UPDATED_DTB="${staged_dir}/${DTB_PATCH%.dtb}-updated.dtb"

qcom-capsule-tool set-dtb-property \
"${ORIG_DTB}" \
/sw/uefi/uefiplat \
QcCapsuleRootCert \
"@list:${ROOT_INC}" \
"${UPDATED_DTB}"

qcom-capsule-tool parse-config \
"${xbl_config}" replace \
"${DTB_SECTION}" \
"${UPDATED_DTB}" \
"${staged_dir}/xbl_config_patched.elf"

mv "${staged_dir}/xbl_config_patched.elf" \
"${xbl_config}"

touch "${CAPSULE_DIR}/.xbl_with_oem_cert"
fi
}

do_compile() {
CBSP_DATA="${STAGING_DATADIR_NATIVE}/cbsp-boot-utilities"
EDK2_BASETOOLS="${STAGING_DATADIR_NATIVE}/edk2-basetools"

# GenFfs/GenFv are staged to ${STAGING_BINDIR_NATIVE} (in PATH) by
# upstream meta-arm's edk2-basetools-native and resolved by
# qcom-capsule-tool via shutil.which. GenerateCapsule.py and its
# Common/ Python package live under ${EDK2_BASETOOLS}; add that to
# PYTHONPATH so `import Common` works when we invoke the script
# directly below.
export PYTHONPATH="${EDK2_BASETOOLS}${PYTHONPATH:+:$PYTHONPATH}"

# Use a board-specific FvUpdate.xml if provided via SRC_URI:append or
# generated from CAPSULE_ENTRIES, otherwise fall back to the default
Expand All @@ -280,9 +214,6 @@ do_compile() {

cd "${CAPSULE_DIR}"

ROOT_INC="${CAPSULE_DIR}/QcFMPRoot.inc"
qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${ROOT_INC}"

# Stage boot binaries so they are writable (XBLConfig patching modifies
# xbl_config.elf in place)
BOOTBINS_STAGED="${CAPSULE_DIR}/bootbins"
Expand All @@ -299,10 +230,12 @@ do_compile() {
"${BOOTBINS_STAGED}/dtb.bin"
fi

# Inject OEM root cert into xbl_config.elf when present. Platforms
# without xbl_config.elf (e.g. hamoa) skip this step.
if [ -f "${BOOTBINS_STAGED}/xbl_config.elf" ]; then
patch_xblconfig_cert "${BOOTBINS_STAGED}/xbl_config.elf"
# Overlay the cert-bearing config ELFs deployed by
# firmware-qcom-oem-cert, so the capsule firmware volume carries the
# same images the device boots. Optional: hamoa has no xbl_config.elf.
if [ -f "${DEPLOY_DIR_IMAGE}/xbl_config-with-oem-cert.elf" ]; then
install -m 0644 "${DEPLOY_DIR_IMAGE}/xbl_config-with-oem-cert.elf" \
"${BOOTBINS_STAGED}/${QCOM_XBL_CONFIG}"
fi

qcom-capsule-tool sysfw-version-create \
Expand All @@ -327,7 +260,7 @@ do_compile() {
-oc "${CAPSULE_SUB_PUB}" \
-g "${CAPSULE_GUID}"

python3 "${EDK2_BASETOOLS}/GenerateCapsule.py" \
qcom-capsule-tool generate-capsule \
-e \
-j config.json \
-o "${PN}.cap" \
Expand All @@ -346,14 +279,6 @@ FILES:${PN} = "${nonarch_base_libdir}/firmware/efi/${PN}.cap"
do_deploy() {
install -d "${DEPLOYDIR}"
install -m 0644 "${CAPSULE_DIR}/${PN}.cap" "${DEPLOYDIR}/"

# When XBLConfig was injected with the OEM root cert, deploy the updated
# binary under a distinct name to avoid a deploy-manifest conflict with
# firmware-qcom-bootbins (which already owns xbl_config.elf).
if [ -f "${CAPSULE_DIR}/.xbl_with_oem_cert" ]; then
install -m 0644 "${CAPSULE_DIR}/bootbins/xbl_config.elf" \
"${DEPLOYDIR}/xbl_config-with-oem-cert.elf"
fi
}
addtask deploy before do_build after do_compile

Expand Down
138 changes: 138 additions & 0 deletions classes-recipe/qcom-oem-cert.bbclass
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#
# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause-Clear
#
# Inject the OEM capsule root certificate into the boot config ELFs.
#
# A recipe of its own, not part of qcom-capsule.bbclass, because the order
# is forced: the certificate must be in the config ELF before that ELF is
# signed (editing a DTB inside it invalidates any signature it carried),
# and the capsule built afterwards is verified against that same
# certificate. By the time the capsule recipe runs the boot firmware is
# already deployed and signed, so the injection cannot live there. Staging
# into ${OEM_CERT_STAGE} between do_compile and do_deploy leaves the seam a
# signing step needs.

# Shared with qcom-capsule.bbclass; ci/capsule-test-keys.yml sets it for CI.
CAPSULE_ROOT_CER ?= ""

BOOTBINS_DIR ?= "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}"

# Named for the signing step that belongs in this seam.
OEM_CERT_STAGE = "${B}/firmware-to-sign"

inherit python3native deploy

do_configure[noexec] = "1"
do_install[noexec] = "1"

do_compile[depends] += "qdte-lite-native:do_populate_sysroot \
cbsp-boot-utilities-native:do_populate_sysroot"
do_compile[depends] += "${@'${QCOM_BOOT_FIRMWARE}:do_deploy' if d.getVar('QCOM_BOOT_FIRMWARE') else ''}"
do_compile[cleandirs] = "${OEM_CERT_STAGE}"

python () {
if not d.getVar('CAPSULE_ROOT_CER'):
raise bb.parse.SkipRecipe(
'%s: CAPSULE_ROOT_CER is not set. Point it at the DER-encoded OEM '
'root certificate (see ci/capsule-test-keys.yml for a '
'CI/development overlay).' % d.getVar('PN'))
}

# Inject the certificate into one boot config ELF.
#
# The DTB names are asked for rather than configured per machine: they are
# assigned during disassembly, from container metadata in one container and
# from each DTB's /compatible in another, so hardcoding them goes stale.
# More than one line is normal -- on hamoa the property appears in a base
# DTB and in a .dtbo overlay, at different node paths -- and the ops are
# joined with '&' so a single pass applies them all.
#
# bin-to-hex owns the DER-to-cells conversion so the padding of a trailing
# partial cell lives in one place rather than being reimplemented here.
#
# $1 - path to the config ELF or its .xz (rewritten in place)
patch_config_elf_cert() {
local config_elf="$1"

local stem
stem=$(basename "${config_elf}")
stem="${stem%.xz}"
stem="${stem%.elf}"

local targets
targets=$(qdte-lite --nogui --input_file "${config_elf}" \
--find_property QcCapsuleRootCert) || {
bbwarn "No DTB in ${stem} defines QcCapsuleRootCert; skipping OEM cert injection."
return
}

local root_inc="${B}/QcFMPRoot.inc"
qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${root_inc}"

local modify_arg="" target
for target in ${targets}; do
if [ -z "${modify_arg}" ]; then
modify_arg="${target}=@list:${root_inc}"
else
modify_arg="${modify_arg}&${target}=@list:${root_inc}"
fi
done

local outdir="${B}/qdte_out/${stem}"
rm -rf "${outdir}"
mkdir -p "${outdir}"

qdte-lite --nogui \
--input_file "${config_elf}" \
--output_path "${outdir}" \
--output_file "${stem}.elf" \
--modify "${modify_arg}"

# qdte-lite always writes a plain ELF; restore the input's compression.
case "${config_elf}" in
*.xz) xz -c "${outdir}/${stem}.elf" > "${config_elf}" ;;
*) install -m 0644 "${outdir}/${stem}.elf" "${config_elf}" ;;
esac
}

do_compile() {
install -d "${OEM_CERT_STAGE}"

# QCOM_XBL_CONFIG is xbl_config_kvm.elf on kvm machines. Both ELFs are
# optional: hamoa has no XBLConfig, UFS-boot platforms no uefi_dtbs.xz.
if [ -f "${BOOTBINS_DIR}/${QCOM_XBL_CONFIG}" ]; then
install -m 0644 "${BOOTBINS_DIR}/${QCOM_XBL_CONFIG}" "${OEM_CERT_STAGE}/"
patch_config_elf_cert "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}"
fi

# uefi_dtbs.xz can sit in a SPI-NOR subdirectory of the boot bins.
UEFI_DTBS_XZ=$(find "${BOOTBINS_DIR}" -name "uefi_dtbs.xz" -print -quit)
if [ -n "${UEFI_DTBS_XZ}" ]; then
install -m 0644 "${UEFI_DTBS_XZ}" "${OEM_CERT_STAGE}/"
patch_config_elf_cert "${OEM_CERT_STAGE}/uefi_dtbs.xz"
fi

if [ -z "$(ls -A ${OEM_CERT_STAGE} 2>/dev/null)" ]; then
bbfatal "No boot config ELF carrying QcCapsuleRootCert was found under ${BOOTBINS_DIR}."
fi
}

do_deploy() {
install -d "${DEPLOYDIR}"

# Fixed deploy name whatever the machine calls its XBLConfig, and
# distinct from it so this does not collide in the deploy manifest with
# the boot firmware recipe that owns the unmodified copy.
if [ -f "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}" ]; then
install -m 0644 "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}" \
"${DEPLOYDIR}/xbl_config-with-oem-cert.elf"
fi

if [ -f "${OEM_CERT_STAGE}/uefi_dtbs.xz" ]; then
install -m 0644 "${OEM_CERT_STAGE}/uefi_dtbs.xz" \
"${DEPLOYDIR}/uefi_dtbs-with-oem-cert.xz"
fi
}
addtask deploy before do_build after do_compile

This file was deleted.

14 changes: 14 additions & 0 deletions recipes-bsp/firmware-boot/firmware-qcom-oem-cert_1.0.bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
DESCRIPTION = "Boot config ELFs carrying the OEM capsule root certificate"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

COMPATIBLE_MACHINE = "hamoa|qcm6490|qcs615|qcs8300|qcs9100"

# Deploy-only. PACKAGES = "" would leave do_package running with nothing to
# split, and buildhistory then fails listing a packages-split that was
# never created.
inherit nopackages

inherit qcom-oem-cert

PACKAGE_ARCH = "${MACHINE_ARCH}"
29 changes: 29 additions & 0 deletions recipes-bsp/firmware/firmware-qcom-capsule_%.bbappend
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CAPSULE_FLASH_TYPE:iq-x7181-evk = "NORUFS"
CAPSULE_ENTRIES:iq-x7181-evk = "dtb"

# Hamoa keeps the Linux DTB FIT image in SPINOR, with dtb as the active
# partition and dtb_BACKUP a rollback copy written before it.
#
# These are varflags, and varflags take no part in override resolution --
# CAPSULE_ENTRY_dtb[dest_disk]:iq-x7181-evk does not exist. Assigned
# plainly they would apply everywhere, so any other board declaring a "dtb"
# entry would silently inherit these SPINOR destinations. Guard on
# MACHINEOVERRIDES to get the scope the override was meant to give.
QCOM_CAPSULE_DTB_ENTRY_MACHINE ?= "iq-x7181-evk"

python () {
machine = d.getVar('QCOM_CAPSULE_DTB_ENTRY_MACHINE')
if machine not in (d.getVar('MACHINEOVERRIDES') or '').split(':'):
return

for flag, value in (
('binary', 'dtb.bin'),
('dest_disk', 'SPINOR'),
('dest_partition', 'dtb'),
('dest_guid', '{2A1A52FC-AA0B-401C-A808-5EA0F91068F8}'),
('backup_disk', 'SPINOR'),
('backup_partition', 'dtb_BACKUP'),
('backup_guid', '{A166F11A-2B39-4FAA-B7E7-F8AA080D0587}'),
):
d.setVarFlag('CAPSULE_ENTRY_dtb', flag, value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ LICENSE = "BSD-3-Clause-Clear"
LIC_FILES_CHKSUM = "file://LICENSE;md5=8e1eb38e3de3966193d29f31f5d7e684"

SRC_URI = "git://github.com/quic/cbsp-boot-utilities.git;protocol=https;branch=main"
SRCREV = "a19e5b6f75cd4aa08aa5ced82f9767f1858d766d"
SRCREV = "8a0f1deef97beae600910506bfba488976465828"

S = "${UNPACKDIR}/${BPN}-${PV}/uefi_capsule_generation"

inherit python_poetry_core native

# edk2-basetools is no longer needed: the tool now carries its own
# GenFfs/GenFv (fv_builder.py) and a generate-capsule that is a drop-in for
# edk2 BaseTools GenerateCapsule.py, producing identical bytes. requests
# went with the setup step that used to fetch them.
DEPENDS += " \
dtc-native \
edk2-basetools-native \
python3-dtc-native \
python3-pyelftools-native \
python3-requests-native \
"

# FvUpdate.xml ships alongside pyproject.toml (not inside the Python
Expand Down
Loading
Loading