Skip to content

Build-time secure-boot signing of Qualcomm firmware - #3168

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

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

Conversation

@igoropaniuk

Copy link
Copy Markdown
Contributor

This PR adds a build-time secure-boot signing pipeline for Qualcomm firmware. It introduces two -native recipes - sectools-native, security-profiles-native, and - together with a qcom-firmware-sign.bbclass that any recipe can inherit to sign its deploy artefacts before they ship. It also generates VIP tables using QDL, so the final package is ready to be flashed with QDL to the secured device with VIP engaged.

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.

To test this build:

kas build ci/base.yml:ci/<machine>.yml:ci/secure-boot.yml:ci/ecdsa-secure-boot-test-keys.yml

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

This is based on the original #2598 + comments addressed

Comment thread classes-recipe/qcom-firmware-sign.bbclass Outdated
Comment thread classes-recipe/qcom-firmware-sign.bbclass
Comment thread conf/qcom-firmware-sign-image-ids.conf
Comment thread recipes-bsp/firmware/firmware-qcom.inc Outdated
Comment thread classes-recipe/image_types_qcom.bbclass
Comment thread conf/machine/include/qcom-qcs615.inc Outdated
@ricardosalveti

Copy link
Copy Markdown
Contributor

Also need a CI entry so we can build with the new kas files.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Test run workflow

Test jobs for commit 2073d47

qcom-distro_linux-qcom-6.18
Pass: 228 | Fail: 5 | Total: 253
nodistro
Pass: 10 | Fail: 0 | Total: 10
qcom-distro
Pass: 11 | Fail: 1 | Total: 12

@test-reporting-app

test-reporting-app Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Test Results

   66 files    311 suites   4h 35m 8s ⏱️
  171 tests   160 ✅  1 💤 10 ❌
2 052 runs  2 015 ✅ 20 💤 17 ❌

For more details on these failures, see this check.

Results for commit 2073d47.

♻️ This comment has been updated with latest results.

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit ee5c01a

  • qcomdistro: multimedia image-prop
    Pass: 33 | Fail: 2 | Others: 5 | Total: 40
  • qcomdistro: multimedia image
    Pass: 9 | Fail: 0 | Total: 9

Comment thread recipes-bsp/firmware/firmware-qcom.inc Outdated
@ricardosalveti

Copy link
Copy Markdown
Contributor

We should also hash the content of the keys, in keys they are modified by the user, something like:

 #   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 ?= ""
+QCOM_FIRMWARE_SIGN_KEY_FILES ?= "\
+    qpsa_rootca0.cer \
+    qpsa_attestca0.cer \
+    qpsa_attestca0.key \
+    sha384_roots_hash.txt \
+"

 # Fuse identifiers, passed through to consumers / signing wrappers.
 QCOM_FUSE_OEM_HW_ID                  ?= ""
@@ -68,7 +74,7 @@ qcom_check_signing_enabled() {
     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
+    for f in ${QCOM_FIRMWARE_SIGN_KEY_FILES}; do
         if [ ! -f "${QCOM_FIRMWARE_SIGN_KEY_DIR}/${f}" ]; then
             bbfatal "Required key file missing: ${QCOM_FIRMWARE_SIGN_KEY_DIR}/${f}"
         fi
@@ -236,4 +242,14 @@ python () {
                      'do_deploy do_package do_populate_sysroot', 'do_install', d)
     d.setVarFlag('do_qcom_firmware_sign', 'fakeroot', '1')
     d.appendVarFlag('do_qcom_firmware_sign', 'depends', ' virtual/fakeroot-native:do_populate_sysroot')
+
+    # The task hash has to follow the key material itself, not just the
+    # path to it: keys rotated in place under an unchanged
+    # QCOM_FIRMWARE_SIGN_KEY_DIR would otherwise leave the previously
+    # signed images valid in sstate.
+    keydir = d.getVar('QCOM_FIRMWARE_SIGN_KEY_DIR')
+    if keydir:
+        keyfiles = d.getVar('QCOM_FIRMWARE_SIGN_KEY_FILES').split()
+        d.appendVarFlag('do_qcom_firmware_sign', 'file-checksums',
+                        ' ' + ' '.join('%s/%s:True' % (keydir, f) for f in keyfiles))
 }

