498 lines
15 KiB
Bash
Executable File
498 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Manage USB authorization from the start of kernel boot
|
|
# omarchy:args=<enable|disable>
|
|
# omarchy:requires-sudo=true
|
|
# omarchy:hidden=true
|
|
|
|
set -euo pipefail
|
|
|
|
drop_in=/etc/limine-entry-tool.d/usb-authorization.conf
|
|
setting='KERNEL_CMDLINE[default]+=" usbcore.authorized_default=0"'
|
|
limine_conf=/boot/limine.conf
|
|
limine_defaults=/etc/default/limine
|
|
boot_path=/boot
|
|
kernel_parameter=usbcore.authorized_default=0
|
|
snapshot_begin='# Omarchy USB authorization begin'
|
|
snapshot_setting='SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0'
|
|
snapshot_end='# Omarchy USB authorization end'
|
|
|
|
usb_authorization_cmdline_has_parameter() {
|
|
local cmdline="$1" parameter="$2" word
|
|
|
|
for word in $cmdline; do
|
|
[[ $word == "$parameter" ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
usb_authorization_secure_boot_enabled() {
|
|
local value variable
|
|
|
|
for variable in /sys/firmware/efi/efivars/SecureBoot-*; do
|
|
[[ -r $variable ]] || continue
|
|
value=$(od -An -t u1 -j 4 -N 1 "$variable")
|
|
value=${value//[[:space:]]/}
|
|
[[ $value == "1" ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
usb_authorization_verify_boot_entries() {
|
|
local expected="$1" config="$2" esp="$3"
|
|
local machine_id="${4:-$(</etc/machine-id)}"
|
|
local secure_boot="${5:-auto}"
|
|
local actual_hash embedded_cmdline expected_hash image mode path paths section
|
|
|
|
if [[ $secure_boot == "auto" ]]; then
|
|
if usb_authorization_secure_boot_enabled; then
|
|
secure_boot=enabled
|
|
else
|
|
secure_boot=disabled
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -r $config ]]; then
|
|
echo "Cannot verify the generated Limine boot entries in $config." >&2
|
|
return 1
|
|
fi
|
|
|
|
paths=$(mktemp /tmp/omarchy-usb-boot-paths.XXXXXXXXXX)
|
|
section=$(mktemp /tmp/omarchy-usb-boot-cmdline.XXXXXXXXXX)
|
|
|
|
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
|
function contains_parameter(value, count, fields, field_index) {
|
|
count = split(value, fields, /[[:space:]]+/)
|
|
for (field_index = 1; field_index <= count; field_index++) {
|
|
if (fields[field_index] == parameter) return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function finish_entry(found) {
|
|
if (!target_os || !is_kernel) return
|
|
entries++
|
|
|
|
if (has_cmdline) {
|
|
found = contains_parameter(cmdline)
|
|
if ((expected == "enabled" && !found) || (expected == "disabled" && found)) bad = 1
|
|
}
|
|
|
|
if (path == "") {
|
|
bad = 1
|
|
} else {
|
|
print (has_cmdline ? "external" : "embedded") "\t" path
|
|
}
|
|
}
|
|
|
|
/^\/[^/]/ {
|
|
finish_entry()
|
|
target_os = 0
|
|
is_kernel = 0
|
|
has_cmdline = 0
|
|
cmdline = ""
|
|
path = ""
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
|
target_os = index($0, "machine-id=" machine_id) > 0
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*\/\// {
|
|
finish_entry()
|
|
is_kernel = 0
|
|
has_cmdline = 0
|
|
cmdline = ""
|
|
path = ""
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*comment:[[:space:]]*kernel-id=/ {
|
|
is_kernel = target_os
|
|
next
|
|
}
|
|
|
|
is_kernel && /^[[:space:]]*path:[[:space:]]*/ {
|
|
path = $0
|
|
sub(/^[[:space:]]*path:[[:space:]]*/, "", path)
|
|
next
|
|
}
|
|
|
|
is_kernel && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
|
has_cmdline = 1
|
|
cmdline = $0
|
|
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", cmdline)
|
|
next
|
|
}
|
|
|
|
END {
|
|
finish_entry()
|
|
if (entries == 0) bad = 1
|
|
exit bad
|
|
}
|
|
' "$config" >"$paths"; then
|
|
rm -f "$paths" "$section"
|
|
echo "The generated Limine kernel entries do not all have USB authorization $expected." >&2
|
|
return 1
|
|
fi
|
|
|
|
while IFS=$'\t' read -r mode path; do
|
|
if [[ $path != 'boot():/'* ]]; then
|
|
rm -f "$paths" "$section"
|
|
echo "Cannot verify the kernel image referenced by Limine: $path" >&2
|
|
return 1
|
|
fi
|
|
|
|
image="$esp/${path#'boot():/'}"
|
|
image=${image%%#*}
|
|
if [[ ! -r $image ]]; then
|
|
rm -f "$paths" "$section"
|
|
echo "Cannot verify the kernel image referenced by Limine: $image" >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ $path == *'#'* ]]; then
|
|
expected_hash=${path##*#}
|
|
actual_hash=$(b2sum "$image")
|
|
actual_hash=${actual_hash%% *}
|
|
if [[ $actual_hash != "$expected_hash" ]]; then
|
|
rm -f "$paths" "$section"
|
|
echo "The verification hash for $image does not match the generated Limine entry." >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
: >"$section"
|
|
if ! objcopy -O binary --only-section=.cmdline "$image" "$section" 2>/dev/null; then
|
|
[[ $mode == "external" ]] && continue
|
|
rm -f "$paths" "$section"
|
|
echo "Cannot inspect the embedded kernel command line in $image." >&2
|
|
return 1
|
|
fi
|
|
embedded_cmdline=$(tr '\0' '\n' <"$section")
|
|
|
|
# Without Secure Boot, Limine's external command line takes precedence.
|
|
# Under Secure Boot, systemd-stub uses a nonempty embedded .cmdline.
|
|
if [[ $mode == "external" && $secure_boot == "disabled" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# A signed UKI can ignore Limine's external cmdline and use its embedded
|
|
# .cmdline instead. If one exists, both representations must agree.
|
|
if [[ -z $embedded_cmdline ]]; then
|
|
if [[ $mode == "external" || $expected == "disabled" ]]; then
|
|
continue
|
|
fi
|
|
rm -f "$paths" "$section"
|
|
echo "The generated kernel image $image has no verifiable kernel command line." >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ $expected == "enabled" ]]; then
|
|
if ! usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
|
rm -f "$paths" "$section"
|
|
echo "The generated kernel image $image does not enable boot-time USB authorization." >&2
|
|
return 1
|
|
fi
|
|
elif usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
|
rm -f "$paths" "$section"
|
|
echo "The generated kernel image $image still enables boot-time USB authorization." >&2
|
|
return 1
|
|
fi
|
|
done <"$paths"
|
|
|
|
rm -f "$paths" "$section"
|
|
}
|
|
|
|
usb_authorization_rebuild_and_verify() {
|
|
local expected="$1" config="$2" esp="$3"
|
|
local machine_id="${4:-$(</etc/machine-id)}" secure_boot="${5:-auto}"
|
|
|
|
if ! limine-mkinitcpio; then
|
|
return 1
|
|
fi
|
|
|
|
# limine-mkinitcpio-hook can swallow an individual kernel build failure and
|
|
# still return success, so its exit status alone is not a safety boundary.
|
|
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id" "$secure_boot"
|
|
}
|
|
|
|
usb_authorization_snapshot_block_status() {
|
|
local config="$1"
|
|
|
|
[[ -r $config ]] || return 2
|
|
awk -v begin="$snapshot_begin" -v setting="$snapshot_setting" -v end="$snapshot_end" '
|
|
$0 == begin { begin_count++; state = 1; next }
|
|
$0 == setting { setting_count++; if (state == 1) state = 2; next }
|
|
$0 == end { end_count++; if (state == 2) valid_count++; state = 0; next }
|
|
{ state = 0 }
|
|
END {
|
|
if (begin_count == 0 && setting_count == 0 && end_count == 0) exit 1
|
|
if (begin_count == 1 && setting_count == 1 && end_count == 1 && valid_count == 1) exit 0
|
|
exit 2
|
|
}
|
|
' "$config"
|
|
}
|
|
|
|
usb_authorization_enable_snapshot_setting() {
|
|
local config="$1" status temporary
|
|
|
|
if usb_authorization_snapshot_block_status "$config"; then
|
|
return 0
|
|
else
|
|
status=$?
|
|
fi
|
|
if (( status != 1 )); then
|
|
echo "Refusing to modify the USB authorization block in $config." >&2
|
|
return 1
|
|
fi
|
|
|
|
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
|
cp --preserve=mode,ownership "$config" "$temporary"
|
|
{
|
|
cat "$config"
|
|
printf '\n%s\n%s\n%s\n' "$snapshot_begin" "$snapshot_setting" "$snapshot_end"
|
|
} >"$temporary"
|
|
mv -f "$temporary" "$config"
|
|
}
|
|
|
|
usb_authorization_disable_snapshot_setting() {
|
|
local config="$1" status temporary
|
|
|
|
if usb_authorization_snapshot_block_status "$config"; then
|
|
status=0
|
|
else
|
|
status=$?
|
|
fi
|
|
(( status == 1 )) && return 0
|
|
if (( status != 0 )); then
|
|
echo "Refusing to modify the USB authorization block in $config." >&2
|
|
return 1
|
|
fi
|
|
|
|
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
|
cp --preserve=mode,ownership "$config" "$temporary"
|
|
awk -v begin="$snapshot_begin" '
|
|
$0 == begin { skip = 2; next }
|
|
skip > 0 { skip--; next }
|
|
{ print }
|
|
' "$config" >"$temporary"
|
|
mv -f "$temporary" "$config"
|
|
}
|
|
|
|
usb_authorization_rewrite_boot_cmdlines() {
|
|
local expected="$1" config="$2" machine_id="${3:-$(</etc/machine-id)}"
|
|
local backup temporary
|
|
|
|
backup=$(mktemp "${config}.omarchy-usb-backup.XXXXXXXXXX")
|
|
temporary=$(mktemp "${config}.omarchy-usb-new.XXXXXXXXXX")
|
|
cp --preserve=mode,ownership "$config" "$backup"
|
|
cp --preserve=mode,ownership "$config" "$temporary"
|
|
|
|
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
|
function rewrite_cmdline(line, prefix, value, count, fields, field_index, result, found) {
|
|
prefix = line
|
|
sub(/cmdline:.*/, "cmdline:", prefix)
|
|
value = line
|
|
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", value)
|
|
count = split(value, fields, /[[:space:]]+/)
|
|
result = ""
|
|
found = 0
|
|
for (field_index = 1; field_index <= count; field_index++) {
|
|
if (fields[field_index] == parameter) {
|
|
found = 1
|
|
continue
|
|
}
|
|
if (fields[field_index] != "") result = result (result == "" ? "" : " ") fields[field_index]
|
|
}
|
|
if (expected == "enabled") result = result (result == "" ? "" : " ") parameter
|
|
return prefix (result == "" ? "" : " ") result
|
|
}
|
|
|
|
/^\/[^/]/ { target_os = 0 }
|
|
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
|
target_os = index($0, "machine-id=" machine_id) > 0
|
|
}
|
|
target_os && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
|
print rewrite_cmdline($0)
|
|
next
|
|
}
|
|
{ print }
|
|
' "$config" >"$temporary"; then
|
|
rm -f "$backup" "$temporary"
|
|
return 1
|
|
fi
|
|
|
|
mv -f "$temporary" "$config"
|
|
if ! limine-enroll-config; then
|
|
mv -f "$backup" "$config"
|
|
limine-enroll-config >/dev/null 2>&1 || true
|
|
echo "Restored $config because its bootloader verification data could not be updated." >&2
|
|
return 1
|
|
fi
|
|
|
|
rm -f "$backup"
|
|
}
|
|
|
|
usb_authorization_rewrite_snapshot_manifest() {
|
|
local expected="$1" manifest="$2" temporary
|
|
|
|
[[ -e $manifest ]] || return 0
|
|
temporary=$(mktemp "${manifest}.omarchy-usb.XXXXXXXXXX")
|
|
if ! cp --preserve=mode,ownership "$manifest" "$temporary"; then
|
|
rm -f "$temporary"
|
|
return 1
|
|
fi
|
|
|
|
# Limine uses the cached lines when present, and structured command lines
|
|
# otherwise. Update both, including nested kernels and restore entries.
|
|
if ! jq --arg expected "$expected" --arg parameter "$kernel_parameter" '
|
|
def cmdline:
|
|
if type != "string" then error("Invalid snapshot command line") else
|
|
[splits("[[:space:]]+") | select(length > 0 and . != $parameter)]
|
|
| if $expected == "enabled" then . + [$parameter] else . end
|
|
| join(" ")
|
|
end;
|
|
def lines:
|
|
map(if test("^\\s*(cmdline|kernel_cmdline):"; "i") then
|
|
capture("^(?<prefix>\\s*(?:cmdline|kernel_cmdline):\\s*)(?<value>.*)$"; "i")
|
|
| .prefix + (.value | cmdline)
|
|
else . end);
|
|
def kernel:
|
|
.cmdlineDetails |= map(.cmdline |= cmdline | .snapshotCmdline |= cmdline)
|
|
| .allInConfig |= lines
|
|
| .allInSnapshotConfig |= lines
|
|
| .subKernels |= map(kernel);
|
|
if .jsonFormatVersion != "1.3.0" or (.snapshotEntries | type) != "array" then
|
|
error("Unsupported Limine snapshot manifest format")
|
|
else
|
|
.snapshotEntries |= map(.kernelEntries |= map(kernel))
|
|
end
|
|
' "$manifest" >"$temporary"; then
|
|
rm -f "$temporary"
|
|
echo "Cannot update stored USB boot policy in $manifest." >&2
|
|
return 1
|
|
fi
|
|
|
|
mv -f "$temporary" "$manifest"
|
|
}
|
|
|
|
usb_authorization_sync_and_verify() (
|
|
local expected="$1" config="$2" esp="$3"
|
|
local machine_id="${4:-$(</etc/machine-id)}" manifest
|
|
|
|
# Share Limine's lock so a concurrent watcher cannot restore an old manifest
|
|
# while we update it. The sync wrapper must not reacquire our lock.
|
|
exec 9>/run/lock/boot-partition.lock
|
|
flock --timeout 30 9 || return 1
|
|
|
|
# Recover/upgrade the manifest first, including backups if necessary.
|
|
limine-snapper-sync --no-mutex --no-hooks || return 1
|
|
for manifest in \
|
|
"$esp/$machine_id/limine_history/snapshots.json" \
|
|
"$esp/$machine_id/limine_history/snapshots.json.old" \
|
|
"/var/cache/boot/$machine_id/lss/snapshots.json"; do
|
|
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest" || return 1
|
|
done
|
|
|
|
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine_id" || return 1
|
|
# 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_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
|
|
)
|
|
|
|
main() {
|
|
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
|
|
|
|
if (( EUID != 0 )); then
|
|
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${1:-}" in
|
|
enable)
|
|
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
|
snapshot_status=0
|
|
else
|
|
snapshot_status=$?
|
|
fi
|
|
if (( snapshot_status == 2 )); then
|
|
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Quote the expected value because the Limine key contains glob brackets.
|
|
if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then
|
|
echo "Refusing to replace the existing $drop_in." >&2
|
|
exit 1
|
|
fi
|
|
|
|
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
|
|
|
if [[ ! -e $drop_in ]]; then
|
|
install -d -m755 "${drop_in%/*}"
|
|
temporary=$(mktemp "${drop_in}.XXXXXXXXXX")
|
|
trap 'rm -f "$temporary"' EXIT
|
|
printf '%s\n' "$setting" >"$temporary"
|
|
chmod 644 "$temporary"
|
|
mv -f "$temporary" "$drop_in"
|
|
trap - EXIT
|
|
fi
|
|
|
|
if ! limine-mkinitcpio ||
|
|
! usb_authorization_sync_and_verify enabled "$limine_conf" "$boot_path"; then
|
|
echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
disable)
|
|
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
|
had_snapshot_setting=1
|
|
else
|
|
snapshot_status=$?
|
|
if (( snapshot_status == 2 )); then
|
|
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ -e $drop_in ]]; then
|
|
if [[ $(<"$drop_in") != "$setting" ]]; then
|
|
echo "Refusing to remove the modified $drop_in." >&2
|
|
exit 1
|
|
fi
|
|
had_drop_in=1
|
|
rm -f "$drop_in"
|
|
fi
|
|
|
|
usb_authorization_disable_snapshot_setting "$limine_defaults"
|
|
if ! limine-mkinitcpio ||
|
|
! usb_authorization_sync_and_verify disabled "$limine_conf" "$boot_path"; then
|
|
if (( had_drop_in )); then
|
|
printf '%s\n' "$setting" >"$drop_in"
|
|
chmod 644 "$drop_in"
|
|
fi
|
|
if (( had_snapshot_setting )); then
|
|
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
|
fi
|
|
echo "USBGuard remains required because the boot images could not be verified as default-allow." >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
main "$@"
|
|
fi
|