Skip to content

[RFC] qcom-firmware-sign: build-time secure-boot signing of Qualcomm firmware#2598

Open
Igor Opaniuk (igoropaniuk) wants to merge 10 commits into
qualcomm-linux:masterfrom
igoropaniuk:feat/secure-boot-signing
Open

[RFC] qcom-firmware-sign: build-time secure-boot signing of Qualcomm firmware#2598
Igor Opaniuk (igoropaniuk) wants to merge 10 commits into
qualcomm-linux:masterfrom
igoropaniuk:feat/secure-boot-signing

Conversation

@igoropaniuk

@igoropaniuk Igor Opaniuk (igoropaniuk) commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This RFC adds a build-time secure-boot signing pipeline for Qualcomm firmware. It introduces three -native recipes - sectools-native, security-profiles-native, and qdl-native - together with a qcom-firmware-sign.bbclass that any recipe can inherit to sign its deploy artefacts before they ship.

A development ECDSA test-key set and a ci/secure-boot.yml kas overlay are included so a fresh checkout can exercise the full pipeline end-to-end without sourcing OEM signing material.

Taking into account that qualcomm/security-profiles is still private, it can be tested with (so build likely to fail until the rep is opened):

# ci/security-profiles-local.yml
header:
  version: 14

local_conf_header:
  security-profiles-local: |
    PREMIRRORS:prepend = "git://github.com/qualcomm/security-profiles.git.* git://${QCOM_SECURITY_PROFILES_LOCAL};protocol=file \n"

To test this build:

kas build ../ci/rb3gen2-core-kit.yml:../ci/secure-boot.yml:../ci/ecdsa-secure-boot-test-keys.yml:../ci/security-profiles-local.yml

When this feature is enabled it generates:

  • Signed boot binaries in deploy/.../<machine>/, for example deploy/images/rb3gen2-core-kit/qcm6490
  • The qcomflash-vip.tar.gz, which contains the full qcomflash artifact set under <image>-<machine>/ plus a vip-tables/DigestsToSign.bin.mbn OEM-signed via the same key chain. The package is ready to be flashed with QDL to the secured device with VIP engaged.

Add a -native recipe that fetches the per-chipset Security Profile
XML files from https://github.com/qualcomm/security-profiles and
stages them under ${datadir}/qcom-security-profiles/ so they can be
resolved by classes-recipe/qcom-firmware-sign.bbclass via the bare
filename in QCOM_FIRMWARE_SIGN_SECPROFILE.

The upstream repository is currently private; fetching it requires a
GitHub credential that has SAML-SSO authorised the qualcomm
organisation (gh auth login + browser flow).  This restriction is
expected to lift soon, at which point this note becomes obsolete.

License is BSD-3-Clause-Clear, anchored to the upstream LICENSE.txt.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add a -native recipe that fetches Qualcomm Sectools v2 from the
Software Center (1.48.0) and stages the per-platform `sectools`
binary under ${datadir}/sectools/, with a symlink in ${bindir} so
consumers can invoke `sectools` from PATH irrespective of build-host
architecture.

The build-host architecture override selects the matching tree from
the upstream zip (Linux x86_64 by default; Linux_aarch64 on aarch64
hosts).  Other platforms shipped in the zip (macOS, Windows) are
intentionally not installed.

The Software Center download is anonymously accessible (verified by
plain wget GET against the published 1.48.0 URL); BitBake's HTTP
fetcher works without any pre-staging step.  The SHA256 is pinned to
the released 1.48.0 archive.

LIC_FILES_CHKSUM is anchored to the in-archive CHANGES.txt because
the zip ships no LICENSE file; LICENSE.qcom-2 is provided via the
layer-local licenses directory through LICENSE_PATH (see
conf/layer.conf).

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add a build-time signing class that wraps sectools-native and
security-profiles-native, modelled on the meta-partner approach in
foundriesio/meta-partner#12.

The class adds a do_qcom_firmware_sign task between do_install and
do_deploy.  When QCOM_FIRMWARE_SIGN_ENABLE is "0" (the default) the
task is a no-op and no extra DEPENDS are pulled in -- consumers can
inherit unconditionally without affecting non-signing builds.

