Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
14 changes: 14 additions & 0 deletions ci/ecdsa-secure-boot-test-keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siemens/kas/master/kas/schema-kas.json
#
# Include this kas file to enable test ECDSA PKI keys for Qualcomm
# secure-boot image signing. These keys are for CI / development use
# only and must NOT be used in production images.
#
# Usage: kas build ci/base.yml:ci/<machine>.yml:ci/secure-boot.yml:ci/ecdsa-secure-boot-test-keys.yml

header:
version: 14

local_conf_header:
ecdsa-secure-boot-test-keys: |
QCOM_FIRMWARE_SIGN_KEY_DIR = "${LAYERDIR_qcom}/ci/test-keys/ecdsa"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

57 changes: 57 additions & 0 deletions ci/secure-boot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siemens/kas/master/kas/schema-kas.json
#
# Include this kas file to enable Qualcomm secure-boot signing of
# pre-built firmware images via classes-recipe/qcom-firmware-sign.bbclass.
# Wires the build through to sectools-native and security-profiles-native
# and supplies the fuse-binding parameters used by sectools at sign time.
#
# Usage:
# kas build ci/base.yml:ci/<machine>.yml:ci/secure-boot.yml:ci/ecdsa-secure-boot-test-keys.yml
#
# For production builds, replace ecdsa-secure-boot-test-keys.yml with
# a kas overlay that sets QCOM_FIRMWARE_SIGN_KEY_DIR to an OEM key
# directory (with qpsa_rootca0.cer / qpsa_attestca0.cer /
# qpsa_attestca0.key / sha384_roots_hash.txt) sourced from a secrets
# manager rather than the in-tree test fixtures.

header:
version: 14

# meta-oe provides libzip, which qdl-native links against (besides
# libusb-1.0 and libxml-2.0 from oe-core). meta-python is not pulled
# in; we have no Python-only dependency.
repos:
meta-openembedded:
url: https://github.com/openembedded/meta-openembedded
layers:
meta-oe:

local_conf_header:
secure-boot: |
QCOM_FIRMWARE_SIGN_ENABLE = "1"
# Per-SoC Security Profile XML. Bare filenames are resolved
# against the directory staged by security-profiles-native; an
# absolute path can be used to point at a custom profile.
QCOM_SECURITY_PROFILE ?= "kodiak_security_profile.xml"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile should probably come from the board conf.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, will do

QCOM_FIRMWARE_SIGN_SECPROFILE = "${QCOM_SECURITY_PROFILE}"
# Fuse-binding identifiers. These are device-specific; the values
# below are placeholders for development. Override per OEM /
# production build.
QCOM_FUSE_OEM_HW_ID = "0x0001"
QCOM_FUSE_OEM_PRODUCT_ID = "0xabcd"
QCOM_FUSE_SEC_KEY_DERIVATION_KEY = ""
# Anti-rollback floor stamped into signed images. Bump only when
# rolling forward security-critical firmware revisions.
QCOM_FIRMWARE_SIGN_ANTI_ROLLBACK = "0x0"
# Enable the qcomflash-vip image type. Produces an additional
# ${IMAGE_NAME}.qcomflash-vip.tar.gz next to the qcomflash tarball
# containing the signed VIP (Validated Image Programming) digest
# table -- intended to be flashed alongside qcomflash via
# `qdl --vip-table-path`. See classes-recipe/image_types_qcom_vip.bbclass
# and recipes-devtools/qdl/qdl-native_2.7.bb.
IMAGE_CLASSES += "image_types_qcom_vip"
IMAGE_FSTYPES += "qcomflash-vip"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate class and separate image type?
Also the class hasn't been seen up to now, so this commit breaks the build. Please reoder them.

Binary file added ci/test-keys/ecdsa/qpsa_attestca0.cer
Binary file not shown.
9 changes: 9 additions & 0 deletions ci/test-keys/ecdsa/qpsa_attestca0.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split this commit in 2, have one dedicated just for the keys.

BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDD0kj7dDRzPghNxNjknJC68swgJH/XGdBVinbVPoWjlEV95Ipr3y6E4
RNF7IU3lT2KgBwYFK4EEACKhZANiAASnFtGeU9cEUA3+kBdiUyP7wDL4HUTgzZoo
KBS+aGWZ0F04+lcPxpKfxGXv3LGJ1Ucbusc2TsijfPsuWoP9YhRnb0bdpvsarUDG
mCuFPv9+lABY30VuKyfVMR0iNGQhQlY=
-----END EC PRIVATE KEY-----
Binary file added ci/test-keys/ecdsa/qpsa_rootca0.cer
Binary file not shown.
1 change: 1 addition & 0 deletions ci/test-keys/ecdsa/sha384_roots_hash.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHA2-384(qpsa_rootca0.cer)= 9bc289bd255ec63e5752d15c15fa64d2f1ec0dd78e0438e5ffea498ae16d0aada49491eadaefe782fe3f91d147a540d3
257 changes: 257 additions & 0 deletions classes-recipe/qcom-firmware-sign.bbclass
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#
# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause-Clear
#

QCOM_FIRMWARE_SIGN_ENABLE ??= "0"

# Sectools binary -- defaults to the one staged by sectools-native.
QCOM_FIRMWARE_SIGN_SECTOOLS ?= "${STAGING_BINDIR_NATIVE}/sectools"

QCOM_FIRMWARE_SIGN_SECPROFILE ?= ""
QCOM_FIRMWARE_SIGN_SECPROFILE_DIR ?= "${STAGING_DATADIR_NATIVE}/qcom-security-profiles"

# Directory containing the OEM signing material:
# qpsa_rootca0.cer -- root certificate
# qpsa_attestca0.cer -- attestation CA certificate
# qpsa_attestca0.key -- attestation CA private key
# sha384_roots_hash.txt -- root hash for verification
# Tests use ci/test-keys/ecdsa via ci/ecdsa-secure-boot-test-keys.yml.
QCOM_FIRMWARE_SIGN_KEY_DIR ?= ""

# Fuse identifiers, passed through to consumers / signing wrappers.
QCOM_FUSE_OEM_HW_ID ?= ""
QCOM_FUSE_OEM_PRODUCT_ID ?= ""
QCOM_FUSE_SEC_KEY_DERIVATION_KEY ?= ""

# Anti-rollback version embedded in the signed images.
QCOM_FIRMWARE_SIGN_ANTI_ROLLBACK ?= "0x0"

# Pull in the build-host signing helpers only when signing is enabled.
DEPENDS += "${@bb.utils.contains('QCOM_FIRMWARE_SIGN_ENABLE', '1', \
'sectools-native security-profiles-native', '', d)}"

# Mapping from common firmware filenames to sectools image-id labels.
QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP ??= "\
a660_zap.mbn:GPU-MICRO-CODE \
adsp.mbn:ADSP \
aop.mbn:AOP \
cdsp.mbn:CDSP \
cpucp.elf:CPUCP \
devcfg.mbn:TZ-DEVCFG \
hypvm.mbn:QHEE \
imagefv.elf:UEFIFV \
ipa_fws.mbn:IPA-FW \
loadalgota64.mbn:TZ-APP-OEM \
msbtfw11.mbn:SKIP \
multi_image.mbn:OEM-MISC \
prog_firehose_ddr.elf:DEVICE-PROGRAMMER \
prog_firehose_lite.elf:DEVICE-PROGRAMMER \
qupv3fw.elf:QUPV3 \
sec.elf:SEC-ELF \
shrm.elf:SHRM \
tz.mbn:TZ \
uefi.elf:UEFI \
uefi_sec.mbn:TZ-APP-OEM \
vpu20_1v.mbn:VENUS-FW \
vpu30_4v.mbn:VENUS-FW \
vpu30_4v_16mb.mbn:VENUS-FW \
wpss.mbn:WPSS \
xbl_config.elf:XBL-CONFIG \
xbl_config_gunyah.elf:XBL-CONFIG \
xbl_config_kvm.elf:XBL-CONFIG \
xbl.elf:XBL \
XblRamdump.elf:XBL-RAM-DUMP \
DigestsToSign.bin.mbn:VIP \
FD02C9DA-306C-48C7-A49C-BBD827AE86EE.mbn:TZ-APP-OEM \
"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to an inc file or similar? This is just metadata for the class.


