From bbb570204fb2887a11380ec226d33ef38404feb9 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 22 Oct 2026 15:17:58 +0000 Subject: [PATCH] test: add an integration test for systemd-firstboot --addon= Creates an image with an ESP via repart, writes an addon into it with all settings systemd-firstboot can provision, and checks what ended up in the image: the sections of the (unsigned) addon, and the credentials in its cpio. Also covers replacing an addon, a second one next to it, the combinations that are refused, and an image without an ESP. --- mkosi/mkosi.sanitizers/mkosi.postinst | 2 + .../TEST-74-AUX-UTILS.firstboot-addon.sh | 223 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100755 test/units/TEST-74-AUX-UTILS.firstboot-addon.sh diff --git a/mkosi/mkosi.sanitizers/mkosi.postinst b/mkosi/mkosi.sanitizers/mkosi.postinst index 4738316c2db..6bf822409ad 100755 --- a/mkosi/mkosi.sanitizers/mkosi.postinst +++ b/mkosi/mkosi.sanitizers/mkosi.postinst @@ -98,6 +98,8 @@ wrap=( multipath multipathd nvme + objcopy + objdump p11-kit pkill ps diff --git a/test/units/TEST-74-AUX-UTILS.firstboot-addon.sh b/test/units/TEST-74-AUX-UTILS.firstboot-addon.sh new file mode 100755 index 00000000000..115c5f9127d --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.firstboot-addon.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Tests systemd-firstboot --addon=, which writes the settings as a (for now unsigned) systemd-stub addon +# into the ESP of an image, without mounting it. We check what ends up in the image, booting it is left to +# the tests of the stub. + +if ! command -v systemd-firstboot >/dev/null; then + echo "systemd-firstboot not found, skipping the test" + exit 77 +fi + +for tool in systemd-repart mcopy mdir mmd objcopy objdump cpio jq sfdisk openssl; do + if ! command -v "$tool" >/dev/null; then + echo "$tool not found, skipping the test" + exit 77 + fi +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) EFI_ARCH=x64 ;; + aarch64) EFI_ARCH=aa64 ;; + *) echo "No addon stub for $ARCH, skipping the test"; exit 77 ;; +esac + +STUB="/usr/lib/systemd/boot/efi/addon${EFI_ARCH}.efi.stub" +if [[ ! -f "$STUB" ]]; then + echo "$STUB not found, skipping the test" + exit 77 +fi + +at_exit() { + set +e + [[ -n "${WORK:-}" ]] && rm -rf "$WORK" +} + +trap at_exit EXIT + +WORK="$(mktemp -d /var/tmp/firstboot-addon.XXXXXXXXXX)" +DEFS="$WORK/defs" +IMAGE="$WORK/image.raw" +mkdir -p "$DEFS" + +# An image with an ESP and a root partition, nothing in them, as the root file system is never looked at +cat >"$DEFS/10-esp.conf" <"$DEFS/20-root.conf" </dev/null +[[ "$(mdir -i "$NODE" -b ::/loader/addons/ | wc -l)" -eq 1 ]] +mcopy -n -i "$NODE" ::/loader/addons/customer.addon.efi "$WORK/customer.addon.efi" + +# It carries the sections an addon is made of, and no signature +objdump -h "$WORK/customer.addon.efi" | grep '\.cmdline' >/dev/null +objdump -h "$WORK/customer.addon.efi" | grep '\.initrd' >/dev/null +objdump -h "$WORK/customer.addon.efi" | grep '\.sbat' >/dev/null +objdump -p "$WORK/customer.addon.efi" | grep -E '^Entry 4\s' | grep '0000000000000000 00000000' >/dev/null + +objcopy --dump-section .cmdline="$WORK/cmdline" \ + --dump-section .initrd="$WORK/creds.cpio" \ + --dump-section .sbat="$WORK/sbat" \ + "$WORK/customer.addon.efi" "$WORK/scratch.efi" +[[ "$(cat "$WORK/cmdline")" == "firstboot.addon=1" ]] +grep -q '^uki-addon,1,' "$WORK/sbat" + +# The credentials in the cpio are the ones the settings map to, in the trusted bucket +mkdir "$WORK/extract" +(cd "$WORK/extract" && cpio -idm --quiet <"$WORK/creds.cpio") +CREDS="$WORK/extract/.extra/system_credentials" +[[ "$(stat -c '%a' "$CREDS")" == "500" ]] +[[ "$(cat "$CREDS/firstboot.hostname.cred")" == "customerbox" ]] +[[ "$(cat "$CREDS/firstboot.locale.cred")" == "C.UTF-8" ]] +[[ "$(cat "$CREDS/firstboot.locale-messages.cred")" == "C.UTF-8" ]] +[[ "$(cat "$CREDS/firstboot.keymap.cred")" == "us" ]] +[[ "$(cat "$CREDS/firstboot.timezone.cred")" == "Europe/Berlin" ]] +[[ "$(cat "$CREDS/firstboot.machine-tags.cred")" == "bar:foo" ]] +[[ "$(cat "$CREDS/system.machine_id.cred")" == "0e1d84a8a1d84c6e9b2d2f0f8f1a2b3c" ]] +[[ "$(cat "$CREDS/passwd.hashed-password.root.cred")" == "$HASHED" ]] +[[ "$(cat "$CREDS/passwd.shell.root.cred")" == "/bin/sh" ]] +cmp "$CREDS/keyring-setup.dm-verity.customer.cred" "$WORK/customer.crt" +cmp "$CREDS/keyring-setup.fs-verity.other.cred" "$WORK/second.crt" +[[ "$(stat -c '%a' "$CREDS/keyring-setup.dm-verity.customer.cred")" == "400" ]] +[[ "$(find "$CREDS" -type f | wc -l)" -eq 11 ]] + +# A plaintext password is hashed before it ends up in the addon, and writing again replaces the addon +systemd-firstboot --image="$IMAGE" --addon=customer --root-password=hunter2 +mcopy -o -n -i "$NODE" ::/loader/addons/customer.addon.efi "$WORK/customer2.addon.efi" +objcopy --dump-section .initrd="$WORK/creds2.cpio" "$WORK/customer2.addon.efi" "$WORK/scratch.efi" +rm -rf "$WORK/extract" +mkdir "$WORK/extract" +(cd "$WORK/extract" && cpio -idm --quiet <"$WORK/creds2.cpio") +[[ "$(find "$CREDS" -type f | wc -l)" -eq 1 ]] +grep -q '^\$' "$CREDS/passwd.hashed-password.root.cred" +grep -qv 'hunter2' "$CREDS/passwd.hashed-password.root.cred" +(! objdump -h "$WORK/customer2.addon.efi" | grep '\.cmdline' >/dev/null) + +# A second addon lives next to the first +systemd-firstboot --image="$IMAGE" --addon=other --hostname=otherbox +[[ "$(mdir -i "$NODE" -b ::/loader/addons/ | wc -l)" -eq 2 ]] + +# A key path containing '=' is not mistaken for NAME=PATH +mkdir "$WORK/a=b" +cp "$WORK/customer.crt" "$WORK/a=b/customer.crt" +systemd-firstboot --image="$IMAGE" --addon=third --key=bpf:"$WORK/a=b/customer.crt" +mcopy -o -n -i "$NODE" ::/loader/addons/third.addon.efi "$WORK/third.addon.efi" +objcopy --dump-section .initrd="$WORK/creds3.cpio" "$WORK/third.addon.efi" "$WORK/scratch.efi" +rm -rf "$WORK/extract" +mkdir "$WORK/extract" +(cd "$WORK/extract" && cpio -idm --quiet <"$WORK/creds3.cpio") +cmp "$CREDS/keyring-setup.bpf.customer.cred" "$WORK/customer.crt" + +# What is refused +(! systemd-firstboot --addon=customer --hostname=foo) +(! systemd-firstboot --image="$IMAGE" --addon=customer --prompt-hostname) +(! systemd-firstboot --image="$IMAGE" --addon=customer --copy-timezone) +(! systemd-firstboot --image="$IMAGE" --addon=customer --hostname=foo --force) +(! systemd-firstboot --image="$IMAGE" --addon=customer --hostname=foo --reset) +(! systemd-firstboot --image="$IMAGE" --addon=customer --delete-root-password) +(! systemd-firstboot --image="$IMAGE" --addon=customer) +(! systemd-firstboot --image="$IMAGE" --addon='../customer' --hostname=foo) +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=dm-verity:/dev/null) +(! systemd-firstboot --image="$IMAGE" --addon=customer --key="$WORK/customer.crt") +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=bad:"$WORK/customer.crt") +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=dm-verity:Bad="$WORK/customer.crt") +(! systemd-firstboot --image="$IMAGE" --addon='cust:omer' --hostname=foo) +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=dm-verity:="$WORK/customer.crt") +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=dm-verity:) +cp "$WORK/customer.crt" "$WORK/.hidden" +(! systemd-firstboot --image="$IMAGE" --addon=customer --key=dm-verity:"$WORK/.hidden") +(! systemd-firstboot --image="$IMAGE" --key=dm-verity:"$WORK/customer.crt") +# An empty kernel command line is not a setting either +(! systemd-firstboot --image="$IMAGE" --addon=customer --kernel-command-line=) +# The locales are at least checked for syntax +(! systemd-firstboot --image="$IMAGE" --addon=customer --locale='not a locale') +(! systemd-firstboot --image="$IMAGE" --addon=customer --locale-messages='not a locale') +# mtools cannot handle '@@' in the image path +cp "$IMAGE" "$WORK/we@@ird.raw" +(! systemd-firstboot --image="$WORK/we@@ird.raw" --addon=customer --hostname=foo) +# A stub that is an addon already is refused +(! SYSTEMD_ADDON_STUB="$WORK/customer.addon.efi" systemd-firstboot --image="$IMAGE" --addon=customer --hostname=foo) + +# An image without an ESP +rm -f "$DEFS/10-esp.conf" +rm -f "$IMAGE" +systemd-repart --empty=create --size=auto --dry-run=no --offline=yes --definitions="$DEFS" "$IMAGE" +(! systemd-firstboot --image="$IMAGE" --addon=customer --hostname=foo) + +# With an XBOOTLDR partition the addon goes there, as that is where the UKIs, and hence the stub looking for +# addons, live +cat >"$DEFS/10-esp.conf" <"$DEFS/15-xbootldr.conf" </dev/null +(! mdir -i "$IMAGE@@$((ESP_START * 512))" ::/loader/addons/ 2>/dev/null)