When enabled, do_qcom_firmware_sign:
  * walks ${B}/firmware-to-sign for *.mbn / *.elf files (or whatever
    the recipe overrides the default with);
  * looks each filename up in QCOM_FIRMWARE_SIGN_IMAGE_ID_MAP to
    obtain the sectools image-id label;
  * invokes `sectools secure-image --sign ...` with the OEM root /
    attestation CA cert + key from QCOM_FIRMWARE_SIGN_KEY_DIR and
    the per-SoC profile resolved from QCOM_FIRMWARE_SIGN_SECPROFILE;
  * verifies the resulting blob's root hash against the OEM hash file.

Bare filenames in QCOM_FIRMWARE_SIGN_SECPROFILE resolve against the
native datadir staged by security-profiles-native; absolute paths
are passed through unchanged so OEM-specific profiles can override
the in-tree set.

Fuse-binding identifiers (QCOM_FUSE_OEM_HW_ID,
QCOM_FUSE_OEM_PRODUCT_ID, QCOM_FUSE_SEC_KEY_DERIVATION_KEY) are
declared here so they participate in task signature hashing; the
actual fuse-blower invocation lives in downstream consumers (and
the qcom-sec-tools shell pipeline).

Image-id lookup uses a space-separated `filename:imageid` map (also
overridable from a recipe) to keep the SoC-specific knowledge in
data rather than code.  Unknown filenames trigger a bbfatal so new
firmware blobs are noticed at build time rather than silently shipped
unsigned.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add the development ECDSA PKI material and a kas overlay that wires
qcom-firmware-sign.bbclass at it, so a fresh checkout can exercise
the firmware-sign path end-to-end without sourcing OEM signing
material.

  * ci/test-keys/ecdsa/ -- the four fixtures consumed by
    qcom-firmware-sign.bbclass:
      qpsa_rootca0.cer       (root certificate)
      qpsa_attestca0.cer     (attestation CA certificate)
      qpsa_attestca0.key     (attestation CA key)
      sha384_roots_hash.txt  (root hash for sectools --verify-root)
    These are DEVELOPMENT KEYS ONLY and must never appear in
    production builds.

  * ci/ecdsa-secure-boot-test-keys.yml -- mirrors the existing
    ci/capsule-test-keys.yml pattern; points
    QCOM_FIRMWARE_SIGN_KEY_DIR at the in-tree test fixtures so the
    next commit's secure-boot.yml can be combined with it:
      kas build ci/base.yml:ci/<machine>.yml:\
                ci/secure-boot.yml:\
                ci/ecdsa-secure-boot-test-keys.yml

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add ci/secure-boot.yml, the main kas overlay that turns the
firmware-sign pipeline on:

  * Flips QCOM_FIRMWARE_SIGN_ENABLE to "1", so the no-op default in
    qcom-firmware-sign.bbclass is replaced with a real sectools
    invocation.
  * Defaults QCOM_FIRMWARE_SIGN_SECPROFILE to the Kodiak Security
    Profile, resolved against the directory staged by
    security-profiles-native.
  * Provides placeholder values for the fuse-binding identifiers
    (QCOM_FUSE_OEM_HW_ID, QCOM_FUSE_OEM_PRODUCT_ID,
    QCOM_FUSE_SEC_KEY_DERIVATION_KEY) and the anti-rollback floor
    (QCOM_FIRMWARE_SIGN_ANTI_ROLLBACK).  Production builds override
    these per OEM.
  * Pulls meta-oe from meta-openembedded for libzip-native, the qdl
    build-host dep used by the qcomflash-vip image type.

To bind the build to actual key material the user combines this
overlay with one of:

  * ci/ecdsa-secure-boot-test-keys.yml -- in-tree development fixtures
    (added in the previous commit).
  * An OEM-specific overlay pointing QCOM_FIRMWARE_SIGN_KEY_DIR at
    production keys sourced from a secrets manager.