Comment thread conf/qcom-firmware-sign-image-ids.conf
@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit d5751ae

  • qcomdistro: multimedia image-prop
    Pass: 41 | Fail: 0 | Total: 41
  • qcomdistro: multimedia image
    Pass: 9 | Fail: 0 | Total: 9

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

Test fails are unrelated to these changes, seems they are board/lab flakes (sm8750/qcs8300/iq-9075 LAVA jobs)

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 510cff2

  • qcomdistro: multimedia image-prop
    Pass: 33 | Fail: 1 | Others: 4 | Total: 38
  • qcomdistro: multimedia image
    Pass: 7 | Fail: 0 | Others: 4 | Total: 11

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 510cff2

  • qcomdistro: multimedia image
    Pass: 7 | Fail: 0 | Others: 4 | Total: 11
  • qcomdistro: multimedia image-prop
    Pass: 26 | Fail: 1 | Others: 10 | Total: 37

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 510cff2

  • qcomdistro: multimedia image-prop
    Pass: 35 | Fail: 1 | Total: 36
  • qcomdistro: multimedia image
    Pass: 9 | Fail: 0 | Total: 9

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 510cff2

  • qcomdistro: multimedia image-prop
    Pass: 34 | Fail: 2 | Total: 36
  • qcomdistro: multimedia image
    Pass: 9 | Fail: 0 | Total: 9

Comment thread classes-recipe/qcom-firmware-sign.bbclass
Comment thread classes-recipe/qcom-firmware-sign.bbclass Outdated
Comment thread classes-recipe/qcom-firmware-sign.bbclass Outdated
Comment thread recipes-bsp/firmware-boot/firmware-qcom-boot-common.inc Outdated
Comment thread recipes-bsp/firmware/firmware-qcom.inc Outdated
Comment thread classes-recipe/image_types_qcom.bbclass Outdated
Comment thread classes-recipe/qcomflash-vip.bbclass Outdated
Comment thread classes-recipe/qcomflash-vip.bbclass Outdated
Comment thread ci/ecdsa-secure-boot-test-keys.yml Outdated
Comment thread ci/secure-boot.yml Outdated
Sectools needs the per-SoC Security Profile XML that describes a
chipset's image ids and signing rules.  Qualcomm publishes them in
qualcomm/security-profiles (BSD-3-Clause-Clear).

Stage them in the native sysroot under ${datadir}/qcom-security-profiles
so qcom-firmware-sign.bbclass can pick one by filename.  Plain data, no
toolchain needed.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Sectools v2 signs, verifies and inspects Qualcomm firmware images
against a Security Profile.  It ships as a pre-built binary from the
Qualcomm Software Center, which allows anonymous downloads.

Package release 1.50.0: install the Linux build matching BUILD_ARCH
(x86_64 or aarch64) into ${bindir}, restrict COMPATIBLE_HOST to those
two and anchor the license to the bundled License.pdf.  The archive
name only carries two version parts, hence ZIP_TOPDIR.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
FIRMWARE_COMPRESSION is applied inline in do_install:append.  A later
change needs to undo it before signing the blobs and redo it afterwards,
so give the compression a name, firmware_qcom_compress, and add its
inverse, firmware_qcom_decompress.  No functional change.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
@igoropaniuk

Copy link
Copy Markdown
Contributor Author