# Internal helper: print the absolute path of the security profile XML
# (resolving QCOM_FIRMWARE_SIGN_SECPROFILE bare filenames against
# QCOM_FIRMWARE_SIGN_SECPROFILE_DIR), or fail with bbfatal.
qcom_sign_resolve_secprofile() {
profile="${QCOM_FIRMWARE_SIGN_SECPROFILE}"
if [ -z "${profile}" ]; then
bbfatal "QCOM_FIRMWARE_SIGN_SECPROFILE is empty -- set it to a profile filename (e.g. kodiak_security_profile.xml) or to an absolute path."
fi
case "${profile}" in
/*) printf '%s\n' "${profile}" ;;
*) printf '%s/%s\n' "${QCOM_FIRMWARE_SIGN_SECPROFILE_DIR}" "${profile}" ;;
esac
}

# Verifies that everything the signing step needs exists before the
# task actually runs -- avoids cryptic sectools errors deep in the
# signing pipeline.
qcom_check_signing_enabled() {
if [ "${QCOM_FIRMWARE_SIGN_ENABLE}" != "1" ]; then
return 1
fi
if [ ! -x "${QCOM_FIRMWARE_SIGN_SECTOOLS}" ]; then
bbfatal "QCOM_FIRMWARE_SIGN_SECTOOLS ('${QCOM_FIRMWARE_SIGN_SECTOOLS}') is missing or not executable."
fi
secprofile="$(qcom_sign_resolve_secprofile)"
if [ ! -f "${secprofile}" ]; then
bbfatal "Security profile not found: ${secprofile}"
fi
if [ ! -d "${QCOM_FIRMWARE_SIGN_KEY_DIR}" ]; then
bbfatal "QCOM_FIRMWARE_SIGN_KEY_DIR ('${QCOM_FIRMWARE_SIGN_KEY_DIR}') is not a directory."
fi
for f in qpsa_rootca0.cer qpsa_attestca0.cer qpsa_attestca0.key sha384_roots_hash.txt; do
if [ ! -f "${QCOM_FIRMWARE_SIGN_KEY_DIR}/${f}" ]; then
bbfatal "Required key file missing: ${QCOM_FIRMWARE_SIGN_KEY_DIR}/${f}"
fi
done
return 0
}

# Look up the sectools image-id for a given firmware filename in
# QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP. Prints the id (or "SKIP" / "")
# to stdout; never fails.
qcom_sign_lookup_image_id() {
basename="$1"
for entry in ${QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP}; do
case "${entry}" in
"${basename}:"*) printf '%s\n' "${entry#*:}"; return 0 ;;
esac
done
printf '\n'
}

# Sign a single file in place. $1 is the absolute path; any non-zero
# sectools exit triggers a bbfatal in the caller. The primary signal
# for "is this file signable" is the filename->image-id map -- the
# upstream Qualcomm reference binaries (qcm6490_bootbinaries.1.0-
# test-device-public et al.) arrive WITHOUT a pre-existing signature,
# so checking `--inspect` for a Software ID skips everything.
qcom_sign_only_file() {
file_path="$1"
file_name="$(basename "${file_path}")"
secprofile="$(qcom_sign_resolve_secprofile)"

image_id="$(qcom_sign_lookup_image_id "${file_name}")"
if [ -z "${image_id}" ]; then
bbdebug 1 "${file_name}: not in image-id map, skipping"
return 0
fi
case "${image_id}" in
SKIP|SKIP:*)
bbnote "${file_name}: skipping (mapped to ${image_id})"
return 0
;;
esac

# Validate the image-id against the active security profile. Each
# profile only declares a subset of the global image-id namespace,
# so skip-with-warn rather than fail for out-of-scope ids.
#
# sectools v1.48 prints the list as a numbered enumeration, e.g.
# Available Image IDs:
# 1. ABL
# 2. ACPI
# ...
# 44. XBL
# 45. XBL-CONFIG
if ! "${QCOM_FIRMWARE_SIGN_SECTOOLS}" secure-image --available-image-ids \
--security-profile "${secprofile}" 2>/dev/null \
| sed -nE 's/^[[:space:]]*[0-9]+\.[[:space:]]*([A-Za-z][A-Za-z0-9_-]*)[[:space:]]*$/\1/p' \
| grep -Fqx "${image_id}"; then
bbwarn "${file_name}: image-id '${image_id}' is not valid for the active security profile, skipping"
return 0
fi

bbnote "sectools sign ${file_name} as ${image_id}"
pre_sha="$(sha256sum "${file_path}" | cut -d' ' -f1)"
"${QCOM_FIRMWARE_SIGN_SECTOOLS}" secure-image \
--sign "${file_path}" \
--image-id="${image_id}" \
--security-profile "${secprofile}" \
--anti-rollback-version="${QCOM_FIRMWARE_SIGN_ANTI_ROLLBACK}" \
--signing-mode LOCAL \
--root-certificate-index 0 \
--root-certificate="${QCOM_FIRMWARE_SIGN_KEY_DIR}/qpsa_rootca0.cer" \
--ca-certificate="${QCOM_FIRMWARE_SIGN_KEY_DIR}/qpsa_attestca0.cer" \
--ca-key="${QCOM_FIRMWARE_SIGN_KEY_DIR}/qpsa_attestca0.key" \
--outfile "${file_path}" \
|| bbfatal "Signing ${file_name} failed"

# sectools' --sign returns 0 for non-fatal "I don't know how to sign
# this" cases (e.g. plain ELF passed in for an image-id whose profile
# entry expects MBN-V6) and produces a byte-identical output instead
# of erroring out. Detect that explicitly so silent no-ops surface as
# warnings instead of being caught downstream by a confusing
# verify-root failure.
post_sha="$(sha256sum "${file_path}" | cut -d' ' -f1)"
if [ "${pre_sha}" = "${post_sha}" ]; then
bbwarn "${file_name}: sectools --sign left the file byte-identical when signing as '${image_id}' -- the input does not appear to be in the format that image-id expects. Mark this entry as 'SKIP:<reason>' in QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP if this is intentional."
fi
}

# Sign + verify-root in one step. Use this for files where the active
# security profile permits OEM-only signatures (i.e. does NOT have
# <oem_vouch_for_disallowed/> on the image entry). For files where the
# profile requires hybrid OEM+QTI vouching (e.g. VIP under the upstream
# github security-profiles repo), call qcom_sign_only_file() instead --
# sectools' verify-root rejects OEM-only signatures against those
# profile entries even though the boot ROM accepts them.
qcom_sign_verify_file() {
file_path="$1"
file_name="$(basename "${file_path}")"

pre_sha="$(sha256sum "${file_path}" | cut -d' ' -f1)"
qcom_sign_only_file "$1" || return $?
post_sha="$(sha256sum "${file_path}" | cut -d' ' -f1)"

# If sign was a no-op (file not in map, mapped to SKIP, image-id
# rejected by the profile, or sectools silently refused -- the warn
# case in qcom_sign_only_file) there is nothing to verify. Skipping
# the verify-root is correct: running it against unchanged bytes
# would fail with a confusing "infile is not signed by OEM" error.
if [ "${pre_sha}" = "${post_sha}" ]; then
bbdebug 1 "${file_name}: unchanged after sign attempt, skipping verify-root"
return 0
fi

root_hash="0x$(cut -d' ' -f2 "${QCOM_FIRMWARE_SIGN_KEY_DIR}/sha384_roots_hash.txt")"
"${QCOM_FIRMWARE_SIGN_SECTOOLS}" secure-image --verify-root "${root_hash}" "${file_path}" \
|| bbfatal "Root hash verification of ${file_name} failed"
}

# Default do_qcom_firmware_sign: walks ${B}/firmware-to-sign and signs
# every *.mbn / *.elf in it. Consumers normally override this to point
# at the actual directory holding their images (e.g. ${D}/ for recipes
# using firmware_install, or ${S}/ for archive-unpacked recipes).
do_qcom_firmware_sign() {
if ! qcom_check_signing_enabled ; then
return 0
fi

sign_root="${B}/firmware-to-sign"
if [ ! -d "${sign_root}" ]; then
bbnote "qcom-firmware-sign: ${sign_root} does not exist, nothing to do."
return 0
fi

bbnote "qcom-firmware-sign: signing MBN/ELF under ${sign_root}"
find "${sign_root}" -type f \( -iname '*.mbn' -o -iname '*.elf' \) | while read -r f; do
qcom_sign_verify_file "${f}" || bbfatal "Failed to sign: ${f}"
done
}

do_qcom_firmware_sign[vardeps] += "\
QCOM_FIRMWARE_SIGN_ENABLE \
QCOM_FIRMWARE_SIGN_KEY_DIR \
QCOM_FIRMWARE_SIGN_SECPROFILE \
QCOM_FIRMWARE_SIGN_SECPROFILE_DIR \
QCOM_FIRMWARE_SIGN_ANTI_ROLLBACK \
QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP \
QCOM_FUSE_OEM_HW_ID \
QCOM_FUSE_OEM_PRODUCT_ID \
QCOM_FUSE_SEC_KEY_DERIVATION_KEY \
"

# Run before do_deploy (for recipes that ship signed blobs via DEPLOYDIR,
# e.g. firmware-qcom-boot-common.inc) and before do_package (for recipes
# that ship via package_install into /lib/firmware, e.g.
# firmware-qcom.inc / firmware-qcom-hlosfw style consumers)
addtask qcom_firmware_sign before do_deploy before do_package after do_install

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again :-)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing EOL

17 changes: 17 additions & 0 deletions recipes-bsp/firmware-boot/firmware-qcom-boot-common.inc
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,21 @@ do_deploy() {
}
addtask deploy before do_build after do_install

# Optional secure-boot signing of pre-built MBN/ELF/MELF images.
# When QCOM_FIRMWARE_SIGN_ENABLE is "0" (default) this is a no-op
inherit qcom-firmware-sign

do_qcom_firmware_sign() {
if ! qcom_check_signing_enabled ; then
return 0
fi

bbnote "qcom-firmware-sign: walking ${S} for MBN/ELF/MELF blobs"
find "${S}" -maxdepth 1 -type f \
\( -iname '*.mbn' -o -iname '*.elf' -o -iname '*.melf' \) \
| while read -r f; do
qcom_sign_verify_file "${f}" || bbfatal "Failed to sign: ${f}"
done
}

inherit allarch
17 changes: 17 additions & 0 deletions recipes-bsp/firmware/firmware-qcom.inc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ do_install:append() {
done
}

# Optional secure-boot signing of pre-built MBN/ELF blobs. When
# QCOM_FIRMWARE_SIGN_ENABLE is "0" (the default) this is a no-op
inherit qcom-firmware-sign

do_qcom_firmware_sign() {
if ! qcom_check_signing_enabled ; then
return 0
fi

bbnote "qcom-firmware-sign: walking ${D}${FW_QCOM_BASE_PATH} for MBN/ELF blobs"
find "${D}${FW_QCOM_BASE_PATH}" -type f \
\( -iname '*.mbn' -o -iname '*.elf' \) \
| while read -r f; do
qcom_sign_verify_file "${f}" || bbfatal "Failed to sign: ${f}"
done
}

INHIBIT_PACKAGE_DEBUG_SPLIT = "1"
INHIBIT_PACKAGE_STRIP = "1"
INHIBIT_DEFAULT_DEPS = "1"
Expand Down
Loading