Restore signed boot files when USB boot policy changes fail

This commit is contained in:
acrogenesis committed 2026-09-18 13:30:03 -06:00
1 parent de72b64872
commit e12c8ed452
5 files changed
+320 -9

No files matched your search

+130 -6
View File
@@ -328,9 +328,11 @@ usb_authorization_rewrite_boot_cmdlines() {
fi
mv -f "$temporary" "$config"
if ! limine-enroll-config; then
if ! usb_authorization_enroll_config "$config" "${config%/*}"; then
mv -f "$backup" "$config"
limine-enroll-config >/dev/null 2>&1 || true
if ! usb_authorization_secure_boot_enabled; then
limine-enroll-config >/dev/null 2>&1 || true
fi
echo "Restored $config because its bootloader verification data could not be updated." >&2
return 1
fi
@@ -391,6 +393,109 @@ usb_authorization_verify_signature() {
fi
}
# Read the same architecture and enrollment setting as the installed Limine tool.
usb_authorization_limine_settings() (
source /usr/lib/limine/limine-common-functions
load_config || return 1
printf '%s %s\n' "$(limine_efi_arch | tr '[:upper:]' '[:lower:]')" "${ENABLE_ENROLL_LIMINE_CONFIG:-no}"
)
usb_authorization_verify_bootloader() (
local config="$1" esp="$2" architecture enrollment image check digest
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
image="$esp/EFI/limine/limine_$architecture.efi"
usb_authorization_verify_signature "$image" || return 1
check=$(mktemp) || return 1
trap 'rm -f "$check"' EXIT
cp "$image" "$check" || return 1
if [[ $enrollment == "yes" ]]; then
digest=$(b2sum "$config") || return 1
limine enroll-config "$check" "${digest%% *}" --quiet || return 1
else
limine enroll-config "$check" --reset --quiet || return 1
fi
# Enrollment edits fixed bytes in the executable. Reapplying the expected
# value must be a no-op, including when enrollment is explicitly disabled.
if ! cmp -s "$image" "$check"; then
echo "Limine's signed executable does not match the configured menu enrollment." >&2
return 1
fi
)
usb_authorization_enroll_config() {
limine-enroll-config || return 1
if usb_authorization_secure_boot_enabled; then
usb_authorization_verify_bootloader "$1" "$2" || return 1
fi
}
# Kernel rebuild hooks also reset and re-sign Limine. Checkpoint the complete
# boot files, not just the menu: rebuilding can overwrite images it references.
# The outer process holds the real boot lock; the private mount namespace gives
# nested Limine tools their own lock inode so they cannot deadlock against it.
usb_authorization_boot_transaction() (
local action="$1" state architecture enrollment index manifest changed=0 committed=0 restored=1
local machine_id="$(</etc/machine-id)"
local cache="${2:-/var/cache/boot/$machine_id}"
local -a paths=("$boot_path" "$cache" "$limine_defaults" "$drop_in")
exec 8>/run/lock/boot-partition.lock
flock --timeout 30 8 || return 1
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
usb_authorization_verify_signature "$boot_path/EFI/limine/limine_$architecture.efi" || return 1
state=$(mktemp -d /var/tmp/omarchy-usb-boot.XXXXXXXXXX) || return 1
trap '
if (( changed && ! committed )); then
echo "USB boot policy failed; restoring the previous boot files and settings." >&2
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d $state/$index && ! -L $state/$index ]]; then
mkdir -p "${paths[index]}" && cp -a "$state/$index/." "${paths[index]}/" || restored=0
elif [[ -e $state/$index || -L $state/$index ]]; then
cp -a --remove-destination "$state/$index" "${paths[index]}" || restored=0
else
rm -rf -- "${paths[index]}" || restored=0
fi
done
# New unreferenced images are harmless, but a newly created cached backup
# manifest must not reintroduce the failed policy on a later recovery.
for manifest in snapshots.json snapshots.json.old; do
[[ -e $state/0/$machine_id/limine_history/$manifest ]] ||
rm -f "$boot_path/$machine_id/limine_history/$manifest" || restored=0
[[ -e $state/1/lss/$manifest ]] || rm -f "$cache/lss/$manifest" || restored=0
done
sync -f "$boot_path" || restored=0
fi
if (( restored )); then
rm -rf "$state"
else
echo "Boot recovery was incomplete. Preserve and restore the checkpoint at $state before rebooting." >&2
fi
' EXIT
trap 'exit 1' HUP INT TERM
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d ${paths[index]} ]]; then
cp -a "${paths[index]}/." "$state/$index" || return 1
elif [[ -e ${paths[index]} || -L ${paths[index]} ]]; then
cp -a "${paths[index]}" "$state/$index" || return 1
fi
done
: >"$state/lock"
changed=1
usb_authorization_run_transaction "$state" "$action" || return 1
usb_authorization_verify_bootloader "$limine_conf" "$boot_path" || return 1
sync -f "$boot_path" || return 1
committed=1
)
usb_authorization_run_transaction() {
unshare --mount --propagation private /bin/bash -c '
mount --bind "$1/lock" /run/lock/boot-partition.lock || exit 1
source "$2"
usb_authorization_main "$3"
' bash "$1" "${BASH_SOURCE[0]}" "$2"
}
# Called with Limine's boot-partition lock held. Prepare signed copies of
# conflicting historical UKIs before changing the selected boot policy.
usb_authorization_reconcile_archived_images() (
@@ -409,7 +514,6 @@ usb_authorization_reconcile_archived_images() (
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
done
limine-enroll-config >/dev/null 2>&1 || true
fi
rm -rf "$staging"
' EXIT
@@ -544,7 +648,7 @@ usb_authorization_reconcile_archived_images() (
for ((index=0; index<${#originals[@]}; index++)); do
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
done
limine-enroll-config || return 1
usb_authorization_enroll_config "$config" "$esp" || return 1
committed=1
)
@@ -586,11 +690,11 @@ usb_authorization_sync_and_verify() (
# Verify the regenerated menu, so synchronization cannot mask stale stored
# command lines with a one-time edit to limine.conf.
limine-snapper-sync --no-mutex --no-hooks || return 1
limine-enroll-config || return 1
usb_authorization_enroll_config "$config" "$esp" || return 1
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
)
main() {
usb_authorization_main() {
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
if (( EUID != 0 )); then
@@ -680,6 +784,26 @@ main() {
esac
}
main() {
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
return 1
fi
case "${1:-}" in
enable|disable)
if usb_authorization_secure_boot_enabled; then
usb_authorization_boot_transaction "$1"
else
usb_authorization_main "$1"
fi
;;
*)
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
return 2
;;
esac
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
+1 -1
View File
@@ -25,7 +25,7 @@ For machines whose disk unlock and recovery never depend on any USB devices, _Se
Older snapshots created before USBGuard was installed and enabled keep all USB disabled even after startup, including keyboards, network adapters, and storage. Trusting the devices connected now does not make them available in those snapshots. Enable boot-time protection only if you can unlock and recover the machine without any USB devices. _Remove > Security > USB at Boot_ restores the normal early-boot behavior while keeping USBGuard active after startup.
With Secure Boot enabled, changing USB boot protection may require access to your signing keys to update older snapshot boot images. If an image uses a custom measured-boot policy or multiple boot profiles, rebuild it with its original signing setup before retrying.
With Secure Boot enabled, changing USB boot protection requires access to your signing keys and enough free space on the system drive for a temporary copy of the boot files. If signing fails, Omarchy restores the previous boot files and settings. If an image uses a custom measured-boot policy or multiple boot profiles, rebuild it with its original signing setup before retrying.
## Changing your passwords
+2
View File
@@ -295,6 +295,8 @@ CONF
source "$ROOT/bin/omarchy-usb-authorization-boot"
limine-mkinitcpio() { return 0; }
limine-enroll-config() { return 0; }
# Signed bootloader enrollment and rollback have a dedicated real-PE suite.
usb_authorization_verify_bootloader() { return 0; }
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
fail "boot authorization rejects Limine's false-success status when entries were not rebuilt"
+4 -2
View File
@@ -29,6 +29,8 @@ sbattach() {
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
@@ -235,7 +237,7 @@ if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
usb_authorization_enable_snapshot_setting "$limine_defaults"
fi
cp "$limine_defaults" "$esp/defaults.before"
if (export SIGN_FAIL=1; main "$action") >"$esp/main-failed" 2>&1; then
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"
@@ -244,7 +246,7 @@ if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
else
[[ $(<"$drop_in") == "$setting" ]] || fail "disable preflight failure must preserve the boot setting"
fi
(main "$action")
(usb_authorization_main "$action")
if [[ $action == enable ]]; then
[[ $(<"$drop_in") == "$setting" ]] || fail "successful enable sets the boot policy"
else
+183
View File
@@ -0,0 +1,183 @@
#!/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
usb_authorization_secure_boot_enabled() { return 0; }
usb_authorization_limine_settings() { printf 'x64 %s\n' "${ENROLLMENT:-yes}"; }
# Real PE enrollment always runs. An optional isolated sbctl configuration also
# exercises real signatures; default runs track signed content without host keys.
sbctl() {
if [[ $1 == sign && ${SIGN_FAIL:-0} == 1 ]]; then
return 1
fi
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
/usr/bin/sbctl --config "$USB_TEST_SBCTL_CONFIG" "$@"
elif [[ $1 == sign ]]; then
sha256sum "${!#}" | cut -d ' ' -f1 >>"$scratch/signed"
elif [[ $1 == verify ]]; then
local digest signed=0
digest=$(sha256sum "${!#}")
if grep -qx "${digest%% *}" "$scratch/signed"; then signed=1; fi
jq -n --arg file "${!#}" --argjson signed "$signed" '[{file_name: $file, is_signed: $signed}]'
fi
}
fixture() {
case_dir=$(mktemp -d "$scratch/case.XXXXXXXX")
boot_path="$case_dir/boot"
cache="$case_dir/cache"
limine_conf="$boot_path/limine.conf"
limine_defaults="$case_dir/defaults"
drop_in="$case_dir/dropins/usb-authorization.conf"
image="$boot_path/EFI/limine/limine_x64.efi"
mkdir -p "${image%/*}" "$cache/lss" "${drop_in%/*}"
printf 'initial menu\n' >"$limine_conf"
printf 'initial kernel\n' >"$boot_path/kernel.efi"
printf 'initial snapshot\n' >"$cache/lss/snapshots.json"
printf 'initial defaults\n' >"$limine_defaults"
: >"$scratch/signed"
cp /usr/share/limine/BOOTX64.EFI "$image"
if [[ ${ENROLLMENT:-yes} == yes ]]; then
digest=$(b2sum "$limine_conf")
limine enroll-config "$image" "${digest%% *}" --quiet
fi
sbctl sign "$image" >/dev/null
cp "$image" "$case_dir/original.efi"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
}
# Matches the installed wrapper: restore unsigned binary, enroll, attempt sign,
# then mask failure with a successful final sync. No real ESP is ever touched.
limine-enroll-config() {
cp /usr/share/limine/BOOTX64.EFI "$image"
if [[ ${ENROLLMENT:-yes} == yes && ${SKIP_ENROLLMENT:-0} != 1 ]]; then
local digest
digest=$(b2sum "$limine_conf")
limine enroll-config "$image" "${digest%% *}" --quiet || return 1
fi
sbctl sign "$image" || true
sync -f "$image"
}
# Relocate the outer real flock into scratch. The runner stands in for the
# namespace child and deliberately mutates the same files as rebuild hooks.
transaction=$(declare -f usb_authorization_boot_transaction)
transaction=${transaction//\/run\/lock\/boot-partition.lock/$scratch\/boot.lock}
eval "$transaction"
usb_authorization_run_transaction() {
if flock --nonblock "$scratch/boot.lock" true; then
fail "the real boot lock must cover the entire transaction"
fi
printf 'changed menu\n' >"$limine_conf"
printf 'changed kernel\n' >"$boot_path/kernel.efi"
printf 'changed metadata\n' >"$cache/lss/snapshots.json"
printf 'new cached backup\n' >"$cache/lss/snapshots.json.old"
mkdir -p "$boot_path/$(</etc/machine-id)/limine_history"
printf 'new archived backup\n' >"$boot_path/$(</etc/machine-id)/limine_history/snapshots.json.old"
printf 'changed defaults\n' >"$limine_defaults"
printf '%s\n' "$setting" >"$drop_in"
if [[ ${EARLY_FAILURE:-0} == 1 ]]; then
cp /usr/share/limine/BOOTX64.EFI "$image"
return 1
fi
if [[ ${UNCHECKED_HOOK:-0} == 1 ]]; then
limine-enroll-config
else
usb_authorization_enroll_config "$limine_conf" "$boot_path"
fi
}
for failure in SIGN_FAIL SKIP_ENROLLMENT EARLY_FAILURE; do
fixture
if (export "$failure=1"; usb_authorization_boot_transaction enable "$cache") >"$case_dir/failure.log" 2>&1; then
fail "$failure must reject the boot transition"
fi
cmp "$image" "$case_dir/original.efi" || fail "$failure must restore the signed bootloader byte-for-byte without signing"
[[ $(<"$limine_conf") == 'initial menu' ]] || fail "$failure must restore matching menu"
[[ $(<"$boot_path/kernel.efi") == 'initial kernel' ]] || fail "$failure must restore images referenced by that menu"
[[ $(<"$cache/lss/snapshots.json") == 'initial snapshot' ]] || fail "$failure must restore cached snapshot metadata"
[[ ! -e $cache/lss/snapshots.json.old && ! -e $boot_path/$(</etc/machine-id)/limine_history/snapshots.json.old ]] || fail "$failure must remove newly created recovery metadata"
[[ $(<"$limine_defaults") == 'initial defaults' && ! -e $drop_in ]] || fail "$failure must restore settings including an absent drop-in"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
done
pass "USB boot transitions restore signed Limine, matching menu, images, metadata and settings on failure"
fixture
printf 'original opt-in\n' >"$drop_in"
if (SIGN_FAIL=1 UNCHECKED_HOOK=1 usb_authorization_boot_transaction disable "$cache") >"$case_dir/failure.log" 2>&1; then
fail "final verification must reject false-success rebuild hooks"
fi
cmp "$image" "$case_dir/original.efi" || fail "false-success hooks must restore original signed Limine"
[[ $(<"$drop_in") == 'original opt-in' ]] || fail "failed disable must preserve an existing drop-in"
pass "USB boot transitions independently verify enrollment after external hooks"
for enrollment in yes no; do
export ENROLLMENT="$enrollment"
fixture
usb_authorization_boot_transaction enable "$cache"
[[ $(<"$limine_conf") == 'changed menu' && -e $drop_in ]] || fail "successful transition must commit"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
done
pass "USB boot transitions commit verified signing with enrollment enabled or disabled"
# Disposable Lab only: exercise the actual namespace runner and both lock
# inodes, with the installed enrollment/restore functions and real signatures.
if [[ ${USB_TEST_NAMESPACE:-0} == 1 ]]; then
(( EUID == 0 )) || fail "namespace tests require root in the disposable guest"
[[ -n ${USB_TEST_SBCTL_CONFIG:-} ]] || fail "namespace tests require isolated signing keys"
export ENROLLMENT=yes
for outcome in failure success; do
fixture
cp /usr/share/limine/BOOTX64.EFI "$case_dir/limine_x64.efi"
tar -cf "$case_dir/limine.bak" -C "$case_dir" limine_x64.efi
lock_inode=$(stat -Lc '%d:%i' /run/lock/boot-partition.lock)
# Source the shipped functions in both the parent and namespace child.
sed '/^if \[\[ ${BASH_SOURCE\[0\]} == "\$0" \]\]; then/,$d' "$ROOT/bin/omarchy-usb-authorization-boot" >"$case_dir/helper"
for variable in scratch case_dir boot_path cache limine_conf limine_defaults drop_in image lock_inode; do
printf '%s=%q\n' "$variable" "${!variable}" >>"$case_dir/helper"
done
declare -f sbctl usb_authorization_limine_settings usb_authorization_secure_boot_enabled >>"$case_dir/helper"
cat >>"$case_dir/helper" <<'SCRIPT'
usb_authorization_main() {
[[ $(stat -Lc '%d:%i' /run/lock/boot-partition.lock) != "$lock_inode" ]] || return 1
exec 9>/run/lock/boot-partition.lock
flock --nonblock 9 || return 1
printf 'changed menu\n' >"$limine_conf"
printf 'changed kernel\n' >"$boot_path/kernel.efi"
source /usr/lib/limine/limine-common-functions
LIMINE_DIR_PATH="${image%/*}"
LIMINE_EFI_FILE=limine_x64.efi
BINARY_TARGET_PATH="$image"
BINARY_BACKUP_PATH="$case_dir/limine.bak"
LIMINE_CONFIG_PATH="$limine_conf"
ENABLE_ENROLL_LIMINE_CONFIG=yes
is_sb_installed() { return 0; }
is_uefi() { return 0; }
limine-enroll-config() { enroll_config; }
usb_authorization_enroll_config "$limine_conf" "$boot_path"
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
usb_authorization_boot_transaction "$1" "$cache"
fi
SCRIPT
if [[ $outcome == failure ]]; then
if SIGN_FAIL=1 bash "$case_dir/helper" enable >"$case_dir/namespace.log" 2>&1; then
fail "actual namespace must reject installed wrapper's false success"
fi
grep -Fq 'Failed to sign' "$case_dir/namespace.log" || fail "failure must reach the installed enrollment function"
cmp "$image" "$case_dir/original.efi" || fail "namespace rollback restores original signed bootloader"
[[ $(<"$limine_conf") == 'initial menu' && $(<"$boot_path/kernel.efi") == 'initial kernel' ]] || fail "namespace rollback restores matching boot files"
else
bash "$case_dir/helper" disable >"$case_dir/namespace.log" 2>&1
[[ $(<"$limine_conf") == 'changed menu' ]] || fail "namespace success commits"
fi
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
flock --nonblock /run/lock/boot-partition.lock true || fail "outer lock must be released"
done
pass "USB real namespace isolates nested locks and recovers installed Limine signing failure"
fi