Jose Quaresma (@quaresmajose) addressed you comments, thanks!

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit f7d5c86

  • qcomdistro: multimedia image
    Pass: 6 | Fail: 3 | Total: 9
  • qcomdistro: multimedia image-prop
    Pass: 27 | Fail: 2 | Others: 3 | Total: 32

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 0558c45

  • qcomdistro: multimedia image
    Pass: 7 | Fail: 2 | Total: 9
  • qcomdistro: multimedia image-prop
    Pass: 27 | Fail: 2 | Others: 3 | Total: 32

The pre-built Qualcomm firmware this layer ships is unsigned, and a
secure-boot enabled device needs an OEM signature on it.

Add a class that signs images at build time with sectools-native and
security-profiles-native.  do_qcom_firmware_sign runs after do_install
and before do_deploy, do_package and do_populate_sysroot, under pseudo so
files rewritten in ${D} keep their ownership.  With
QCOM_FIRMWARE_SIGN_ENABLE at its default "0" the task is noexec, pulls in
no dependencies, and the per-SoC Security Profile stays out of the task
hashes so shared recipes keep identical signatures across machines.

The task is driven by data: every file with a QCOM_FIRMWARE_SIGN_SUFFIXES
suffix under QCOM_FIRMWARE_SIGN_DIRS is mapped to a sectools image id
(conf/qcom-firmware-sign-image-ids.conf), checked against the active
profile, signed with the keys in QCOM_FIRMWARE_SIGN_KEY_DIR and verified
against the OEM root hash.  Recipes configure those variables and hook
prefuncs/postfuncs rather than redefining the task.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
The SoC boot binaries are deployed exactly as unpacked, unsigned.

Point qcom-firmware-sign at QCOM_BOOT_BINARY_DIRS, one level deep and
including .melf, so the images are signed in place before do_deploy picks
them up.  The recipes stay allarch unless signing is on; a signed build
is machine-specific.  No-op unless QCOM_FIRMWARE_SIGN_ENABLE is "1".

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
HLOS firmware is authenticated by TrustZone and needs an OEM signature
on a secure-boot enabled device, like the boot binaries.

Point qcom-firmware-sign at ${D}${FW_QCOM_BASE_PATH}.  Compressed blobs
no longer match the image-id map, so firmware_qcom_decompress runs as a
prefunc of the sign task and firmware_qcom_compress as a postfunc; the
packaged output is unchanged for builds that do not sign; the recipes
stay allarch unless signing is on and are machine-specific otherwise.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
A class that adds files to the qcomflash bundle has to run after every
artefact is in place and before the tarball is created.  Move the
symlink and tarball creation out of create_qcomflash_pkg into
create_qcomflash_tarball, registered as a postfunc of do_image_qcomflash,
so such a class can prepend its own postfunc.  No functional change.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
A VIP-fused device only programs images whose digests appear in a table
signed with the OEM keys, so a signed build is not flashable without
one, and the table must cover exactly the files in the bundle.

Add qcomflash-vip.bbclass: qdl --dry-run --create-digests produces the
table, sectools mbn-tool wraps it and it is signed as image id VIP.  It
lands in the qcomflash tarball as vip-tables/DigestsToSign.bin.mbn, via a
do_image_qcomflash postfunc ahead of the tarball, ready for
qdl --vip-table-path.  image_types_qcom.bbclass inherits the class when
QCOMFLASH_VIP is "1", which follows QCOM_FIRMWARE_SIGN_ENABLE.
qdl-native comes from the qdl 2.8 recipe.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
The Security Profile is a property of the silicon, not of a build
overlay.  Set QCOM_FIRMWARE_SIGN_SECPROFILE in the SoC includes for the
targets qualcomm/security-profiles covers: kodiak (QCS6490), lemans
(QCS9100), monaco (QCS8300) and talos (QCS615).

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Exercising the signing pipeline needs an OEM key set, and production
material cannot live in the tree.  Add a development ECDSA PKI under
ci/test-keys/ecdsa: root and attestation CA certificates, the CA key
and the root hash for sectools --verify-root.

