-
Notifications
You must be signed in to change notification settings - Fork 303
[RFC] qcom-firmware-sign: build-time secure-boot signing of Qualcomm firmware #2598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
5086824
f032556
446e1ec
a998372
8c7a2de
6265560
bb6079a
52c0458
efca6cc
a1ee798
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 \ | ||
| " | ||
|
|
||
| # 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again :-)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing EOL |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| SUMMARY = "Qualcomm Sectools v2 -- image signing / verification tool" | ||
| DESCRIPTION = "Qualcomm Security Tools v2: the host \ | ||
| binary used to sign, verify and inspect Qualcomm firmware images (XBL, \ | ||
| TZ, modem, etc.) according to a per-chipset Security Profile XML" | ||
| 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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that was actually the issue (no LICENSE file inside the archive )
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| ZIP_TOPDIR = "1.48" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PV?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| SRC_URI = "https://softwarecenter.qualcomm.com/api/download/software/tools/Qualcomm_Security_Tools/All/${PV}/${ZIP_TOPDIR}.zip;name=sectools-zip;downloadfilename=qcom-sectools-${PV}.zip" | ||
| SRC_URI[sectools-zip.sha256sum] = "d89773bbfcc9c80c871b628bd2e766460a876277661ed6634d57590b7fd80fba" | ||
|
|
||
| S = "${UNPACKDIR}/${ZIP_TOPDIR}" | ||
|
|
||
| INHIBIT_DEFAULT_DEPS = "1" | ||
|
|
||
| inherit native | ||
|
|
||
| do_configure[noexec] = "1" | ||
| 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'}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also set COMPATIBLE_HOST. |
||
|
|
||
| # Stage the per-platform sectools binary under ${datadir}/sectools/ and | ||
| # drop a thin symlink into ${bindir} so consumers can invoke `sectools` | ||
| # from PATH regardless of which subdir was selected. | ||
| do_install() { | ||
| install -d "${D}${datadir}/sectools/${SECTOOLS_PLATFORM_DIR}" | ||
| install -m 0755 "${S}/${SECTOOLS_PLATFORM_DIR}/sectools" \ | ||
| "${D}${datadir}/sectools/${SECTOOLS_PLATFORM_DIR}/sectools" | ||
|
|
||
| install -d "${D}${bindir}" | ||
| ln -sf "../share/sectools/${SECTOOLS_PLATFORM_DIR}/sectools" \ | ||
| "${D}${bindir}/sectools" | ||
|
|
||
| install -m 0644 "${S}/CHANGES.txt" "${D}${datadir}/sectools/CHANGES.txt" | ||
| } | ||
|
|
||
| do_unpack[postfuncs] += "sectools_chmod_unpacked" | ||
| sectools_chmod_unpacked() { | ||
| chmod -R u+w "${UNPACKDIR}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? |
||
| } | ||
|
|
||
| FILES:${PN} += "${datadir}/sectools" | ||
| INSANE_SKIP:${PN} += "already-stripped" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing EOL. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| SUMMARY = "Qualcomm Security Profiles" | ||
| DESCRIPTION = "Per-chipset Security Profile XML files consumed by Sectools" | ||
| HOMEPAGE = "https://github.com/qualcomm/security-profiles" | ||
| LICENSE = "BSD-3-Clause-Clear" | ||
| LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=2998c54c288b081076c9af987bdf4838" | ||
|
|
||
| SRC_URI = "git://github.com/qualcomm/security-profiles.git;protocol=https;branch=main" | ||
| SRCREV = "122f1917af2b428880a7607ed705bb46bec66f5b" | ||
|
|
||
| inherit native | ||
|
|
||
| do_configure[noexec] = "1" | ||
| do_compile[noexec] = "1" | ||
|
|
||
| # Install the XML profiles into a well-known native datadir location | ||
| # so qcom-firmware-sign.bbclass can resolve them by filename via | ||
| # QCOM_FIRMWARE_SIGN_SECPROFILE. | ||
| do_install() { | ||
| install -d "${D}${datadir}/qcom-security-profiles" | ||
| install -m 0644 "${S}"/*_security_profile.xml \ | ||
| "${D}${datadir}/qcom-security-profiles/" | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing EOL. |
||
There was a problem hiding this comment.
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.