Typical CI / dev invocation:
  kas build ci/base.yml:ci/<machine>.yml:\
            ci/secure-boot.yml:\
            ci/ecdsa-secure-boot-test-keys.yml

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add `inherit qcom-firmware-sign` plus a recipe-appropriate
do_qcom_firmware_sign override so the build-time signing class
actually runs against the SoC boot binaries every
firmware-qcom-boot-*.bb recipe ships.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add `inherit qcom-firmware-sign` plus a recipe-appropriate
do_qcom_firmware_sign override so HLOS firmware shipped through
the linux-firmware-qcom-* split packages gets signed alongside
the boot binaries.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Remove recipes-devtools/qdl/qdl_git.bb.  The recipe has not been
functionally maintained, is pinned to a SRCREV from before qdl
switched its build system from Makefile to meson, and still uses
`oe_runmake install`.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add a -native recipe that builds Qualcomm's Download Tool (qdl) from
the linux-msm/qdl upstream at v2.7. The recipe is mainly used for
VIP (Validated Image Programming) digest-table generation.  In
--dry-run + --create-digests mode, qdl walks the rawprogram / patch
XMLs the same way a real flash would but, instead of transferring data,
computes a digest over each programmed segment and writes a
DigestsToSign.bin file.  classes-recipe/
image_types_qcom_vip.bbclass consumes this to build the signed
qcomflash-vip image type.

Build system is meson + ninja with libusb-1.0, libxml-2.0 and libzip
runtime deps, all of which are packaged as -native by oe-core, so
the recipe collapses to `inherit native meson pkgconfig` plus the
three DEPENDS.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Add a new image type that bolts onto the existing qcomflash image
type to produce a signed VIP (Validated Image Programming) digest
table.  Modelled on the "make sign-build" pipeline in
qcom-sec-tools/secure.sh; the three on-target steps boil down to:

  1. qdl --dry-run --create-digests -- walks the rawprogram/patch
     XMLs the same way a real flash would, but computes per-segment
     digests instead of transferring data.  Produces
     DigestsToSign.bin under ${IMGDEPLOYDIR}/${IMAGE_NAME}.qcomflash-vip/.
  2. sectools mbn-tool generate --mbn-version 6 -- wraps the digest
     binary in a Qualcomm firmware MBN container so the on-target
     verifier recognises it.
  3. qcom_sign_verify_file -- signs the MBN as image-id VIP using
     the same keying material as the rest of the firmware blobs.

Output: ${IMAGE_NAME}.qcomflash-vip.tar.gz, intended to be flashed
alongside the qcomflash tarball via qdl --vip-table-path.

The class inherits image_types_qcom (so qcomflash is defined) and
qcom-firmware-sign (so signing helpers / key material are wired in)
and adds itself to IMAGE_TYPES with an IMAGE_TYPEDEP on qcomflash so
the dependency graph keeps the qcomflash build as a prerequisite of
the VIP step.  Activate via:

    IMAGE_FSTYPES += "qcomflash-vip"

When QCOM_FIRMWARE_SIGN_ENABLE is not "1" the VIP step is a no-op
because there is nothing OEM-specific to bind the digests to.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>

@koenkooi Koen Kooi (koenkooi) left a comment

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.

This completely removes qdl (non-native), please change that to one commit updating qdl_git to 2.7+ and adding inherit native. I use qdl on-device to flash other devices, it's more convenient than buying even more USB hubs 😎

@igoropaniuk

Igor Opaniuk (igoropaniuk) commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

This completely removes qdl (non-native), please change that to one commit updating qdl_git to 2.7+ and adding inherit native. I use qdl on-device to flash other devices, it's more convenient than buying even more USB hubs 😎

Sure, I thought it's no used anymore (as still points to 2.1, which is a bit obsolete). Also 2.7 release requires libzip, which is available in meta-oe, that's why I also moved the recipe to the dynamic layers

@igoropaniuk

Igor Opaniuk (igoropaniuk) commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

FYI: the build fails because of security-profiles

install -d "${D}${datadir}/qcom-security-profiles"
install -m 0644 "${S}"/*_security_profile.xml \
"${D}${datadir}/qcom-security-profiles/"
} No newline at end of file

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.

HOMEPAGE = "https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Security_Tools"
LICENSE = "LICENSE.qcom-2"

LIC_FILES_CHKSUM = "file://${UNPACKDIR}/${ZIP_TOPDIR}/CHANGES.txt;md5=d2a0bb01dcd8befe660b832fbbe05900"

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.

This is not really correct, started an internal thread to see if we can have a license inside the zip file.

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.

Yeah, that was actually the issue (no LICENSE file inside the archive )

LICENSE = "LICENSE.qcom-2"

LIC_FILES_CHKSUM = "file://${UNPACKDIR}/${ZIP_TOPDIR}/CHANGES.txt;md5=d2a0bb01dcd8befe660b832fbbe05900"
ZIP_TOPDIR = "1.48"

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.

PV?

@ricardosalveti Ricardo Salveti (ricardosalveti) Jun 24, 2026

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.

Saw now that the directory is 1.48.0 but the zip is 1.48.zip, not great.

}

FILES:${PN} += "${datadir}/sectools"
INSANE_SKIP:${PN} += "already-stripped" No newline at end of file

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.

do_compile[noexec] = "1"

# Pick the per-platform sectools binary that matches the build host.
SECTOOLS_PLATFORM_DIR = "${@'Linux_aarch64' if d.getVar('BUILD_ARCH') == 'aarch64' else 'Linux'}"

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.

We should also set COMPATIBLE_HOST.

# 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 No newline at end of file

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 :-)

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.


local_conf_header:
ecdsa-secure-boot-test-keys: |
QCOM_FIRMWARE_SIGN_KEY_DIR = "${LAYERDIR_qcom}/ci/test-keys/ecdsa" No newline at end of file

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.

@@ -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.

Comment thread ci/secure-boot.yml
# 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

Comment thread ci/secure-boot.yml
# `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" No newline at end of file

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.

# by the meson + pkgconfig classes.
DEPENDS = "libusb1-native libxml2-native libzip-native"

inherit native meson pkgconfig No newline at end of file

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.

# qdl links against libusb-1.0, libxml-2.0 and libzip; all three are
# packaged as -native in OE-core. meson/ninja and pkgconfig provided
# by the meson + pkgconfig classes.
DEPENDS = "libusb1-native libxml2-native libzip-native"

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.

Why is it depending on libzip?

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.

Because of this feature linux-msm/qdl@fb3b622

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.

Maybe we can decouple this functionality and have a compile flag in meson.option (it can be enabled by default, but in our case for VIP generation it's not needed)

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.

Yeah, would be nice to have this as an optional dependency upstream, that way we won't have to depend on meta-oe for such a core BSP functionality.

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.

If it needs to be a ZIP file, can we use a standard zlib for it?


ln -sf "${IMAGE_NAME}.qcomflash-vip.tar.gz" \
"${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.qcomflash-vip.tar.gz"
}

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.

I would prefer this to be done as part of the main tarball, to avoid confusion.

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.

will do

HOMEPAGE = "https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Security_Tools"
LICENSE = "LICENSE.qcom-2"

LIC_FILES_CHKSUM = "file://${UNPACKDIR}/${ZIP_TOPDIR}/CHANGES.txt;md5=d2a0bb01dcd8befe660b832fbbe05900"

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.

Drop UNPACKDIR. Also, can we work with the sectools authors to include the licence into the archive?


do_unpack[postfuncs] += "sectools_chmod_unpacked"
sectools_chmod_unpacked() {
chmod -R u+w "${UNPACKDIR}"

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.

Why?

# 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 No newline at end of file

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

Comment thread ci/secure-boot.yml
# `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" No newline at end of file

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.

@@ -1,23 +0,0 @@
SUMMARY = "Qualcomm DownLoader flashing tool"

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.

Would it be better to upgrade it instead of adding a native-only version?

sectools-native:do_populate_sysroot \
security-profiles-native:do_populate_sysroot"

create_qcomflash_vip_pkg() {

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.

Okay, having a separate fstype kind of makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants