From 4893ea5c368db61b9f1ab703071ad162ed93e77a Mon Sep 17 00:00:00 2001 From: Sean Rhodes Date: Wed, 26 Aug 2026 17:55:11 +0100 Subject: [PATCH 1/2] initrd: Consume signed fwupd capsules Allow boards to consume firmware staged by fwupd on a FAT ESP. Validate the EFI capsule before passing its ROM to a board-selected writer. Authenticate an exact manifest and payload with an isolated vendor keyring. Bind each package to its board and capsule GUID, enforce bounded sizes, and require a newer generation to prevent rollback. Quarantine rejected requests and mark accepted requests as applying before invoking the writer. Remove a request only after a successful write, then reboot according to board policy. Signed-off-by: Sean Rhodes --- bin/build_heads_fwupd_capsule.sh | 118 +++++++++++ initrd/bin/gui-init.sh | 5 + initrd/bin/heads-fwupd.sh | 348 +++++++++++++++++++++++++++++++ tests/fwupd/test_heads_fwupd.sh | 153 ++++++++++++++ 4 files changed, 624 insertions(+) create mode 100755 bin/build_heads_fwupd_capsule.sh create mode 100755 initrd/bin/heads-fwupd.sh create mode 100755 tests/fwupd/test_heads_fwupd.sh diff --git a/bin/build_heads_fwupd_capsule.sh b/bin/build_heads_fwupd_capsule.sh new file mode 100755 index 000000000..024809b81 --- /dev/null +++ b/bin/build_heads_fwupd_capsule.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later + +set -euo pipefail + +rom="" +guid="" +board="" +generation="" +version="" +output="" +local_user="" + +usage() { + cat <&2 + exit 2 + ;; + esac +done + +[ -s "$rom" ] || { echo "ROM is missing or empty" >&2; exit 1; } +[[ $guid =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || { + echo "GUID must be a lowercase canonical GUID" >&2 + exit 1 +} +[[ $board =~ ^[A-Za-z0-9_-]+$ ]] || { echo "Board name is invalid" >&2; exit 1; } +[[ $generation =~ ^[0-9]+$ ]] || { echo "Generation must be an integer" >&2; exit 1; } +[ -n "$version" ] || { echo "Version is required" >&2; exit 1; } +[ -n "$output" ] || { echo "Output path is required" >&2; exit 1; } + +tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/heads-fwupd-capsule.XXXXXX") +trap 'rm -rf "$tmpdir"' EXIT + +cp "$rom" "$tmpdir/firmware.rom" +rom_size=$(wc -c <"$tmpdir/firmware.rom" | tr -d ' ') +rom_sha256=$(sha256sum "$tmpdir/firmware.rom" | awk '{print $1}') +cat >"$tmpdir/manifest" <"$output" + +echo "$output" diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index ce03d953b..c7200ac14 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -913,6 +913,11 @@ if [ -x /bin/hotp_verification ]; then detect_usb_security_dongle_branding fi +if [ "$CONFIG_HEADS_FWUPD" = "y" ]; then + /bin/heads-fwupd.sh || whiptail_error --title "Firmware Update Rejected" \ + --msgbox "A staged firmware update could not be authenticated or did not match this system. It was not applied." 0 80 +fi + if detect_boot_device; then # /boot device with installed OS found clean_boot_check diff --git a/initrd/bin/heads-fwupd.sh b/initrd/bin/heads-fwupd.sh new file mode 100755 index 000000000..ce64a25bc --- /dev/null +++ b/initrd/bin/heads-fwupd.sh @@ -0,0 +1,348 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-3.0-or-later + +set -e -o pipefail + +# shellcheck source=/dev/null +. "${HEADS_FWUPD_FUNCTIONS_SH:-/etc/functions.sh}" +# shellcheck source=/dev/null +. "${HEADS_FWUPD_CONFIG_SH:-/tmp/config}" + +CAPSULE="" +KEYRING="${CONFIG_HEADS_FWUPD_VENDOR_KEYRING:-/etc/heads-fwupd-vendor.gpg}" +WRITER="${CONFIG_HEADS_FWUPD_WRITER:-/bin/flash.sh}" +ASSUME_YES="${HEADS_FWUPD_ASSUME_YES:-${CONFIG_HEADS_FWUPD_ASSUME_YES:-n}}" +MAX_CAPSULE_SIZE="${CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE:-}" +MAX_ROM_SIZE="${CONFIG_HEADS_FWUPD_MAX_ROM_SIZE:-}" +REBOOT="${CONFIG_HEADS_FWUPD_REBOOT:-y}" +KEEP_CAPSULE="n" +SOURCE_DEVICE="" +SOURCE_RELATIVE="" +SOURCE_ACTIVE_RELATIVE="" +QUARANTINE_ON_FAILURE="n" +WORKDIR="${TMPDIR:-/tmp}/heads-fwupd" +LOCKDIR="${TMPDIR:-/tmp}/heads-fwupd.lock" + +fail() { + if [ "$QUARANTINE_ON_FAILURE" = "y" ]; then + QUARANTINE_ON_FAILURE="n" + quarantine_capsule || + ERROR "Could not quarantine the rejected firmware update" + fi + ERROR "Heads firmware update rejected: $*" + return 1 +} + +cleanup() { + if grep -qs " $WORKDIR/esp " /proc/mounts; then + if ! umount "$WORKDIR/esp" 2>/dev/null; then + ERROR "Could not unmount the firmware update ESP; leaving the work directory intact" + return + fi + fi + rm -rf "$WORKDIR" + rmdir "$LOCKDIR" 2>/dev/null || true +} + +usage() { + echo "Usage: $0 [--capsule FILE] [--keyring FILE] [--writer FILE] [--assume-yes] [--keep]" +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --capsule) + CAPSULE="$2" + shift 2 + ;; + --keyring) + KEYRING="$2" + shift 2 + ;; + --writer) + WRITER="$2" + shift 2 + ;; + --assume-yes) + ASSUME_YES="y" + shift + ;; + --keep) + KEEP_CAPSULE="y" + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + usage >&2 + exit 2 + ;; + esac +done + +EXPECTED_GUID="${CONFIG_HEADS_FWUPD_CAPSULE_GUID:-}" +CURRENT_GENERATION="${CONFIG_HEADS_FWUPD_GENERATION:-}" + +[ -n "$EXPECTED_GUID" ] || fail "no capsule GUID is configured" +[ -n "$CURRENT_GENERATION" ] || fail "no firmware generation is configured" +[ -n "$MAX_CAPSULE_SIZE" ] || fail "no maximum capsule size is configured" +[ -n "$MAX_ROM_SIZE" ] || fail "no maximum firmware size is configured" +case "$CURRENT_GENERATION:$MAX_CAPSULE_SIZE:$MAX_ROM_SIZE" in +*[!0-9:]* | *::* | :*) fail "configured size or generation is invalid" ;; +esac +[ "$MAX_CAPSULE_SIZE" -gt 0 ] || fail "maximum capsule size is invalid" +[ "$MAX_ROM_SIZE" -gt 0 ] || fail "maximum firmware size is invalid" +case "$REBOOT" in +y | n) ;; +*) fail "firmware update reboot policy is invalid" ;; +esac +[ -r "$KEYRING" ] || fail "vendor keyring is unavailable" +[ -x "$WRITER" ] || fail "firmware writer is unavailable" + +mkdir "$LOCKDIR" 2>/dev/null || exit 0 +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR/esp" "$WORKDIR/package" "$WORKDIR/gnupg" +chmod 700 "$WORKDIR/gnupg" +trap cleanup EXIT INT TERM + +quarantine_capsule() { + local rejected + + [ -n "$SOURCE_DEVICE" ] || return 0 + rejected="${SOURCE_RELATIVE}.rejected" + if ! mount -o rw "$SOURCE_DEVICE" "$WORKDIR/esp"; then + return 1 + fi + if ! mv -f "$WORKDIR/esp/$SOURCE_RELATIVE" "$WORKDIR/esp/$rejected"; then + umount "$WORKDIR/esp" 2>/dev/null || true + return 1 + fi + sync + umount "$WORKDIR/esp" +} + +capsule_size() { + local size + + size=$(wc -c <"$1" | tr -d ' ') || return 1 + case "$size" in + '' | *[!0-9]*) return 1 ;; + esac + printf '%s' "$size" +} + +copy_capsule_from_esp() { + local device="$1" + local relative="EFI/UpdateCapsule/fwupd-${EXPECTED_GUID}.cap" + local size + + if ! mount -o ro "$device" "$WORKDIR/esp" 2>/dev/null; then + return 1 + fi + if [ ! -f "$WORKDIR/esp/$relative" ]; then + umount "$WORKDIR/esp" || return 2 + return 1 + fi + size=$(capsule_size "$WORKDIR/esp/$relative") || { + umount "$WORKDIR/esp" 2>/dev/null || true + ERROR "Could not determine the staged capsule size" + return 2 + } + if [ "$size" -gt "$MAX_CAPSULE_SIZE" ]; then + umount "$WORKDIR/esp" 2>/dev/null || true + SOURCE_DEVICE="$device" + SOURCE_RELATIVE="$relative" + QUARANTINE_ON_FAILURE="y" + fail "staged firmware capsule exceeds the configured size limit" || true + return 2 + fi + if ! cp "$WORKDIR/esp/$relative" "$WORKDIR/update.cap"; then + umount "$WORKDIR/esp" 2>/dev/null || true + ERROR "Could not copy the staged firmware capsule" + return 2 + fi + if ! umount "$WORKDIR/esp"; then + ERROR "Could not unmount the firmware update ESP" + return 2 + fi + SOURCE_DEVICE="$device" + SOURCE_RELATIVE="$relative" + CAPSULE="$WORKDIR/update.cap" +} + +find_capsule() { + local device + local found="n" + + for device in $(blkid 2>/dev/null | sed -n '/TYPE="vfat"/s/:.*//p'); do + if copy_capsule_from_esp "$device"; then + if [ "$found" = "y" ]; then + ERROR "Heads firmware update rejected: more than one matching capsule was found" + return 2 + fi + found="y" + else + case "$?" in + 1) ;; + *) return 2 ;; + esac + fi + done + [ "$found" = "y" ] || return 1 +} + +if [ -z "$CAPSULE" ]; then + if find_capsule; then + QUARANTINE_ON_FAILURE="y" + else + case "$?" in + 1) exit 0 ;; + *) exit 1 ;; + esac + fi +else + FILE_SIZE=$(capsule_size "$CAPSULE") || fail "capsule size could not be determined" + [ "$FILE_SIZE" -le "$MAX_CAPSULE_SIZE" ] || fail "capsule exceeds the configured size limit" + cp "$CAPSULE" "$WORKDIR/update.cap" + CAPSULE="$WORKDIR/update.cap" +fi + +[ -s "$CAPSULE" ] || fail "capsule is empty" + +GUID_HEX=$(hexdump -v -n 16 -e '1/1 "%02x"' "$CAPSULE") +[ "${#GUID_HEX}" -eq 32 ] || fail "capsule GUID is truncated" +CAPSULE_GUID="${GUID_HEX:6:2}${GUID_HEX:4:2}${GUID_HEX:2:2}${GUID_HEX:0:2}" +CAPSULE_GUID="${CAPSULE_GUID}-${GUID_HEX:10:2}${GUID_HEX:8:2}" +CAPSULE_GUID="${CAPSULE_GUID}-${GUID_HEX:14:2}${GUID_HEX:12:2}" +CAPSULE_GUID="${CAPSULE_GUID}-${GUID_HEX:16:4}-${GUID_HEX:20:12}" +[ "$CAPSULE_GUID" = "$EXPECTED_GUID" ] || fail "capsule GUID does not match this board" + +HEADER_SIZE=$(hexdump -v -s 16 -n 4 -e '1/4 "%u"' "$CAPSULE") +IMAGE_SIZE=$(hexdump -v -s 24 -n 4 -e '1/4 "%u"' "$CAPSULE") +FILE_SIZE=$(wc -c <"$CAPSULE" | tr -d ' ') +case "$HEADER_SIZE:$IMAGE_SIZE:$FILE_SIZE" in +*[!0-9:]* | ::*) fail "capsule size fields are invalid" ;; +esac +[ "$HEADER_SIZE" -ge 28 ] || fail "capsule header is too small" +[ "$HEADER_SIZE" -lt "$FILE_SIZE" ] || fail "capsule has no payload" +[ "$IMAGE_SIZE" -eq "$FILE_SIZE" ] || fail "capsule image size is incorrect" +[ "$FILE_SIZE" -le "$MAX_CAPSULE_SIZE" ] || fail "capsule exceeds the configured size limit" + +tail -c "+$((HEADER_SIZE + 1))" "$CAPSULE" >"$WORKDIR/payload.tar" || + fail "capsule payload could not be read" +PACKAGE_LIST=$(tar -tf "$WORKDIR/payload.tar" | LC_ALL=C sort) +EXPECTED_LIST=$(printf '%s\n' firmware.rom manifest manifest.asc | LC_ALL=C sort) +[ "$PACKAGE_LIST" = "$EXPECTED_LIST" ] || fail "capsule payload has an unexpected file set" +for file in firmware.rom manifest manifest.asc; do + tar -xOf "$WORKDIR/payload.tar" "$file" >"$WORKDIR/package/$file" || + fail "capsule payload is missing $file" +done + +MANIFEST="$WORKDIR/package/manifest" +if [ "$(wc -l <"$MANIFEST" | tr -d ' ')" -ne 7 ] || + grep -Ev '^(format|guid|board|generation|version|size|sha256)=[^[:cntrl:]]+$' "$MANIFEST" >/dev/null; then + fail "manifest format is invalid" +fi + +manifest_value() { + local key="$1" + local value + value=$(sed -n "s/^${key}=//p" "$MANIFEST") + [ "$(printf '%s\n' "$value" | wc -l)" -eq 1 ] || return 1 + [ -n "$value" ] || return 1 + printf '%s' "$value" +} + +FORMAT=$(manifest_value format) || fail "manifest format is missing" +MANIFEST_GUID=$(manifest_value guid) || fail "manifest GUID is missing" +MANIFEST_BOARD=$(manifest_value board) || fail "manifest board is missing" +GENERATION=$(manifest_value generation) || fail "manifest generation is missing" +VERSION=$(manifest_value version) || fail "manifest version is missing" +ROM_SIZE=$(manifest_value size) || fail "manifest ROM size is missing" +ROM_SHA256=$(manifest_value sha256) || fail "manifest ROM hash is missing" + +[ "$FORMAT" = "1" ] || fail "manifest version is unsupported" +[ "$MANIFEST_GUID" = "$EXPECTED_GUID" ] || fail "signed manifest GUID does not match this board" +[ "$MANIFEST_BOARD" = "$CONFIG_BOARD" ] || fail "signed manifest targets another board" +case "$GENERATION:$CURRENT_GENERATION:$ROM_SIZE" in +*[!0-9:]* | ::*) fail "manifest numeric fields are invalid" ;; +esac +[ "$GENERATION" -gt "$CURRENT_GENERATION" ] || fail "firmware generation is not newer" +[ "$ROM_SIZE" -le "$MAX_ROM_SIZE" ] || fail "firmware exceeds the configured size limit" + +ACTUAL_SIZE=$(wc -c <"$WORKDIR/package/firmware.rom" | tr -d ' ') +ACTUAL_SHA256=$(sha256sum "$WORKDIR/package/firmware.rom" | awk '{print $1}') +[ "$ACTUAL_SIZE" -eq "$ROM_SIZE" ] || fail "firmware size does not match the signed manifest" +[ "$ACTUAL_SHA256" = "$ROM_SHA256" ] || fail "firmware hash does not match the signed manifest" + +STATUS "Verifying firmware signature" +gpg --homedir "$WORKDIR/gnupg" --batch --quiet --no-default-keyring \ + --keyring "$WORKDIR/vendor.gpg" --import "$KEYRING" >/dev/null 2>&1 || + fail "vendor keyring could not be loaded" +gpg --homedir "$WORKDIR/gnupg" --batch --quiet --no-default-keyring \ + --keyring "$WORKDIR/vendor.gpg" --verify \ + "$WORKDIR/package/manifest.asc" "$MANIFEST" >/dev/null 2>&1 || + fail "vendor signature verification failed" + +QUARANTINE_ON_FAILURE="n" + +if [ "$ASSUME_YES" != "y" ]; then + whiptail_warning --title "Apply Heads Firmware Update?" \ + --yesno "A signed firmware update for $CONFIG_BOARD was found.\n\nVersion: $VERSION\nGeneration: $GENERATION\n\nApply it now?" 0 80 || exit 0 +fi + +change_capsule_request_state() { + local from="$1" + local to="$2" + + if ! mount -o rw "$SOURCE_DEVICE" "$WORKDIR/esp"; then + return 1 + fi + if [ -e "$WORKDIR/esp/$to" ]; then + umount "$WORKDIR/esp" 2>/dev/null || true + return 1 + fi + if ! mv "$WORKDIR/esp/$from" "$WORKDIR/esp/$to"; then + umount "$WORKDIR/esp" 2>/dev/null || true + return 1 + fi + sync + umount "$WORKDIR/esp" +} + +remove_capsule_request() { + if ! mount -o rw "$SOURCE_DEVICE" "$WORKDIR/esp"; then + return 1 + fi + if ! rm "$WORKDIR/esp/$SOURCE_ACTIVE_RELATIVE"; then + umount "$WORKDIR/esp" 2>/dev/null || true + return 1 + fi + sync + umount "$WORKDIR/esp" +} + +if [ "$KEEP_CAPSULE" != "y" ] && [ -n "$SOURCE_DEVICE" ]; then + SOURCE_ACTIVE_RELATIVE="${SOURCE_RELATIVE}.applying" + change_capsule_request_state "$SOURCE_RELATIVE" "$SOURCE_ACTIVE_RELATIVE" || + fail "could not mark the staged update as applying" +fi + +if ! "$WRITER" "$WORKDIR/package/firmware.rom"; then + if [ -n "$SOURCE_ACTIVE_RELATIVE" ]; then + change_capsule_request_state "$SOURCE_ACTIVE_RELATIVE" "$SOURCE_RELATIVE" || + ERROR "Could not restore the staged update after the writer failed" + fi + fail "firmware writer failed" +fi + +if [ -n "$SOURCE_ACTIVE_RELATIVE" ]; then + remove_capsule_request || fail "could not remove the applied firmware update request" +fi + +STATUS_OK "Heads firmware update applied" +if [ "$REBOOT" = "y" ]; then + /bin/reboot.sh + fail "system did not reboot after applying firmware" +fi diff --git a/tests/fwupd/test_heads_fwupd.sh b/tests/fwupd/test_heads_fwupd.sh new file mode 100755 index 000000000..344f558bd --- /dev/null +++ b/tests/fwupd/test_heads_fwupd.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/heads-fwupd-test.XXXXXX") +trap 'rm -rf "$tmpdir"' EXIT + +guid="0617fcc9-0266-5650-964b-8d8deb52d992" +board="qemu-coreboot-fbwhiptail-tpm2-fwupd" + +mkdir -p "$tmpdir/gnupg" "$tmpdir/package" +chmod 700 "$tmpdir/gnupg" +GNUPGHOME="$tmpdir/gnupg" gpg --batch --passphrase '' \ + --quick-generate-key 'Heads fwupd test ' rsa2048 sign 0 >/dev/null 2>&1 +GNUPGHOME="$tmpdir/gnupg" gpg --batch --export >"$tmpdir/vendor.gpg" + +cat >"$tmpdir/functions.sh" <<'EOF' +STATUS() { :; } +STATUS_OK() { :; } +ERROR() { echo "$*" >&2; } +EOF + +cat >"$tmpdir/config" <"$tmpdir/writer" <"$tmpdir/written.sha256" +EOF +chmod +x "$tmpdir/writer" + +guid_to_efi_hex() { + local compact=${1//-/} + printf '%s%s%s%s' \ + "${compact:6:2}${compact:4:2}${compact:2:2}${compact:0:2}" \ + "${compact:10:2}${compact:8:2}" \ + "${compact:14:2}${compact:12:2}" \ + "${compact:16}" +} + +le32() { + local hex + printf -v hex '%08x' "$1" + printf '%s' "${hex:6:2}${hex:4:2}${hex:2:2}${hex:0:2}" +} + +make_capsule() { + local generation=$1 + local manifest_guid=$2 + local manifest_board=$3 + local output=$4 + local tamper_manifest=${5:-n} + local rom_size rom_hash payload_size image_size + + rm -rf "$tmpdir/package" + mkdir "$tmpdir/package" + printf 'deterministic test firmware\n' >"$tmpdir/package/firmware.rom" + rom_size=$(wc -c <"$tmpdir/package/firmware.rom") + rom_hash=$(sha256sum "$tmpdir/package/firmware.rom" | awk '{print $1}') + cat >"$tmpdir/package/manifest" <"$output" +} + +run_consumer() { + HEADS_FWUPD_FUNCTIONS_SH="$tmpdir/functions.sh" \ + HEADS_FWUPD_CONFIG_SH="$tmpdir/config" \ + "$repo_root/initrd/bin/heads-fwupd.sh" \ + --capsule "$1" --keyring "$tmpdir/vendor.gpg" \ + --writer "$tmpdir/writer" --assume-yes --keep +} + +make_capsule 8 "$guid" "$board" "$tmpdir/valid.cap" +run_consumer "$tmpdir/valid.cap" +test -s "$tmpdir/written.sha256" + +rm -f "$tmpdir/written.sha256" +sed -i 's/^CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE=.*/CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE=1024/' \ + "$tmpdir/config" +if run_consumer "$tmpdir/valid.cap"; then + echo "Oversized capsule was accepted" >&2 + exit 1 +fi +test ! -e "$tmpdir/written.sha256" +sed -i 's/^CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE=.*/CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE=20971520/' \ + "$tmpdir/config" + +make_capsule 7 "$guid" "$board" "$tmpdir/rollback.cap" +if run_consumer "$tmpdir/rollback.cap"; then + echo "Rollback capsule was accepted" >&2 + exit 1 +fi +test ! -e "$tmpdir/written.sha256" + +make_capsule 8 "0d063fe8-e15d-5a25-b09e-1445568ee097" "$board" "$tmpdir/wrong-guid.cap" +if run_consumer "$tmpdir/wrong-guid.cap"; then + echo "Wrong manifest GUID was accepted" >&2 + exit 1 +fi + +make_capsule 8 "$guid" "another_board" "$tmpdir/wrong-board.cap" +if run_consumer "$tmpdir/wrong-board.cap"; then + echo "Wrong board was accepted" >&2 + exit 1 +fi + +make_capsule 8 "$guid" "$board" "$tmpdir/bad-signature.cap" y +if run_consumer "$tmpdir/bad-signature.cap"; then + echo "Capsule with a tampered manifest was accepted" >&2 + exit 1 +fi + +cp "$tmpdir/valid.cap" "$tmpdir/wrong-header-guid.cap" +printf '\000' | dd of="$tmpdir/wrong-header-guid.cap" bs=1 seek=0 conv=notrunc status=none +if run_consumer "$tmpdir/wrong-header-guid.cap"; then + echo "Wrong capsule-header GUID was accepted" >&2 + exit 1 +fi + +cp "$tmpdir/valid.cap" "$tmpdir/malformed.cap" +printf '\377\377\377\177' | dd of="$tmpdir/malformed.cap" bs=1 seek=16 conv=notrunc status=none +if run_consumer "$tmpdir/malformed.cap"; then + echo "Malformed capsule was accepted" >&2 + exit 1 +fi + +echo "Heads fwupd capsule tests passed" From 50d099cdb94c17d1d4d72bc1568cd388cd450d47 Mon Sep 17 00:00:00 2001 From: Sean Rhodes Date: Wed, 26 Aug 2026 17:55:44 +0100 Subject: [PATCH 2/2] boards: Add fwupd QEMU fixture Add a TPM2 QEMU board for exercising the fwupd capsule handoff with a disposable public key and a file-backed mock writer. Give the VM a stable SMBIOS identity that the companion fwupd proof of concept can match. Confirmation and reboot are bypassed only for this non-flashing fixture; physical boards remain disabled. Signed-off-by: Sean Rhodes --- .../initrd/bin/heads-fwupd-qemu-writer.sh | 11 +++++++++++ .../initrd/etc/heads-fwupd-qemu-test-key.asc | 18 ++++++++++++++++++ .../qemu-coreboot-fbwhiptail-tpm2-fwupd.config | 15 +++++++++++++++ targets/qemu.mk | 1 + 4 files changed, 45 insertions(+) create mode 100755 boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/bin/heads-fwupd-qemu-writer.sh create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/etc/heads-fwupd-qemu-test-key.asc create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/qemu-coreboot-fbwhiptail-tpm2-fwupd.config diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/bin/heads-fwupd-qemu-writer.sh b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/bin/heads-fwupd-qemu-writer.sh new file mode 100755 index 000000000..8157fe330 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/bin/heads-fwupd-qemu-writer.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-3.0-or-later + +set -e + +# shellcheck source=/dev/null +. /etc/functions.sh + +[ -s "$1" ] || DIE "QEMU firmware update payload is empty" +sha256sum "$1" >/tmp/heads-fwupd-qemu-applied.sha256 +STATUS_OK "QEMU file-backed firmware writer accepted the update" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/etc/heads-fwupd-qemu-test-key.asc b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/etc/heads-fwupd-qemu-test-key.asc new file mode 100644 index 000000000..0d2f2b306 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/initrd/etc/heads-fwupd-qemu-test-key.asc @@ -0,0 +1,18 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGqPAxUBCAC4yy0MKn5SaIxvQn93tnul4DU6ZX3hL2tXWWjwxv9dPxK3ll5Y +z7p2lnGeMQChS1CttI2tHP2aKrlOdirrU/seZMTZftNu5LBQciIK6bYe0Iuydz2Z +6xpawcrCpuiAw0QARIErn2uB2CGMjHRY7aPb+k7L58kGxzMr/5eGx9cP06xrgOY9 +tL9RCrKZVi7F//ydPl8lTshCIBlxFxn2wCrwCDy8djJE7w3zOBkobPd5VdMwCznq +mMsbFGJXZloHR3p7JRjOCytEFmJO0D2lnlKAjM7yVdn4T942gm+IPCvFepYx3UT+ +IXNR0EjEST8uQiG16ZdK7LN/XG55yXpd72wBABEBAAG0O1N0YXIgTGFicyBIZWFk +cyBRRU1VIHRlc3Qga2V5IDxoZWFkcy1xZW11QHN0YXJsYWJzLnN5c3RlbXM+iQFO +BBMBCgA4FiEE4GKn3RF6gBzu/bQ1pcgDVTX2or0FAmqPAxUCGwMFCwkIBwIGFQoJ +CAsCBBYCAwECHgECF4AACgkQpcgDVTX2or2CEAf8CiWJ2AmjrxTYLqavrHuGkVig +9vunbmhqA03lw0rkSI/0DVn/SNq64AntD9EheOwPfSuAtTfX9jdNI5QgntKr+IBp +qrJzxqPITjPLYLkWi9rNEF792DfOem93YpGjWfwEWZu5jAptkwzevsTJIXYGtPv5 +D4I0UpMaO2L5uXs08zkmTSCSZgfSgdUNRztuULymsFARvQrge8lTEuiZqNzXEW+d +2o+x1+vIDCq9bDu59ZrxRDEXdTfRdZ6Uyg6OTf77aeZMFAVBbPypyCEmpJrYkh9p +w0Vh5B27hqSJ9jYbi6DPwVuGD+wyRDuDCPC3rX5hIAnYS2dKzdl4CoaOCG7K4w== +=hi7E +-----END PGP PUBLIC KEY BLOCK----- diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/qemu-coreboot-fbwhiptail-tpm2-fwupd.config b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/qemu-coreboot-fbwhiptail-tpm2-fwupd.config new file mode 100644 index 000000000..77c4f4570 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-fwupd/qemu-coreboot-fbwhiptail-tpm2-fwupd.config @@ -0,0 +1,15 @@ +# QEMU fixture for exercising the fwupd capsule handoff without writing flash. +include $(pwd)/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config + +export CONFIG_BOARD_NAME=qemu-coreboot-fbwhiptail-tpm2-fwupd +export CONFIG_HEADS_FWUPD=y +export CONFIG_HEADS_FWUPD_CAPSULE_GUID=0617fcc9-0266-5650-964b-8d8deb52d992 +export CONFIG_HEADS_FWUPD_GENERATION=0 +export CONFIG_HEADS_FWUPD_MAX_CAPSULE_SIZE=20971520 +export CONFIG_HEADS_FWUPD_MAX_ROM_SIZE=16777216 +export CONFIG_HEADS_FWUPD_VENDOR_KEYRING=/etc/heads-fwupd-qemu-test-key.asc +export CONFIG_HEADS_FWUPD_WRITER=/bin/heads-fwupd-qemu-writer.sh +export CONFIG_HEADS_FWUPD_ASSUME_YES=y +export CONFIG_HEADS_FWUPD_REBOOT=n + +QEMU_SMBIOS_ARGS := -smbios "type=1,manufacturer=Star Labs,product=starlabs_qemu,family=QEMU" diff --git a/targets/qemu.mk b/targets/qemu.mk index 53208ce62..79d141dbf 100644 --- a/targets/qemu.mk +++ b/targets/qemu.mk @@ -118,6 +118,7 @@ run: $(TPMDIR)/.manufacture $(ROOT_DISK_IMG) $(MEMORY_SIZE_FILE) $(USB_FD_IMG) -device usb-tablet \ -drive file="$(QEMU_USB_FD_IMG)",if=none,id=usb-fd-drive,format=raw \ -device usb-storage,bus=usb.0,drive=usb-fd-drive \ + $(QEMU_SMBIOS_ARGS) \ $(QEMU_USB_TOKEN_DEV) \ stty sane