258 lines
13 KiB
Bash
Executable File
258 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
source "$ROOT/bin/omarchy-usb-authorization-boot"
|
|
|
|
scratch=$(mktemp -d)
|
|
trap 'rm -rf "$scratch"' EXIT
|
|
machine=12345678901234567890123456789012
|
|
export calls="$scratch/calls"
|
|
: >"$calls"
|
|
|
|
# Optional integration run uses real sbctl keys and sbattach in isolated paths.
|
|
sbctl() {
|
|
printf '%s\n' "$1" >>"$calls"
|
|
[[ ${SIGN_FAIL:-0} != 1 || $1 != sign ]] || return 1
|
|
[[ ${VERIFY_FAIL:-0} != 1 || $1 != verify ]] || return 1
|
|
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
|
|
/usr/bin/sbctl --config "$USB_TEST_SBCTL_CONFIG" "$@"
|
|
elif [[ $1 == verify ]]; then
|
|
jq -n --arg file "${!#}" --argjson signed "${TEST_SIGNED:-1}" '[{file_name: $file, is_signed: $signed}]'
|
|
fi
|
|
}
|
|
sbattach() {
|
|
if [[ -n ${USB_TEST_SBATTACH:-} ]]; then
|
|
"$USB_TEST_SBATTACH" "$@"
|
|
fi
|
|
}
|
|
omarchy-cmd-missing() { return 1; }
|
|
limine-enroll-config() { [[ ${ENROLL_FAIL:-0} != 1 ]]; }
|
|
usb_authorization_secure_boot_enabled() { [[ ${SECURE_BOOT:-1} == 1 ]]; }
|
|
# Bootloader enrollment/rollback is exercised with real EFI binaries in its own suite.
|
|
usb_authorization_verify_bootloader() { return 0; }
|
|
|
|
fixture() {
|
|
local embedded="$1" hash digest image_base
|
|
esp="$scratch/case-$RANDOM"
|
|
history="$esp/$machine/limine_history"
|
|
manifest="$history/snapshots.json"
|
|
cache="$esp/cache.json"
|
|
config="$esp/limine.conf"
|
|
mkdir -p "$history"
|
|
printf '%s\0' "$embedded" >"$esp/cmdline"
|
|
cp /usr/lib/systemd/boot/efi/linuxx64.efi.stub "$esp/image.efi"
|
|
image_base=$(objdump -p "$esp/image.efi" | awk '$1 == "ImageBase" {print "0x" $2}')
|
|
objcopy --add-section .cmdline="$esp/cmdline" --change-section-vma .cmdline="$((image_base + 0x100000))" "$esp/image.efi"
|
|
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
|
|
sbctl sign "$esp/image.efi" >/dev/null
|
|
fi
|
|
digest=$(sha256sum "$esp/image.efi"); digest=${digest%% *}
|
|
old_name="test.efi_sha256_$digest"
|
|
cp "$esp/image.efi" "$history/$old_name"
|
|
hash=$(b2sum "$esp/image.efi"); hash=${hash%% *}
|
|
printf '/Omarchy\ncomment: machine-id=%s\n //snapshot\n comment: kernel-id=linux\n path: boot():/%s/limine_history/%s#%s\n cmdline: %s rootflags=subvol=/snapshot\n' \
|
|
"$machine" "$machine" "$old_name" "$hash" "$embedded" >"$config"
|
|
jq -n --arg name "$old_name" --arg hash "$hash" --arg machine "$machine" --arg cmdline "$embedded" '
|
|
def kernel:
|
|
{imageDetails: [{fileHashName: $name, fileName: "test.efi",
|
|
snapshotFilePathLine: "/" + $machine + "/limine_history/" + $name + "#" + $hash,
|
|
filePathLine: "/EFI/Linux/test.efi#" + $hash, properties: {HASH: "#" + $hash}}],
|
|
cmdlineDetails: [{cmdline: $cmdline, snapshotCmdline: $cmdline + " rootflags=subvol=/snapshot"}],
|
|
allInConfig: ["path: boot():/EFI/Linux/test.efi#" + $hash, "cmdline: " + $cmdline],
|
|
allInSnapshotConfig: ["comment: kernel-id=linux", "path: boot():/" + $machine + "/limine_history/" + $name + "#" + $hash, "cmdline: " + $cmdline + " rootflags=subvol=/snapshot"], subKernels: []};
|
|
{jsonFormatVersion: "1.3.0", snapshotEntries: [{kernelEntries: [kernel | .subKernels = [kernel]]}]}
|
|
' >"$manifest"
|
|
cp "$manifest" "$manifest.old"
|
|
cp "$manifest" "$cache"
|
|
cp "$manifest" "$esp/before.json"
|
|
cp "$config" "$esp/before.conf"
|
|
: >"$calls"
|
|
}
|
|
|
|
for expected in enabled disabled; do
|
|
old='quiet'
|
|
[[ $expected != disabled ]] || old+=' usbcore.authorized_default=0'
|
|
fixture "$old"
|
|
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine" "$cache"
|
|
new_name=$(jq -r '.snapshotEntries[0].kernelEntries[0].imageDetails[0].fileHashName' "$manifest")
|
|
[[ $new_name != "$old_name" && -f $history/$new_name ]] || fail "conflicting archives get new content-addressed copies"
|
|
cmp "$history/$old_name" "$esp/image.efi" || fail "original archived image remains unchanged"
|
|
digest=$(sha256sum "$history/$new_name")
|
|
[[ $new_name == *"${digest%% *}" ]] || fail "replacement name matches its content hash"
|
|
objcopy -O binary --only-section=.cmdline "$history/$new_name" "$esp/section"
|
|
embedded=$(tr '\0' '\n' <"$esp/section")
|
|
wanted=quiet
|
|
[[ $expected != enabled ]] || wanted+=' usbcore.authorized_default=0'
|
|
[[ $embedded == "$wanted" ]] || fail "replacement must preserve embedded boot arguments and change only USB policy"
|
|
objcopy -O binary --only-section=.text "$history/$old_name" "$esp/text.before"
|
|
objcopy -O binary --only-section=.text "$history/$new_name" "$esp/text.after"
|
|
cmp "$esp/text.before" "$esp/text.after" || fail "archive conversion must not alter executable code"
|
|
[[ $(grep -c '^sign$' "$calls") == 1 && $(grep -c '^verify$' "$calls") == 2 ]] || fail "deduplicated image must be verified before and after signing"
|
|
for file in "$manifest" "$manifest.old" "$cache"; do
|
|
! grep -Fq "$old_name" "$file" || fail "no old image references may survive in manifests"
|
|
usb_authorization_rewrite_snapshot_manifest "$expected" "$file"
|
|
# Both cached and structured restore representations must regenerate valid entries.
|
|
for format in cached structured; do
|
|
{
|
|
printf '/Omarchy\ncomment: machine-id=%s\n //snapshot\n' "$machine"
|
|
if [[ $format == cached ]]; then
|
|
jq -r '.snapshotEntries[0].kernelEntries[0].allInSnapshotConfig[]' "$file"
|
|
else
|
|
printf 'comment: kernel-id=linux\n'
|
|
jq -r '.snapshotEntries[0].kernelEntries[0].subKernels[0] | "path: boot():" + .imageDetails[0].snapshotFilePathLine, "cmdline: " + .cmdlineDetails[0].snapshotCmdline' "$file"
|
|
fi
|
|
} >"$esp/regenerated.conf"
|
|
usb_authorization_verify_boot_entries "$expected" "$esp/regenerated.conf" "$esp" "$machine" enabled || fail "regenerated $format $expected snapshot must verify"
|
|
done
|
|
done
|
|
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine"
|
|
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine" enabled || fail "$expected archive transition must verify under Secure Boot"
|
|
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine" "$cache"
|
|
[[ $(grep -c '^sign$' "$calls") == 1 ]] || fail "retry must not resign an already-compatible archive"
|
|
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
|
|
usb_authorization_verify_signature "$history/$new_name"
|
|
fi
|
|
pass "USB $expected transition updates signed history and all cached/structured references"
|
|
done
|
|
|
|
fixture quiet
|
|
old_hash=$(b2sum "$esp/image.efi"); old_hash=${old_hash%% *}
|
|
printf ' //current\n comment: kernel-id=current\n path: boot():/image.efi#%s\n cmdline: quiet\n' "$old_hash" >>"$config"
|
|
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
|
|
grep -Fq "path: boot():/image.efi#$old_hash" "$config" || fail "preflight must not rewrite hashes of unchanged current images sharing archive content"
|
|
pass "USB archive preflight preserves unchanged current-image hashes"
|
|
|
|
fixture 'quiet rd.test="two spaces"'
|
|
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
|
|
new_name=$(jq -r '.snapshotEntries[0].kernelEntries[0].imageDetails[0].fileHashName' "$manifest")
|
|
objcopy -O binary --only-section=.cmdline "$history/$new_name" "$esp/section"
|
|
[[ $(tr '\0' '\n' <"$esp/section") == 'quiet rd.test="two spaces" usbcore.authorized_default=0' ]] || fail "unrelated quoted arguments must retain their whitespace"
|
|
pass "USB archive policy changes preserve unrelated embedded arguments and executable code"
|
|
|
|
for failure in SIGN_FAIL VERIFY_FAIL ENROLL_FAIL; do
|
|
fixture quiet
|
|
if (export "$failure=1"; usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache") >"$esp/failure.log" 2>&1; then
|
|
fail "$failure must prevent publication"
|
|
fi
|
|
cmp "$config" "$esp/before.conf" || fail "$failure must preserve the original boot menu"
|
|
for file in "$manifest" "$manifest.old" "$cache"; do
|
|
cmp "$file" "$esp/before.json" || fail "$failure must preserve every manifest"
|
|
done
|
|
cmp "$history/$old_name" "$esp/image.efi" || fail "$failure must preserve the archived image"
|
|
done
|
|
pass "USB archive signing, verification and enrollment failures preserve original references"
|
|
|
|
fixture quiet
|
|
printf corrupt >>"$history/$old_name"
|
|
if usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/hash-failure" 2>&1; then
|
|
fail "corrupt original archive must never be signed"
|
|
fi
|
|
[[ ! -s $calls ]] || fail "hash failure must occur before signing"
|
|
cmp "$config" "$esp/before.conf"
|
|
SECURE_BOOT=0 usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
|
|
[[ ! -s $calls ]] || fail "non-Secure-Boot transitions must not rewrite archive images"
|
|
pass "USB archive conversion rejects corruption and skips non-Secure-Boot systems"
|
|
|
|
if [[ -z ${USB_TEST_SBCTL_CONFIG:-} ]]; then
|
|
fixture quiet
|
|
if TEST_SIGNED=0 usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/unsigned" 2>&1; then
|
|
fail "a false-success sbctl result cannot authorize signing"
|
|
fi
|
|
! grep -qx sign "$calls" || fail "an unsigned original must not be resigned"
|
|
fi
|
|
for missing in menu manifest; do
|
|
fixture quiet
|
|
if [[ $missing == menu ]]; then
|
|
sed -i '/cmdline:/d' "$config"
|
|
else
|
|
jq '.snapshotEntries[0].kernelEntries[0].cmdlineDetails = []' "$manifest" >"$esp/modified"
|
|
mv "$esp/modified" "$manifest"
|
|
fi
|
|
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" || fail "embedded-only $missing must support policy changes"
|
|
usb_authorization_rewrite_boot_cmdlines enabled "$config" "$machine"
|
|
usb_authorization_verify_boot_entries enabled "$config" "$esp" "$machine" enabled || fail "embedded-only $missing must retain a valid command line"
|
|
done
|
|
for section in .profile .pcrsig; do
|
|
fixture quiet
|
|
printf protected >"$esp/protected"
|
|
objcopy --add-section "$section=$esp/protected" "$history/$old_name" 2>/dev/null
|
|
if usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/protected-failure" 2>&1; then
|
|
fail "a $section image must require its original builder"
|
|
fi
|
|
grep -Fq "($section)" "$esp/protected-failure" || fail "unsupported signing policies need an actionable error"
|
|
[[ ! -s $calls ]] || fail "unsupported signing policies must not be resigned"
|
|
done
|
|
pass "USB archive conversion requires authentic signatures and preserves boot/unlock requirements"
|
|
|
|
# Package hooks use this same lock. Installing the optional signature tool
|
|
# must finish before preparation takes the lock, while sync must run under it.
|
|
fixture quiet
|
|
prepare=$(declare -f usb_authorization_prepare_archived_images)
|
|
prepare=${prepare//\/run\/lock\/boot-partition.lock/$scratch\/preflight.lock}
|
|
eval "$prepare"
|
|
omarchy-cmd-missing() { return 0; }
|
|
omarchy-pkg-add() {
|
|
flock --nonblock "$scratch/preflight.lock" true || fail "package installation must precede the boot lock"
|
|
printf 'package\n' >>"$calls"
|
|
}
|
|
limine-snapper-sync() {
|
|
[[ $* == '--no-mutex --no-hooks' ]] || fail "preparation must use the already-held lock"
|
|
if flock --nonblock "$scratch/preflight.lock" true; then
|
|
fail "snapshot synchronization must hold the boot lock"
|
|
fi
|
|
}
|
|
usb_authorization_prepare_archived_images enabled "$config" "$esp" "$machine"
|
|
grep -qx package "$calls" || fail "missing signature tooling must be installed before preparing archives"
|
|
pass "USB archive preparation installs optional tools before taking Limine's lock"
|
|
|
|
# The disposable Lab runs the actual root-only main function with only boot paths
|
|
# and external build/sync commands redirected into this fixture.
|
|
if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
|
|
(( EUID == 0 )) || fail "main-path fixtures require the disposable guest's root"
|
|
usb_authorization_prepare_archived_images() {
|
|
usb_authorization_reconcile_archived_images "$1" "$2" "$3" "$machine" "$cache"
|
|
}
|
|
limine-mkinitcpio() { return 0; }
|
|
usb_authorization_sync_and_verify() {
|
|
local file
|
|
for file in "$manifest" "$manifest.old" "$cache"; do
|
|
usb_authorization_rewrite_snapshot_manifest "$1" "$file" || return 1
|
|
done
|
|
usb_authorization_rewrite_boot_cmdlines "$1" "$2" "$machine" || return 1
|
|
usb_authorization_verify_boot_entries "$1" "$2" "$3" "$machine" enabled
|
|
}
|
|
for action in enable disable; do
|
|
old=quiet
|
|
[[ $action != disable ]] || old+=' usbcore.authorized_default=0'
|
|
fixture "$old"
|
|
drop_in="$esp/entry-tool/usb-authorization.conf"
|
|
limine_defaults="$esp/defaults"
|
|
limine_conf="$config"
|
|
boot_path="$esp"
|
|
printf '# fixture defaults\n' >"$limine_defaults"
|
|
if [[ $action == disable ]]; then
|
|
mkdir -p "${drop_in%/*}"
|
|
printf '%s\n' "$setting" >"$drop_in"
|
|
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
|
fi
|
|
cp "$limine_defaults" "$esp/defaults.before"
|
|
if (export SIGN_FAIL=1; usb_authorization_main "$action") >"$esp/main-failed" 2>&1; then
|
|
fail "$action must stop if archive preparation fails"
|
|
fi
|
|
cmp "$limine_defaults" "$esp/defaults.before" || fail "$action failure must precede snapshot-setting changes"
|
|
if [[ $action == enable ]]; then
|
|
[[ ! -e $drop_in ]] || fail "enable preflight failure must not create the boot setting"
|
|
else
|
|
[[ $(<"$drop_in") == "$setting" ]] || fail "disable preflight failure must preserve the boot setting"
|
|
fi
|
|
(usb_authorization_main "$action")
|
|
if [[ $action == enable ]]; then
|
|
[[ $(<"$drop_in") == "$setting" ]] || fail "successful enable sets the boot policy"
|
|
else
|
|
[[ ! -e $drop_in ]] || fail "successful disable clears the boot policy"
|
|
fi
|
|
pass "USB $action main path prepares archives before changing boot policy"
|
|
done
|
|
fi
|