DEVELOPMENT KEYS ONLY, never for production builds.

The commands used to generate the certificates and keys:

    # Generate Root CA certificate/keys
    openssl ecparam -genkey -name secp384r1 -outform PEM -out qpsa_rootca0.key
    openssl req -new -key qpsa_rootca0.key -sha384 -out rootca_pem0.crt \
        -subj '/C=US/CN=Generated OEM Root CA/OU=CDMA Technologies/OU=General Use OEM Key (OEM should update all fields)/L=San Diego/O=SecTools/ST=California' \
        -config opensslroot.cfg -x509 -days 7300 -set_serial 1
    openssl x509 -in rootca_pem0.crt -inform PEM -out qpsa_rootca0.cer -outform DER

    # Generate Attestation CA certificate/keys
    openssl ecparam -genkey -name secp384r1 -outform PEM -out qpsa_attestca0.key
    openssl req -new -key qpsa_attestca0.key -out ca0.CSR \
        -subj '/C=US/ST=California/CN=Generated OEM Attestation CA/O=SecTools/L=San Diego' \
        -config opensslroot.cfg -sha384
    openssl x509 -req -in ca0.CSR -CA rootca_pem0.crt -CAkey qpsa_rootca0.key -out ca_pem0.crt \
        -set_serial 1 -days 7300 -extfile v3.ext -sha384 -CAcreateserial
    openssl x509 -inform PEM -in ca_pem0.crt -outform DER -out qpsa_attestca0.cer

    # Calculate hash of Root CA certificate
    openssl dgst -sha384 qpsa_rootca0.cer > sha384_roots_hash.txt

opensslroot.cfg is a stock openssl.cnf (a copy of the one shipped with
OpenSSL is enough) whose [ req ] section points x509_extensions at a
[ v3_ca ] section with basicConstraints = CA:true, keyUsage = cRLSign,
keyCertSign and subjectKeyIdentifier = hash; these become the root
certificate's extensions.  v3.ext is a four-line extension file for the
attestation CA:

    authorityKeyIdentifier=keyid,issuer
    subjectKeyIdentifier=hash
    basicConstraints=CA:true,pathlen:0
    keyUsage=keyCertSign

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Point QCOM_FIRMWARE_SIGN_KEY_DIR at ci/test-keys/ecdsa, mirroring
ci/capsule-test-keys.yml:

    kas build ci/<machine>.yml:ci/secure-boot.yml:ci/ecdsa-secure-boot-test-keys.yml

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
ci/secure-boot.yml sets QCOM_FIRMWARE_SIGN_ENABLE to "1", which also
puts the VIP table into the qcomflash bundle, and provides development
placeholders for the fuse-binding ids and the anti-rollback floor.  The
profile comes from the machine, the keys from a second overlay such as
ci/ecdsa-secure-boot-test-keys.yml.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Nothing exercises ci/secure-boot.yml in CI, so a change to the signing
class or the sectools recipe only breaks a signing build after it lands.

Add an rb3gen2-core-kit entry that builds with the secure-boot overlay
and the ECDSA test keys, next to the capsule one.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 2073d47

  • qcomdistro: multimedia image
    Pass: 4 | Fail: 0 | Total: 4
  • qcomdistro: multimedia image-prop
    Pass: 19 | Fail: 0 | Others: 3 | Total: 22


create_qcomflash_tarball() {
# Create symlink to ${QCOMFLASH_DIR} dir
ln -rsf ${QCOMFLASH_DIR} ${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.qcomflash

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 ln -rsf now runs after create_symlinks (image.bbclass prepends it to every do_image_* postfunc list), so the link already exists and points at a directory. Without -n, ln follows it and creates <IMAGE_NAME>.qcomflash -> . inside the bundle, which then ends up in the tarball (confirmed on a local rb3gen2 build). create_symlinks already creates the same link, so just drop this line.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants