Merge pull request #11874 from acrogenesis/feature/usb-device-authorization

Require approval for new USB and Thunderbolt devices by default.

Reported-by: Jurre van Bergen me@jurrevanbergen.nl
This commit is contained in:
Erik Melton authored and GitHub committed 2026-10-09 21:16:12 +02:00
commit 5cb31317b4
57 files changed
+5775 -2

No files matched your search

+9
View File
@@ -1034,6 +1034,15 @@ run_provisioning() {
limine-update
fi
if omarchy-pkg-present usbguard; then
log_step "enrolling the owner's USB devices"
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
usb_authorization_provision_owner "$username"
fi
log_step "enrolling the owner's Thunderbolt accessories"
/usr/bin/omarchy-thunderbolt-authorization-admin owner
log_step "cleaning up provisioning state"
cleanup_oem_state
log_step "first-boot setup complete"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# omarchy:summary=Restore Bolt's default authorization behavior
set -euo pipefail
gum confirm "Restore Bolt's previous policy? New accessories may be authorized automatically when IOMMU protection is active." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin disable
systemctl --user disable --now omarchy-thunderbolt-authorization.service
echo "Previous Bolt policy restored. Saved Omarchy trust is retained for re-enabling."
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:summary=Restore saved Thunderbolt firmware boot permissions
set -euo pipefail
gum confirm "Restore the previous Thunderbolt firmware boot allowlists? Device approval will remain enabled." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-disable
echo "Previous firmware boot permissions restored and verified."
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# omarchy:summary=Stop asking before new USB devices can access the system
# omarchy:args=[--boot-only] [--yes]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
boot_only=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot-only) boot_only=1 ;;
-h | --help)
echo "Usage: omarchy-remove-security-usb-authorization [--boot-only] [--yes]"
echo
echo "Disables Omarchy's USB approval prompts and restores the kernel's"
echo "normal default-allow behavior."
echo "Use --boot-only to keep USBGuard enabled after the system starts."
exit 0
;;
*)
echo "omarchy-remove-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
if (( boot_only )); then
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB authorization from boot"
echo
echo "USBGuard will continue blocking unknown devices after its service starts."
echo "Devices can reach kernel drivers earlier during boot."
echo
gum confirm "Disable USB authorization from kernel boot?" || exit 130
fi
sudo omarchy-usb-authorization-boot disable
echo
echo "Boot-time USB authorization is disabled and takes effect after reboot."
exit 0
fi
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB device authorization"
echo
echo "New USB devices will be allowed to bind to kernel drivers without approval."
echo
gum confirm "Disable USB device authorization?" || exit 130
fi
# Rebuild a permissive boot image before stopping USBGuard. If rebuilding
# fails, the command stops here and leaves the complete policy enabled.
sudo omarchy-usb-authorization-boot disable
if omarchy-pkg-present usbguard; then
# Keep the watcher and its IPC access until blocking has actually stopped.
# In particular, a failed stop must not strand a running daemon without UI.
sudo systemctl disable --now usbguard.service
if systemctl is-active --quiet usbguard.service || systemctl is-enabled --quiet usbguard.service; then
echo "USBGuard is still active or enabled; approval prompts have been preserved." >&2
exit 1
fi
sudo omarchy-usb-authorization-restore-default
sudo usbguard remove-user "$USER"
fi
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
rm -f "$unit"
systemctl --user daemon-reload
if [[ -d $state_dir/requests ]]; then
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
rmdir "$state_dir/requests" 2>/dev/null || true
fi
rm -f "$state_dir/package-installed-by-omarchy" \
"$state_dir/policy-generated-by-omarchy" \
"$state_dir/service-enabled-by-omarchy"
rmdir "$state_dir" 2>/dev/null || true
echo "USB device authorization is disabled."
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# omarchy:summary=Require approval for new Thunderbolt PCIe accessories
# omarchy:args=[--quiet|status|recover]
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
if [[ ${1:-} == "status" ]]; then
tb_status
elif [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin recover
elif [[ $# == 0 || ( $# == 1 && $1 == "--quiet" ) ]]; then
if [[ ${1:-} != "--quiet" ]]; then
if [[ -f $TB_PENDING ]]; then tb_status; fi
echo "First setup, including a deferred setup, trusts connected accessories. Re-enabling a completed setup preserves saved trust."
gum confirm "Require approval for new Thunderbolt accessories?" || exit 0
fi
# System units and rules are shipped by omarchy-settings.
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
sudo systemctl daemon-reload
action=enable
if [[ ${1:-} == "--quiet" ]]; then
action=migrate
fi
sudo /usr/bin/omarchy-thunderbolt-authorization-admin "$action"
systemctl --user daemon-reload
systemctl --user enable --now omarchy-thunderbolt-authorization.service
if [[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]; then
for ((attempt=0; attempt<10; attempt++)); do
if tb_public_snapshot >/dev/null 2>&1; then break; fi
sleep 1
done
tb_status
elif [[ -f $TB_PENDING ]]; then
tb_status
fi
else
echo "Usage: omarchy-setup-security-thunderbolt-authorization [--quiet|status|recover]" >&2
exit 1
fi
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# omarchy:summary=Clear supported Thunderbolt firmware boot allowlists
# omarchy:args=[recover]
set -euo pipefail
if [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-recover
elif [[ $# == 0 ]]; then
gum style --bold "Thunderbolt at Boot"
echo "This trusts connected accessories for use after Linux starts, then clears the firmware boot allowlist."
echo "Trusted Thunderbolt keyboards, storage, and network adapters may be unavailable at disk unlock."
echo "You must be able to unlock and recover without Thunderbolt accessories."
echo "All controllers must be connected and support a boot allowlist in user or secure mode."
echo "Older snapshots or another operating system may restore firmware boot permissions."
gum confirm "Clear firmware boot access with these recovery limits?" || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-enable
echo "Firmware boot allowlists cleared and verified. Current connections were not disconnected."
else
echo "Usage: omarchy-setup-security-thunderbolt-boot [recover]" >&2
exit 1
fi
+184
View File
@@ -0,0 +1,184 @@
#!/bin/bash
# omarchy:summary=Ask before new USB devices can access the system
# omarchy:args=[--boot] [--yes] [--review-existing]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
from_boot=0
review_existing=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot) from_boot=1 ;;
--review-existing) review_existing=1 ;;
-h | --help)
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes] [--review-existing]"
echo
echo "Trusts USB devices that are connected now, then blocks new devices"
echo "until you approve them from an Omarchy notification."
echo
echo "--boot also blocks USB devices from the start of kernel boot. Connected"
echo "devices are added to USBGuard's policy, but remain unavailable before"
echo "USBGuard starts, including at an encrypted-disk password prompt."
echo "Older snapshots without USBGuard keep all USB disabled after startup,"
echo "including keyboards, network adapters, and storage. Use --boot only if"
echo "disk unlock and recovery work without any USB devices."
echo
echo "--review-existing reviews connected devices with older port-bound approvals."
echo "Each portable approval requires confirmation; existing rules are preserved."
exit 0
;;
*)
echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
rules_file=/etc/usbguard/rules.conf
daemon_config=/etc/usbguard/usbguard-daemon.conf
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
if (( review_existing )); then
# Older Omarchy rules have no per-rule ownership marker. They cannot safely
# be distinguished from intentionally restricted administrator rules.
(( ! assume_yes && ! from_boot )) || { echo "Review requires individual confirmation; do not combine it with --yes or --boot." >&2; exit 2; }
policy=$(usbguard list-rules)
declare -A reviewed=()
# Keep stdin attached to the terminal for gum (and any sudo prompt).
mapfile -t policy_lines <<<"$policy"
for line in "${policy_lines[@]}"; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
rule=${line#*: }
[[ $rule == allow\ * ]] || continue
portable=$(usb_authorization_portable_rule "$rule") || continue
[[ ${portable% label *} != "$rule" ]] || continue
devices=$(usbguard list-devices "$rule")
mapfile -t candidates <<<"$devices"
for device in "${candidates[@]}"; do
[[ $device =~ ^([0-9]+):[[:space:]](allow[[:space:]].*)$ ]] || continue
id=${BASH_REMATCH[1]} current=${BASH_REMATCH[2]}
[[ ! -v reviewed[$id] ]] || continue
reviewed[$id]=1
echo
printf '%s\n' "$current" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo "This device has a location-bound approval. Details are supplied by the device."
if gum confirm "Trust this device on any USB port or hub?"; then
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "$current"
fi
done
fi
done
echo "Review complete. Disconnected devices can be approved when reconnected."
exit 0
fi
# The watcher runs in the session's environment, which may differ from a
# command invoked in a checkout. Fail before enabling blocking on mismatched
# or incomplete installations, including dangling dev-link unit symlinks.
session_environment=$(systemctl --user show-environment)
grep -Fqx -- "OMARCHY_PATH=$OMARCHY_PATH" <<<"$session_environment" || {
echo "The session and this command use different Omarchy installations. Update the session before enabling USB protection." >&2
exit 1
}
for required in "$unit_source" /usr/share/omarchy/install/helpers/usb-authorization-policy.sh; do
[[ -r $required ]] || { echo "USB authorization installation is incomplete: $required" >&2; exit 1; }
done
for required in "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-review" \
/usr/bin/omarchy-usb-authorization-approve; do
[[ -r $required && -x $required ]] || { echo "USB authorization command is unavailable: $required" >&2; exit 1; }
done
if (( ! assume_yes )); then
if (( from_boot )); then
gum style --foreground 212 --bold "USB authorization from boot"
else
gum style --foreground 212 --bold "USB device authorization"
fi
echo
echo "Devices connected now will be trusted. Disconnect anything you do not trust."
echo "New USB devices will remain blocked until you approve them."
if (( from_boot )); then
echo
echo "Boot-time protection blocks all USB devices until USBGuard starts."
echo "A USB keyboard will not work at an encrypted-disk password prompt, even"
echo "when it is connected now."
echo
echo "Older snapshots without USBGuard keep ALL USB disabled after startup,"
echo "including keyboards, network adapters, and storage."
echo "Continue only if disk unlock and recovery work without any USB devices."
fi
echo
if (( from_boot )); then
gum confirm "Enable USB authorization from kernel boot?" || exit 130
else
gum confirm "Enable USB device authorization?" || exit 130
fi
fi
omarchy-pkg-add usbguard
usb_authorization_require_secure_settings "$daemon_config"
mkdir -p "$state_dir" "${unit%/*}"
chmod 700 "$state_dir"
if ! usb_authorization_rules_present "$rules_file"; then
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX")
trap 'rm -f "$policy"' EXIT
chmod 600 "$policy"
usb_authorization_generate_policy "$policy"
sudo install -Dm600 -o root -g root "$policy" "$rules_file"
touch "$state_dir/policy-generated-by-omarchy"
fi
# This user can inspect USBGuard events and approve the device named by an
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
sudo usbguard add-user "$USER" \
--devices=list,listen,modify \
--policy=list \
--exceptions=listen
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# Install a real file: a link into a checkout can become dangling on a branch
# switch. Start the waiting watcher before USBGuard can block new devices.
unit_temporary=$(mktemp "${unit}.XXXXXXXXXX")
install -m644 "$unit_source" "$unit_temporary"
mv -fT "$unit_temporary" "$unit"
systemctl --user daemon-reload
systemctl --user enable omarchy-usb-authorization.service
systemctl --user restart omarchy-usb-authorization.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
sudo systemctl enable usbguard.service
# add-user ACL changes are loaded only when the daemon starts, so restart an
# existing service as well as starting a fresh one.
sudo systemctl restart usbguard.service
systemctl is-active --quiet usbguard.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
if (( from_boot )); then
# The fixed helper independently verifies each full live device snapshot.
usb_authorization_trust_present_devices
sudo omarchy-usb-authorization-boot enable
fi
echo
echo "USB device authorization is enabled."
echo "Devices connected during setup remain trusted; new devices require approval."
echo "To review older port-bound approvals, run: omarchy-setup-security-usb-authorization --review-existing"
if (( from_boot )); then
echo "Boot-time authorization is enabled and takes effect after reboot."
fi
+21
View File
@@ -320,6 +320,12 @@ scrub_factory_accounts() {
sanitize_factory_baseline() {
local factory="$1"
btrfs property set -ts "$factory" ro false
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
btrfs property set -ts "$factory" ro true
fail "could not reset Thunderbolt authorization in the factory baseline"
fi
fi
if ! scrub_factory_accounts "$factory"; then
btrfs property set -ts "$factory" ro true
@@ -345,6 +351,14 @@ stage_full_reset() {
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
fail "the factory snapshot predates provisioning support; cannot reset from it"
# A normal install's factory snapshot includes the original owner's USB
# policy and enabled daemon. Keep input usable during the new owner's setup;
# provisioning enrolls their devices before enabling USBGuard again.
if [[ -f $next/usr/lib/systemd/system/usbguard.service ]]; then
systemctl --root="$next" disable usbguard.service >>"$LOG_FILE" 2>&1 ||
fail "could not disable USBGuard in the factory system (see $LOG_FILE)"
fi
log "Scrubbing machine identity from the factory system"
systemd-id128 new >"$next/etc/machine-id"
rm -f "$next"/etc/ssh/ssh_host_*
@@ -360,6 +374,13 @@ stage_full_reset() {
scrub_factory_accounts "$next" ||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
# Leave the new owner's input available before interactive enrollment. Clear
# inherited accessory identities in both roots; never change the live daemon.
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
fail "could not reset Thunderbolt authorization in the staged root"
fi
# @factory itself survives the wipe as the baseline for future resets. If it
# came from a normal install it still holds the seller's account and
# /etc/shadow, which the new wheel user could mount and read — and a second
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash -p
# omarchy:summary=Manage system Thunderbolt authorization settings
# omarchy:hidden=true
# This root entrypoint uses installed code and rejects unsafe Bash startup.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
source /usr/share/omarchy/install/helpers/thunderbolt-setup.sh
tb_admin "$@"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash -p
# omarchy:summary=Approve one verified Thunderbolt connection
# omarchy:hidden=true
# pkexec permits this exact program for an active local wheel user. Suppress
# startup injection before reading input; it cannot dispatch general root work.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
[[ $# == 2 && ( $1 == "true" || $1 == "false" ) ]] || exit 64
(( ${#2} <= 16384 )) || exit 64
jq -e 'type == "object"' <<< "$2" >/dev/null
exec 9> "$TB_LOCK"
flock -x 9
result=$(tb_approve "$2" "$1" true)
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$result"
printf '%s\n' "$result"
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Check whether Thunderbolt firmware boot access is restricted
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
tb_public_snapshot | jq -e .boot_protection >/dev/null
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash -p
# omarchy:summary=Restore trusted Thunderbolt devices and publish their status
# omarchy:hidden=true
# Keep root startup independent of a caller's environment or checkout.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
tb_daemon
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval review
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
[[ $# == 1 ]] || exit 64
tb_review "$1"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval watch
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
tb_watch
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash -p
# omarchy:hidden=true
# omarchy:summary=Save portable trust for one verified USB device
# This narrow Polkit boundary accepts a live device snapshot, never a policy or
# file path. Privileged Bash suppresses startup code from the caller's environment.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset USBGUARD_DAEMON_CONF USBGUARD_DEBUG
source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh
(( $# == 2 )) || exit 64
# Omarchy persists to this file. Refuse custom daemon storage rather than
# mistaking a stale copy of a rule for a successful persistent decision.
rules_file=$(awk -F= '/^[[:space:]]*RuleFile[[:space:]]*=/ { value=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value; exit }' /etc/usbguard/usbguard-daemon.conf)
[[ $rules_file == "/etc/usbguard/rules.conf" ]] || exit 1
exec 8>/run/omarchy-usb-authorization.lock
flock -x 8
usb_authorization_permanent "$1" "$2"
+809
View File
@@ -0,0 +1,809 @@
#!/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 ! usb_authorization_enroll_config "$config" "${config%/*}"; then
mv -f "$backup" "$config"
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
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_verify_signature() {
# sbctl verify can return zero for unsigned or unreadable files. Check its
# structured result, including the exact path, instead of just the exit code.
if ! sbctl verify --json "$1" | jq -e --arg file "$1" \
'length == 1 and .[0].file_name == $file and .[0].is_signed == 1' >/dev/null; then
echo "Cannot verify the Secure Boot signature of $1 using this machine's signing keys." >&2
return 1
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() (
local expected="$1" config="$2" esp="$3" machine_id="$4"
local cache="${5:-/var/cache/boot/$machine_id/lss/snapshots.json}"
local history="$esp/$machine_id/limine_history" staging manifest names name image
local cmdline hash_command old_digest new_digest old_hash new_hash replacement index=0
local published=0 committed=0 section layout image_base image_size alignment address
local -a manifests=() originals=()
usb_authorization_secure_boot_enabled || return 0
[[ -d $history ]] || return 0
staging=$(mktemp -d "$esp/.omarchy-usb-uki.XXXXXXXXXX") || return 1
trap '
if (( published && ! committed )); then
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
done
fi
rm -rf "$staging"
' EXIT
for manifest in "$history/snapshots.json" "$history/snapshots.json.old" "$cache"; do
[[ -f $manifest ]] || continue
jq -e '.jsonFormatVersion == "1.3.0" and (.snapshotEntries | type == "array")' "$manifest" >/dev/null || return 1
manifests+=("$manifest")
done
(( ${#manifests[@]} )) || return 0
names=$(jq -sr '[.[] | .snapshotEntries[] | .. | objects | .imageDetails? // empty | .[] | .fileHashName] | unique[]' "${manifests[@]}") || return 1
printf '[]\n' >"$staging/replacements.json"
while IFS= read -r name; do
[[ -n $name ]] || continue
# Only image names, never arbitrary paths supplied by a manifest.
if [[ $name == */* || $name == "." || $name == ".." ]]; then
echo "Invalid archived kernel image name: $name" >&2
return 1
fi
image="$history/$name"
[[ -f $image && ! -L $image ]] || continue
: >"$staging/cmdline"
objcopy -O binary --only-section=.cmdline "$image" "$staging/cmdline" 2>/dev/null || continue
cmdline=$(tr '\0' '\n' <"$staging/cmdline")
[[ -n $cmdline ]] || continue
if usb_authorization_cmdline_has_parameter "$cmdline" "$kernel_parameter"; then
[[ $expected == "disabled" ]] || continue
else
[[ $expected == "enabled" ]] || continue
fi
# Multi-profile images and signed PCR policies need their original builder.
# Do not silently invalidate their additional boot/unlock guarantees.
for section in .profile .pcrsig; do
objcopy -O binary --only-section="$section" "$image" "$staging/section" || return 1
if [[ -s $staging/section ]]; then
echo "Rebuild archived image $image with its original signing policy before changing USB boot protection ($section)." >&2
return 1
fi
done
if [[ ! $name =~ ^(.+)_(sha1|sha256|b3|xxh)_([0-9a-f]+)$ ]]; then
echo "Cannot verify the content hash of archived image $image." >&2
return 1
fi
case "${BASH_REMATCH[2]}" in
sha1) hash_command=sha1sum ;;
sha256) hash_command=sha256sum ;;
b3) hash_command=b3sum ;;
xxh) hash_command=xxhsum ;;
esac
old_digest=${BASH_REMATCH[3]}
new_digest=$("$hash_command" "$image") || return 1
if [[ ${new_digest%% *} != "$old_digest" ]]; then
echo "Archived image hash mismatch: $image" >&2
return 1
fi
# Verify the old signature before signing a modified copy with this machine's key.
usb_authorization_verify_signature "$image" || return 1
cp "$image" "$staging/unsigned.efi" || return 1
sbattach --remove "$staging/unsigned.efi" || return 1
# Preserve the embedded boot arguments and change only our exact USB
# parameter. Do not turn a signed command line into an external override.
printf '%s' "$cmdline" | jq -Rrs --arg parameter "$kernel_parameter" --arg expected "$expected" '
[scan("[^[:space:]]+|[[:space:]]+") | select(. != $parameter)]
| join("") | sub("[[:space:]]+$"; "")
| if $expected == "enabled" then . + " " + $parameter else . end
' >"$staging/new-cmdline" || return 1
printf '\0' >>"$staging/new-cmdline"
# objcopy --update-section retains PE VirtualSize and can truncate a longer
# command line. Add a fresh section after the existing virtual image instead.
layout=$(objdump -p "$staging/unsigned.efi" | awk '
$1 == "ImageBase" { base = $2 }
$1 == "SizeOfImage" { size = $2 }
$1 == "SectionAlignment" { alignment = $2 }
END {
if (base !~ /^[0-9a-fA-F]+$/ || size !~ /^[0-9a-fA-F]+$/ || alignment !~ /^[0-9a-fA-F]+$/) exit 1
print "0x" base, "0x" size, "0x" alignment
}
') || return 1
read -r image_base image_size alignment <<<"$layout"
(( alignment > 0 )) || return 1
address=$((image_base + (image_size + alignment - 1) / alignment * alignment))
objcopy --remove-section=.cmdline "$staging/unsigned.efi" "$staging/without-cmdline.efi" || return 1
objcopy --add-section .cmdline="$staging/new-cmdline" \
--change-section-vma .cmdline="$address" \
--set-section-flags .cmdline=alloc,load,readonly,data,contents \
"$staging/without-cmdline.efi" "$staging/image.efi" || return 1
sbctl sign "$staging/image.efi" || return 1
usb_authorization_verify_signature "$staging/image.efi" || return 1
objcopy -O binary --only-section=.cmdline "$staging/image.efi" "$staging/cmdline" || return 1
cmp -s "$staging/cmdline" "$staging/new-cmdline" || return 1
new_digest=$("$hash_command" "$staging/image.efi") || return 1
replacement="${name%"$old_digest"}${new_digest%% *}"
old_hash=$(b2sum "$image") || return 1
new_hash=$(b2sum "$staging/image.efi") || return 1
# Publish copies first: every old reference remains usable until its manifest changes.
if [[ -e $history/$replacement || -L $history/$replacement ]]; then
[[ ! -L $history/$replacement ]] && cmp -s "$staging/image.efi" "$history/$replacement" || return 1
else
mv "$staging/image.efi" "$history/$replacement" || return 1
fi
jq --arg old "$name" --arg new "$replacement" \
--arg old_hash "${old_hash%% *}" --arg new_hash "${new_hash%% *}" \
'. + [{old: $old, new: $new, old_hash: $old_hash, new_hash: $new_hash}]' \
"$staging/replacements.json" >"$staging/next.json" || return 1
mv "$staging/next.json" "$staging/replacements.json" || return 1
done <<<"$names"
jq -e 'length > 0' "$staging/replacements.json" >/dev/null || return 0
originals=("$config" "${manifests[@]}")
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "${originals[index]}" "$staging/original.$index" || return 1
cp --preserve=mode,ownership "${originals[index]}" "$staging/updated.$index" || return 1
# Update cached menu lines, structured image metadata, restore paths and
# both hashes. The new file name remains a valid Limine content address.
if (( index == 0 )); then
jq -Rrs --slurpfile replacements "$staging/replacements.json" '
split("\n") | map(reduce $replacements[0][] as $r (. ;
if contains($r.old) then split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash)
else . end)) | join("\n")
' "$staging/original.$index" >"$staging/updated.$index" || return 1
else
jq --slurpfile replacements "$staging/replacements.json" '
walk(if type == "string" then
reduce $replacements[0][] as $r (. ; split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash))
else . end)
' "$staging/original.$index" >"$staging/updated.$index" || return 1
fi
done
published=1
for ((index=0; index<${#originals[@]}; index++)); do
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
done
usb_authorization_enroll_config "$config" "$esp" || return 1
committed=1
)
usb_authorization_prepare_archived_images() (
local expected="$1" config="$2" esp="$3" machine_id="${4:-$(</etc/machine-id)}"
usb_authorization_secure_boot_enabled || return 0
# Package hooks acquire the boot lock too; install this optional tool before
# taking it, never while rewriting archived images.
if omarchy-cmd-missing sbattach; then
omarchy-pkg-add sbsigntools || return 1
fi
exec 9>/run/lock/boot-partition.lock
flock --timeout 30 9 || return 1
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id"
)
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
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id" || 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
usb_authorization_enroll_config "$config" "$esp" || return 1
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
)
usb_authorization_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_prepare_archived_images enabled "$limine_conf" "$boot_path" || exit 1
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
fi
usb_authorization_prepare_archived_images disabled "$limine_conf" "$boot_path" || exit 1
if (( had_drop_in )); then
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
}
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
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Create an approval request for a blocked USB device
set -euo pipefail
case "${USBGUARD_IPC_SIGNAL:-}" in
IPC.Connected) ;;
Device.PolicyApplied)
# PresenceChanged precedes policy evaluation, even for trusted devices.
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
;;
Device.PresenceChanged)
[[ ${USBGUARD_DEVICE_EVENT:-} == "Remove" ]] || exit 0
;;
*) exit 0 ;;
esac
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
mkdir -p "$requests"
chmod 700 "${requests%/*}" "$requests"
exec 9>"$requests/.lock"
request_generation() {
local daemon_id
# USBGuard reuses device IDs after restarting. Include both the daemon
# invocation and watcher lifetime so old prompts cannot suppress new ones.
daemon_id=$(systemctl show usbguard.service --property=InvocationID --value) || return 1
[[ $daemon_id =~ ^[0-9a-f]{32}$ ]] || return 1
printf '%s:%s' "${OMARCHY_USB_AUTHORIZATION_WATCH_ID:-standalone}" "$daemon_id"
}
if [[ $USBGUARD_IPC_SIGNAL == "IPC.Connected" ]]; then
flock 9
generation=$(request_generation) || exit 1
for pending in "$requests"/request-*.json; do
[[ -f $pending ]] || continue
if ! jq -e --arg generation "$generation" '.generation == $generation' "$pending" >/dev/null 2>&1; then
rm -f "$pending"
fi
done
flock -u 9
# USBGuard scans present devices before accepting IPC connections. Repeat
# this on every reconnect: temporary approvals disappear on daemon restart.
blocked=$(usbguard list-devices --blocked) || exit 1
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" || true
fi
done <<<"$blocked"
exit 0
fi
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
# A removed device's final target may be allow even though a blocked request
# remains. Its identity attributes are unchanged; normalize the target.
USBGUARD_DEVICE_RULE="block ${USBGUARD_DEVICE_RULE#* }"
# The token is safe to carry through the presentation launcher's command
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
# is never interpolated into a shell command.
token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" | sha256sum | cut -d' ' -f1)"
request="$requests/$token.json"
# watch executes handlers concurrently. A reconnect scan and a policy event
# can refer to the same device; publish only one pending request.
if [[ $USBGUARD_IPC_SIGNAL == "Device.PresenceChanged" ]]; then
flock 9
blocked=$(usbguard list-devices --blocked) || exit 1
# A delayed removal event must not erase a request for a reappeared device.
if ! grep -Fqx -- "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" <<<"$blocked"; then
rm -f "$request"
fi
exit 0
fi
# Keep retrying without needing another USB event. Handlers belong to the
# watcher service and are stopped with the graphical session.
while true; do
flock 9
generation=$(request_generation) || exit 1
if [[ -f $request ]] && jq -e --arg generation "$generation" '.generation == $generation' "$request" >/dev/null 2>&1; then
exit 0
fi
# Discard events overtaken by another policy decision or device replacement.
blocked=$(usbguard list-devices --blocked) || exit 1
current=0
while IFS= read -r line; do
if [[ $line == "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" ]]; then
current=1
break
fi
done <<<"$blocked"
(( current )) || exit 0
# Login and shell restarts can leave D-Bus alive before notifications are ready.
# Bound each wait so disappeared devices are rechecked between attempts.
if ! omarchy-notification-wait 1; then
flock -u 9
sleep 1
continue
fi
temporary=$(mktemp "$requests/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq -n \
--arg id "$USBGUARD_DEVICE_ID" \
--arg rule "$USBGUARD_DEVICE_RULE" \
--arg generation "$generation" \
'{id: $id, rule: $rule, generation: $generation}' >"$temporary"
chmod 600 "$temporary"
mv -f "$temporary" "$request"
if omarchy-notification-send \
--app-name omarchy-action \
--urgency critical \
--glyph 󰖀 \
"USB accessory blocked" \
"Click to review it. Keep it blocked unless you recognize the device." \
--exec omarchy-launch-floating-terminal-with-presentation \
omarchy-usb-authorization-review "$token"; then
exit 0
fi
rm -f "$request"
flock -u 9
sleep 1
done
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# omarchy:summary=Restore the kernel's default USB authorization state
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2
exit 1
fi
# USBGuard intentionally leaves root hubs in default-deny mode when it exits.
# Restore both the hub defaults and devices that are still connected.
shopt -s nullglob
for attribute in /sys/bus/usb/devices/usb*/authorized_default; do
echo 1 >"$attribute"
done
for attribute in /sys/bus/usb/devices/*/authorized; do
echo 1 >"$attribute"
done
shopt -u nullglob
+142
View File
@@ -0,0 +1,142 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Review a blocked USB device authorization request
set -euo pipefail
token="${1:-}"
if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
echo "This USB authorization request is invalid or expired." >&2
exit 1
fi
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
# Serialize request updates with watcher events, but release the lock while the
# user chooses. A changed request must never inherit an old dialog's approval.
if [[ ! -d ${request%/*} ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
exec 9>"${request%/*}/.lock"
flock 9
if [[ ! -f $request || -L $request ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
snapshot=$(cat "$request")
id=$(jq -er '.id | select(test("^[0-9]+$"))' <<<"$snapshot")
expected_rule=$(jq -er '.rule | select(type == "string" and startswith("block "))' <<<"$snapshot")
choice=$(jq -er '.approval // "" | select(. == "" or . == "Allow once" or . == "Always allow this device")' <<<"$snapshot")
allowed_rule="allow ${expected_rule#block }"
approval_failed() {
echo "Could not confirm USB approval. The request has been kept; try again." >&2
exit 1
}
read_current_rule() {
local devices line
current_rule=""
devices=$(usbguard list-devices) || approval_failed
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then
current_rule="${line#"$id: "}"
return
fi
done <<<"$devices"
}
validate_device() {
read_current_rule
if [[ $current_rule == "$expected_rule" ]] ||
[[ -n $choice && $current_rule == "$allowed_rule" ]]; then
return
fi
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
}
validate_device
if [[ -z $choice ]]; then
flock -u 9
gum style --foreground 212 --bold "Blocked USB accessory"
echo
echo "The following details come from the device and can be forged:"
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo
choice=$(gum choose \
"Allow once" \
"Always allow this device" \
"Keep blocked") || exit 130
flock 9
if [[ ! -f $request || -L $request || $(cat "$request") != "$snapshot" ]]; then
echo "This USB authorization request changed. Open the latest notification to review it." >&2
exit 1
fi
# A fresh choice can authorize only the blocked identity shown in the dialog.
read_current_rule
if [[ $current_rule != "$expected_rule" ]]; then
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
fi
fi
case "$choice" in
"Allow once" | "Always allow this device")
# Save intent before applying policy: a later query can fail after the device
# was authorized. Retry must verify that approval, including persistent trust.
temporary=$(mktemp "${request%/*}/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
mv -f "$temporary" "$request"
if [[ $choice == "Always allow this device" ]]; then
pkexec /usr/bin/omarchy-usb-authorization-approve "$id" "$current_rule" || approval_failed
elif [[ $current_rule == "$expected_rule" ]]; then
# Match the complete identity, since numeric IDs can be reassigned.
usbguard allow-device "$expected_rule" || approval_failed
fi
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
# return success without authorizing anything. Verify the exact device.
allowed=$(usbguard list-devices --allowed) || approval_failed
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
if [[ $choice == "Always allow this device" ]]; then
# Let USBGuard match policy rules: saved rules can omit the device's port.
# --show-devices indents the devices matching each policy rule.
policy=$(usbguard list-rules --label omarchy-usb-authorization-v1 --show-devices) || approval_failed
policy_allows=0
matched=0
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
policy_allows=0
if [[ $line =~ ^[0-9]+:[[:space:]]allow[[:space:]] ]]; then
policy_allows=1
fi
elif (( policy_allows )) && [[ $line == $'\t'"$id: $allowed_rule" ]]; then
matched=1
break
fi
done <<<"$policy"
(( matched )) || approval_failed
echo "USB accessory added to the trusted policy."
else
echo "USB accessory allowed until it is disconnected."
fi
;;
"Keep blocked")
echo "USB accessory remains blocked."
;;
*) exit 1 ;;
esac
rm -f "$request"
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Watch USBGuard for devices awaiting user approval
set -euo pipefail
# Starting a new graphical-session watcher must offer fresh prompts even if
# their old notification actions were dismissed and aged out of history.
OMARCHY_USB_AUTHORIZATION_WATCH_ID=$(cat /proc/sys/kernel/random/uuid)
export OMARCHY_USB_AUTHORIZATION_WATCH_ID
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
# The handler scans blocked devices on IPC.Connected, including reconnects
# after a daemon restart, once the event subscription is established.
exec usbguard watch --wait --exec "$event_handler"
+8
View File
@@ -184,8 +184,12 @@
"setup.plugin.remove": {"icon":"󰭌","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"},
"setup.security": {"icon":"","label":"Security"},
"setup.config": {"icon":"","label":"Config"},
"setup.security.thunderbolt": {"icon":"󱐋","label":"Thunderbolt","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization"},
"setup.security.thunderbolt-boot": {"icon":"󱐋","label":"Thunderbolt Boot","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-boot"},
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
"setup.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","checked":"systemctl is-enabled --quiet usbguard.service && systemctl --user is-enabled --quiet omarchy-usb-authorization.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-usb-authorization"},
"setup.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","checked":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-security-usb-authorization --boot'"},
"setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
"setup.security.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
"setup.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
@@ -304,8 +308,12 @@
"remove.windows": {"icon":"󰍲","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"},
"remove.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"},
"remove.security": {"icon":"","label":"Security","title":"Remove"},
"remove.security.thunderbolt": {"icon":"󱐋","label":"Thunderbolt","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled || -f /etc/omarchy/thunderbolt-authorization.pending ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-authorization"},
"remove.security.thunderbolt-boot": {"icon":"󱐋","label":"Thunderbolt Boot","when":"omarchy-thunderbolt-authorization-boot-enabled","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-boot"},
"remove.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
"remove.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","when":"systemctl is-enabled --quiet usbguard.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-usb-authorization"},
"remove.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","when":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-security-usb-authorization --boot-only'"},
"remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
"remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.thunderbolt.approve">
<description>Approve a connected Thunderbolt accessory</description>
<message>Authentication is required to approve this Thunderbolt accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-thunderbolt-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.usb.approve">
<description>Trust a connected USB accessory on any port</description>
<message>Authentication is required to trust this USB accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-usb-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
@@ -0,0 +1,16 @@
[Unit]
Description=Offer approval for blocked Thunderbolt accessories
After=graphical-session.target
PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.enabled
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.pending
[Service]
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-watch
Restart=always
RestartSec=2
UMask=0077
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,16 @@
[Unit]
Description=Ask before allowing new USB devices
After=graphical-session.target dbus.socket
Requires=dbus.socket
PartOf=graphical-session.target
ConditionEnvironment=OMARCHY_PATH
ConditionEnvironment=WAYLAND_DISPLAY
[Service]
Type=simple
ExecStart=/usr/bin/bash -lc 'exec "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch"'
Restart=always
RestartSec=2
[Install]
WantedBy=graphical-session.target
+43
View File
@@ -0,0 +1,43 @@
# Thunderbolt authorization
Omarchy uses Bolt for Thunderbolt/USB4 PCIe authorization. USB functions remain separate and need USB authorization. The end-user controls and firmware limitations are documented in [the Security manual](../manual/48-security.md#thunderbolt-device-authorization).
## Policy and services
Bolt's default IOMMU policy can enroll new devices without asking. Omarchy keeps `AuthMode=disabled` in `/var/lib/boltd/boltd.conf` and authorizes individual devices through Bolt's D-Bus API. This does not disable Bolt or prevent explicit authorization. Setting only `DefaultPolicy=manual` would still leave IOMMU auto-enrollment enabled.
The root `omarchy-thunderbolt-authorization.service` holds explicit consent in `/var/lib/omarchy/thunderbolt-authorization/policy.json`. Bolt can import devices authorized by firmware, so its database is not treated as consent. Initial setup captures currently connected accessories; subsequent enables preserve that inventory, including an initially empty inventory. Always-allow approvals authorize, read back the real status, enroll with manual policy, verify storage, and save consent. Trusted devices are restored even without an unlocked graphical session. No operation globally enables Bolt to authorize one device.
Omarchy records which Bolt entries it enrolled, including their store timestamps, separately from consent. Removing protection converts only matching, successfully approved entries from manual to automatic policy before restoring Bolt's global mode; this includes disconnected accessories. Existing manual records and externally recreated records retain their policies. Secure keys are kept. The handoff verifies the stored policies after restarting Bolt and checkpoints policies, firmware allowlists, configuration and services for rollback. Automatic policies resume Bolt's normal firmware allowlist behavior, so every controller must be online for a recoverable handoff. A completed handoff clears enrollment ownership while retaining the trust list for re-enabling.
The Bolt startup drop-in sets the persisted mode before hardware enumeration whenever `/etc/omarchy/thunderbolt-authorization.enabled` exists. The Omarchy service checks the live mode again. Firmware security `none` remains a bypass: software cannot prevent driver loading after firmware has already opened a PCIe tunnel. This produces a warning instead of disconnecting active storage.
Secure-capable devices require both a stored Bolt record and a saved key for automatic trust. An explicit permanent approval pins the current sysfs directory and key descriptor, seeds an empty kernel key before authorization, and verifies Bolt imported it before saving consent. Automatic reconciliation cannot initialize a missing key and retains its read-only sysfs sandbox. First activation is deferred when a captured secure-capable device lacks a saved Bolt key: the `.pending` marker starts a setup notification, while existing Bolt configuration and services remain untouched. Installation can finish; only explicit setup after safe disconnection clears the pending state. Initial identity capture uses Bolt's numeric device/vendor fallback when descriptive sysfs attributes are absent.
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
The terminal dialog saves an approval intent; the background watcher submits it through `pkexec` to the fixed installed `/usr/bin/omarchy-thunderbolt-authorization-approve` helper. Polkit grants active local wheel users this individual approval action. The helper validates its arguments and the live connection, then independently verifies authorization and any saved trust before returning success. Root entrypoints use protected Bash startup and fixed installed libraries; they discard caller-supplied D-Bus addresses. A shared lock serializes setup, reconciliation, and approvals. General setup uses a separate administrator command invoked with `sudo` from the terminal. Blanket Bolt management requires administrator authentication while protection is enabled. These controls protect against unsolicited accessories, not malicious software already running as an administrator. Identifiers can be forged; Bolt's secure connection keys provide additional verification on supported hardware.
## Firmware boot access
The optional boot control applies only to online controllers with a firmware BootACL in `user` or `secure` mode. It captures connected accessories, changes stored policies to manual, clears each allowlist, and independently reads back the results. Reconciliation keeps firmware imports and returning controllers from silently restoring automatic boot permissions. Unsupported or offline controllers produce explicit warnings/errors.
Changes retain the previous policy and firmware lists. A failed transition restores matching state, or leaves a recovery checkpoint and reports failure. Device-authorization setup has a separate checkpoint that includes service state and firmware settings when boot protection was active. Restore commands are described in the manual. Firmware modified by another OS or an older snapshot is outside the current system's control; this is not an early-boot kernel authorization switch.
Deferred owner setup leaves ordinary Bolt behavior in place until the owner finishes provisioning. Factory reset disables the copied service and removes previous-owner trust, recovery data, Bolt identities, and keys from both staged and retained roots. It does not change the live daemon.
## Validation
Run policy, notification, rollback, deferred-owner, and real offline-systemctl tests with:
```sh
bash test/shell.d/thunderbolt-authorization-test.sh
```
The optional integration suite runs the installed `boltd` against upstream's UMockdev Thunderbolt fixture and a private D-Bus. Its hardware simulator uses Python/GObject; all Omarchy policy code and test assertions execute Bash. It needs `python-gobject`, `python-dbusmock`, `umockdev`, Bolt, and a Bolt source checkout (validated with 0.9.11):
```sh
umockdev-wrapper env BOLT_TEST_SOURCE=/path/to/bolt bash test/shell.d/thunderbolt-authorization-test.sh
```
It covers user/secure modes, IOMMU default denial, verified permanent and one-time approval, daemon restart, stale requests, firmware ACL restoration, and a separate Bash daemon publishing status after reconnecting to Bolt. Removal cases exercise the real setup and approval functions, automatic reconnection with retained secure keys, preservation of existing manual policies, disconnected accessories, and rollback of policies, firmware ACLs and the real private approval daemon after a setup failure. A service-control bridge manages only the fixture's private processes. These tests simulate kernel devices and firmware. They do not demonstrate physical DMA isolation or real firmware enforcement during boot. Systemd packaging, the installed approval helper, and the actual desktop Polkit rules require installed-guest validation; source-only dev linking does not install system units or rules.
@@ -0,0 +1,17 @@
polkit.addRule(function(action, subject) {
if (action.id === "org.omarchy.thunderbolt.approve" &&
subject.active && subject.local && subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
// Bolt's vendor rule grants wheel passwordless management, including enabling
// automatic approval of every IOMMU device. Keep blanket policy changes behind
// administrator authentication; the narrow Omarchy action approves one device.
if (action.id === "org.freedesktop.bolt.manage" && subject.user !== "root") {
try {
polkit.spawn(["/usr/bin/test", "-f", "/etc/omarchy/thunderbolt-authorization.enabled"]);
return polkit.Result.AUTH_ADMIN;
} catch (error) {
// Protection is disabled: retain the packaged Bolt permission rules.
}
}
});
@@ -0,0 +1,6 @@
polkit.addRule(function(action, subject) {
if (action.id === "org.omarchy.usb.approve" &&
subject.active && subject.local && subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
@@ -0,0 +1,3 @@
[Service]
# Runs before Bolt enumerates hardware, including D-Bus/udev activation.
ExecStartPre=/usr/bin/omarchy-thunderbolt-authorization-admin guard
@@ -0,0 +1,26 @@
[Unit]
Description=Restore approved Thunderbolt devices and handle new approvals
Wants=bolt.service
After=bolt.service polkit.service
ConditionPathExists=/etc/omarchy/thunderbolt-authorization.enabled
[Service]
Type=simple
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-daemon
RuntimeDirectory=omarchy-thunderbolt-authorization
RuntimeDirectoryMode=0755
Restart=on-failure
RestartSec=2
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/omarchy/thunderbolt-authorization /run/lock
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX
[Install]
WantedBy=multi-user.target
+2
View File
@@ -7,5 +7,7 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh"
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
run_logged "$OMARCHY_INSTALL/config/docker.sh"
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh"
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
run_logged "$OMARCHY_INSTALL/config/thunderbolt-authorization.sh"
@@ -0,0 +1,10 @@
echo "Configure Thunderbolt device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
# The builder's accessories must not become the new owner's trust policy.
# Keep ordinary Bolt behavior until interactive owner setup has finished.
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin prepare
systemctl enable omarchy-thunderbolt-authorization.service
fi
+26
View File
@@ -0,0 +1,26 @@
echo "Configure USB device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# A deferred install has only the builder's peripherals attached. The owner
# needs working input before there is an account or a session for approvals.
if [[ -z ${OMARCHY_INSTALL_USER:-} ]]; then
systemctl disable usbguard.service
return 0
fi
rules_file="${OMARCHY_USB_AUTHORIZATION_RULES_FILE:-/etc/usbguard/rules.conf}"
daemon_config="${OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG:-/etc/usbguard/usbguard-daemon.conf}"
source "$OMARCHY_INSTALL/helpers/usb-authorization.sh"
usb_authorization_require_secure_settings "$daemon_config"
if [[ ! -s $rules_file ]]; then
usb_authorization_install_policy "$rules_file"
fi
usb_authorization_add_user "$OMARCHY_INSTALL_USER"
# Installs are followed by a reboot, so enable the daemon without starting it
# in the live ISO's chroot.
systemctl enable usbguard.service
+319
View File
@@ -0,0 +1,319 @@
# Shared by the root controller and session commands. These paths are fixed at
# privileged entrypoints; tests may source the functions in an isolated fixture.
TB_STATE=/var/lib/omarchy/thunderbolt-authorization/policy.json
TB_CONFIG=/var/lib/boltd/boltd.conf
TB_MARKER=/etc/omarchy/thunderbolt-authorization.enabled
TB_PENDING=/etc/omarchy/thunderbolt-authorization.pending
TB_RUNTIME=/run/omarchy-thunderbolt-authorization
TB_LOCK=/run/lock/omarchy-thunderbolt-authorization.lock
TB_SERVICE=omarchy-thunderbolt-authorization.service
TB_BOLT=org.freedesktop.bolt
TB_MANAGER=org.freedesktop.bolt1.Manager
TB_DEVICE=org.freedesktop.bolt1.Device
TB_DOMAIN=org.freedesktop.bolt1.Domain
TB_PATH=/org/freedesktop/bolt
TB_SYSFS=/sys/bus/thunderbolt/devices
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
tb_json_write() {
local target=$1 mode=${2:-600} temporary
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
if jq -cS . > "$temporary" && chmod "$mode" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$target" && sync "${target%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_state() {
jq -ce 'select(.version == 1 and (.trusted | type == "object") and (.enabled | type == "boolean"))' "$TB_STATE"
}
tb_bus() {
busctl --system --auto-start=no --timeout=10 --json=short "$@"
}
tb_owner() {
tb_bus call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$TB_BOLT" | jq -er '.data[0]'
}
tb_properties() {
tb_bus call "$1" "$2" org.freedesktop.DBus.Properties GetAll s "$3" | jq -ce '.data[0] | map_values(.data)'
}
tb_set() {
local owner=$1 path=$2 interface=$3 key=$4 value=$5 signature=${6:-s} current
local -a values=()
current=$(tb_properties "$owner" "$path" "$interface") || return 1
if ! jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null; then
if [[ $signature == "as" ]]; then
mapfile -t values < <(jq -r '.[]' <<< "$value")
tb_bus set-property "$owner" "$path" "$interface" "$key" as "${#values[@]}" "${values[@]}" >/dev/null || return 1
else
tb_bus set-property "$owner" "$path" "$interface" "$key" s "$(jq -r . <<< "$value")" >/dev/null || return 1
fi
fi
# Bolt rejects no-op BootACL writes; even skipped writes are read back.
current=$(tb_properties "$owner" "$path" "$interface") || return 1
jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null ||
tb_fail "Bolt did not retain the requested $key"
}
tb_inventory() {
local owner manager paths path item inode domains='[]' devices='[]' stored='[]'
owner=$(tb_owner) || return 1
manager=$(tb_properties "$owner" "$TB_PATH" "$TB_MANAGER") || return 1
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDomains | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DOMAIN" | jq -c --arg path "$path" '. + {path:$path}') || return 1
domains=$(jq -c --argjson item "$item" '. + [$item]' <<< "$domains") || return 1
done < <(jq -r '.[]' <<< "$paths")
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDevices | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DEVICE" | jq -c --arg path "$path" '. + {path:$path}') || return 1
[[ $(jq -r .Type <<< "$item") == "peripheral" ]] || continue
if jq -e .Stored <<< "$item" >/dev/null; then
stored=$(jq -c --argjson item "$item" '. + [$item]' <<< "$stored") || return 1
fi
[[ $(jq -r .Status <<< "$item") != "disconnected" ]] || continue
inode=$(stat -Lc '%i' -- "$(jq -r .SysfsPath <<< "$item")") || return 1
item=$(jq -c --arg inode "$inode" '. + {inode:$inode}' <<< "$item") || return 1
devices=$(jq -c --argjson item "$item" '. + [$item]' <<< "$devices") || return 1
done < <(jq -r '.[]' <<< "$paths")
[[ $(tb_owner) == "$owner" ]] || { tb_fail "Bolt restarted while reading its inventory"; return 1; }
jq -cn --arg owner "$owner" --argjson manager "$manager" --argjson domains "$domains" \
--argjson devices "$devices" --argjson stored "$stored" '
{owner:$owner,manager:$manager,domains:$domains,stored:$stored,devices:[$devices[] |
. + {Secure:((.Domain as $domain | any($domains[]; .Uid == $domain and .SecurityLevel == "secure"))
and (.AuthFlags | contains("nokey") | not))}]}'
}
tb_read_inventory() {
local inventory
inventory=$(tb_inventory) || return 1
jq -e '.manager.AuthMode == "disabled"' <<< "$inventory" >/dev/null || {
tb_fail "Bolt automatic authorization is enabled; protection is not active"
return 1
}
printf '%s\n' "$inventory"
}
tb_identity() {
jq -c --arg owner "$1" --arg generation "$2" \
'{owner:$owner,generation:$generation,path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime}'
}
tb_snapshot() {
local state inventory generation recovery=false
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
[[ -f ${TB_STATE%/*}/boot-recovery.json ]] && recovery=true
jq -c --argjson state "$state" --arg generation "$generation" --argjson recovery "$recovery" \
--arg error "${1:-}" --argjson time "$(date +%s)" "$TB_TRUST_JQ"'
.owner as $owner |
[.devices[] | . as $d | {
identity: ({owner:$owner,generation:$generation} + ($d | {path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime})),
trusted: ($d | trusted($state)),
status:.Status,stored:.Stored,policy:.Policy,flags:.AuthFlags
}] as $devices |
{available:true,time:$time,owner:$owner,generation:$generation,devices:$devices,error:$error,
boot_protection:($state.boot_protection // false),
domains:[.domains[] | {Uid,SecurityLevel,IOMMU,BootACL}],
warnings: ([
(if $recovery then "A firmware boot-access change needs recovery. Boot protection is not confirmed." else empty end),
(.domains[] | if .SecurityLevel == "none" then
"This controller allows PCIe connections in firmware. Select user authorization in firmware settings; Omarchy cannot block its devices before their drivers load."
else empty end),
(.domains[] | .SecurityLevel as $level | select(["none","user","secure","dponly","usbonly","nopcie"] | index($level) | not) |
"The controller authorization mode could not be verified."),
(.domains[] | select($state.boot_protection and (.SysfsPath == "" or
(.SecurityLevel != "user" and .SecurityLevel != "secure") or (.BootACL | length == 0) or any(.BootACL[]; . != ""))) |
"The firmware boot allowlist is not confirmed empty on every controller. Check Thunderbolt Boot before relying on boot protection."),
($devices[] | select((.status | startswith("authorized")) and (.trusted | not) and (.flags | contains("boot"))) |
"An untrusted device was authorized by firmware before Linux started. Disconnect it safely to apply approval on its next connection.")
] | unique)}' <<< "$inventory"
}
tb_current() {
local expected=$1 inventory owner generation device identity
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if jq -e --argjson expected "$expected" '. == $expected' <<< "$identity" >/dev/null; then
jq -cn --arg owner "$owner" --argjson device "$device" '{owner:$owner,device:$device}'
return 0
fi
done < <(jq -c '.devices[]' <<< "$inventory")
tb_fail "That Thunderbolt device was removed or changed. Open its latest notification."
}
tb_prepare_key() (
local identity=$1 current key key_fd
current=$(tb_current "$identity") || return 1
# Bolt creates keys for stored devices itself. For a new device, disabled
# AuthMode prevents enrollment before authorization. Seed the kernel key so
# Authorize provisions it and the subsequent enrollment imports it into Bolt.
jq -e '.device.Secure and (.device.Stored | not)' <<< "$current" >/dev/null || return 0
cd -- "$(jq -r .device.SysfsPath <<< "$current")" || return 1
[[ $(stat -Lc '%i' .) == "$(jq -r .device.inode <<< "$current")" &&
$(cat unique_id) == "$(jq -r .device.Uid <<< "$current")" ]] || {
tb_fail 'The Thunderbolt connection changed before key setup'; return 1;
}
# Pin the attribute in the original directory; do not follow a replacement
# connection at the same path, or truncate an existing key on a retry.
exec {key_fd}<>key || return 1
key=$(cat key) || return 1
if [[ -z $key ]]; then
[[ $(cat authorized) == "0" ]] || { tb_fail 'Reconnect the device before saving secure approval'; return 1; }
key=$(openssl rand -hex 32) || return 1
[[ $key =~ ^[0-9a-f]{64}$ ]] || return 1
tb_current "$identity" >/dev/null || return 1
printf '%s' "$key" >&"$key_fd" || return 1
[[ $(cat key) == "$key" ]] || { tb_fail 'Could not verify the Thunderbolt key'; return 1; }
else
[[ $key =~ ^[0-9a-fA-F]{64}$ ]] || { tb_fail 'The Thunderbolt key is invalid'; return 1; }
fi
)
tb_approve() {
local identity=$1 permanent=$2 initialize=${3:-false} current owner path status state
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail "Thunderbolt protection is disabled"; return 1; }
current=$(tb_current "$identity") || return 1
owner=$(jq -r .owner <<< "$current")
path=$(jq -r .device.path <<< "$current")
status=$(jq -r .device.Status <<< "$current")
case "$status" in
connected|auth-error)
if [[ $permanent == "true" ]]; then
if [[ $initialize == "true" ]]; then
tb_prepare_key "$identity" || return 1
else
jq -e '(.device.Secure | not) or (.device.Stored and (.device.Key | IN("have","new")))' <<< "$current" >/dev/null || {
tb_fail 'A secure key requires explicit device approval'; return 1;
}
fi
fi
current=$(tb_current "$identity") || return 1
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null || return 1 ;;
authorized|authorized-newkey|authorized-secure) ;;
*) tb_fail "Device authorization is still in progress; try again"; return 1 ;;
esac
current=$(tb_current "$identity") || return 1
jq -e '.device.Status | IN("authorized","authorized-newkey","authorized-secure")' <<< "$current" >/dev/null || {
tb_fail "Could not confirm device authorization; try again"; return 1;
}
if [[ $permanent == "true" ]]; then
if ! jq -e .device.Stored <<< "$current" >/dev/null; then
# Save enrollment intent before Bolt writes its record, so a lost reply
# can be retried without confusing our record with a pre-existing one.
state=$(jq --argjson device "$(jq -c .device <<< "$current")" \
'.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state") || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$(jq -r .device.Uid <<< "$current")" manual '' >/dev/null || return 1
fi
current=$(tb_current "$identity") || return 1
jq -e .device.Stored <<< "$current" >/dev/null || { tb_fail "Could not confirm saved device information; try again"; return 1; }
jq -e '(.device.Secure | not) or (.device.Key | IN("have","new"))' <<< "$current" >/dev/null || {
tb_fail 'Secure approval has no saved key. Safely disconnect, reconnect, and approve the device again.'; return 1;
}
jq --argjson device "$(jq -c .device <<< "$current")" '
if .enrolled[$device.Uid] == ($device | {Uid,Name,Vendor}) then
.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor,StoreTime})
else . end |
.trusted[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state" |
tb_json_write "$TB_STATE" || return 1
fi
tb_snapshot
}
tb_boot_apply() {
local saved=$1 inventory owner item uid policies acls value path
inventory=$(tb_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
jq -e --argjson saved "$saved" '
(($saved.policies | keys) - [.stored[].Uid] | length == 0) and
(($saved.acls | keys) - [.domains[] | select(.SysfsPath != "") | .Uid] | length == 0)' <<< "$inventory" >/dev/null || {
tb_fail "Reconnect the original controllers and restore missing Bolt records before recovery"; return 1;
}
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.policies[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DEVICE" Policy "$value" || return 1
done < <(jq -c '.stored[]' <<< "$inventory")
# Changing a device policy can update firmware ACLs, so restore ACLs last.
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.acls[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
[[ $(jq length <<< "$value") == "$(jq '.BootACL | length' <<< "$item")" ]] || {
tb_fail "Firmware boot allowlist capacity changed"; return 1;
}
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DOMAIN" BootACL "$value" as || return 1
done < <(jq -c '.domains[]' <<< "$inventory")
}
tb_reconcile() {
local inventory state owner generation device identity error='' saved
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
if jq -e .boot_protection <<< "$state" >/dev/null; then
saved=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map(select(.SysfsPath != "" and (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length>0))) |
map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
tb_boot_apply "$saved" || return 1
fi
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if ! tb_approve "$identity" true >/dev/null; then
error='A trusted Thunderbolt device could not be restored. Check the system service log.'
fi
done < <(jq -c --argjson state "$state" "$TB_TRUST_JQ"'.devices | sort_by(.SysfsPath | length)[] |
select((.Status == "connected" or .Status == "auth-error") and (.AuthFlags | contains("nopcie") | not)) |
# Never silently initialize a missing secure binding on a later connection.
select(trusted($state))' <<< "$inventory")
tb_snapshot "$error"
}
tb_daemon() {
local snapshot error
install -d -m 755 "$TB_RUNTIME"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
chmod 644 "$TB_RUNTIME/generation"
while [[ -f $TB_MARKER ]]; do
(
flock -x 9
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$snapshot"
else
cat "$TB_RUNTIME/error" >&2
error=$(< "$TB_RUNTIME/error")
jq -cn --arg error "$error" --argjson time "$(date +%s)" '{available:false,time:$time,error:$error}' |
tb_json_write "$TB_RUNTIME/snapshot.json" 644
fi
) 9> "$TB_LOCK"
sleep 2
done
}
tb_public_snapshot() {
local snapshot generation
snapshot=$(cat "$TB_RUNTIME/snapshot.json") || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
jq -e --argjson now "$(date +%s)" --arg generation "$generation" \
'.available and .generation == $generation and .time <= $now and $now - .time < 20' <<< "$snapshot" >/dev/null || {
tb_fail 'Thunderbolt policy is unavailable or could not be verified.'; return 1;
}
printf '%s\n' "$snapshot"
}
+254
View File
@@ -0,0 +1,254 @@
# Session-side notification and terminal dialog functions.
tb_requests() {
TB_REQUESTS="$HOME/.local/state/omarchy/thunderbolt-authorization/requests"
mkdir -p -m 700 "$TB_REQUESTS"
chmod 700 "${TB_REQUESTS%/*}" "$TB_REQUESTS"
}
tb_safe_text() {
jq -r 'gsub("[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]"; "?")'
}
tb_notify() {
local token=$1 retry=${2:-false} title='Thunderbolt accessory blocked' description
description='Click to review it. Keep it blocked unless you recognize the device.'
if [[ $retry == "true" ]]; then
title='Thunderbolt approval needs retry'
description='The last approval could not be confirmed. Click to retry it.'
fi
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical --glyph '󱐋' \
"$title" "$description" --exec omarchy-launch-floating-terminal-with-presentation \
omarchy-thunderbolt-authorization-review "$token"
}
tb_attention() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Thunderbolt protection needs attention' 'The approval or firmware policy could not be confirmed. Click for details.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization status
}
tb_setup_notice() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Finish Thunderbolt protection setup' 'A connected accessory needs secure enrollment. Protection is not enabled. Click to finish safely.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization
}
tb_token() {
local digest
digest=$(jq -cS --arg watch "$2" '{watch:$watch,identity:.}' <<< "$1" | sha256sum) || return 1
printf 'request-%s\n' "${digest%% *}"
}
tb_request_read() {
[[ -f $1 && ! -L $1 ]] || return 1
jq -ce 'select(.identity | type == "object")' "$1"
}
tb_deliver() {
local path=$1 request token
request=$(tb_request_read "$path") || return 1
if ! jq -e '.notified // false' <<< "$request" >/dev/null; then
token=${path##*/}
if tb_notify "${token%.json}" "$(jq -r '.approval != null' <<< "$request")"; then
jq '.notified=true' <<< "$request" | tb_json_write "$path"
else
return 1
fi
fi
}
tb_request_apply() {
# The watcher has no terminal. Polkit permits this one fixed helper for an
# active local administrator; it cannot dispatch general setup operations.
pkexec --disable-internal-agent /usr/bin/omarchy-thunderbolt-authorization-approve "$1" "$2"
}
tb_request_process() {
local path=$1 request identity permanent result error_file error
request=$(tb_request_read "$path") || return 1
if jq -e '.pending // false' <<< "$request" >/dev/null; then
identity=$(jq -c .identity <<< "$request")
permanent=$(jq -r '.approval == "Always allow this device"' <<< "$request")
error_file=$(mktemp "$TB_REQUESTS/.error.XXXXXX") || return 1
if result=$(tb_request_apply "$permanent" "$identity" 2> "$error_file") &&
jq -e --argjson identity "$identity" --argjson permanent "$permanent" \
'any(.devices[]; .identity==$identity and (.status | IN("authorized","authorized-newkey","authorized-secure")) and
($permanent == false or .trusted))' <<< "$result" >/dev/null; then
jq --argjson result "$result" '.pending=false | .result=$result | del(.error)' <<< "$request" |
tb_json_write "$path" || return 1
else
error=$(cat "$error_file")
[[ -n $error ]] || error='Could not confirm approval. Click the retry notification.'
jq --arg error "$error" '.pending=false | .notified=false | .error=$error' <<< "$request" |
tb_json_write "$path" || return 1
fi
rm -f -- "$error_file"
fi
tb_deliver "$path"
}
tb_scan() {
local watch=$1 snapshot path request device identity token existing
snapshot=$(tb_public_snapshot) || return 1
(
flock -x 9
for path in "$TB_REQUESTS"/request-*.json; do
[[ -e $path || -L $path ]] || continue
if ! request=$(tb_request_read "$path"); then
rm -f -- "$path"
continue
fi
identity=$(jq -c .identity <<< "$request")
device=$(jq -c --argjson identity "$identity" '.devices[] | select(.identity==$identity)' <<< "$snapshot") || exit 1
if [[ -z $device ]] || { ! jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null &&
! jq -e '.approval != null' <<< "$request" >/dev/null; }; then
rm -- "$path"
continue
fi
if [[ $(jq -r .watch <<< "$request") != "$watch" ]]; then
rm -- "$path"
# Resume an interrupted attempt only for the same exact connection.
jq -e '.approval != null and .result == null' <<< "$request" >/dev/null || continue
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
request=$(jq -c --arg watch "$watch" '.watch=$watch | .notified=false' <<< "$request") || exit 1
tb_json_write "$path" <<< "$request" || exit 1
fi
tb_request_process "$path" || true
done
snapshot=$(tb_public_snapshot) || exit 1
while IFS= read -r device; do
identity=$(jq -c .identity <<< "$device")
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
if [[ ! -f $path ]]; then
jq -cn --argjson identity "$identity" --arg watch "$watch" \
'{identity:$identity,watch:$watch,notified:false}' | tb_json_write "$path" || exit 1
if ! tb_deliver "$path"; then
rm -f -- "$path"
fi
fi
done < <(jq -c '.devices[] | select((.trusted | not) and (.status | IN("connected","auth-error")) and
(.flags | contains("nopcie") | not))' <<< "$snapshot")
) 9> "$TB_REQUESTS/.lock" || return 1
printf '%s\n' "$snapshot"
}
tb_watch() {
local watch snapshot warning warned='' failures=0
tb_requests
watch=$(cat /proc/sys/kernel/random/uuid)
while true; do
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
sleep 2
continue
fi
if snapshot=$(tb_scan "$watch"); then
failures=0
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
if jq -e '(.warnings | length > 0) or .error != ""' <<< "$snapshot" >/dev/null; then
if [[ $warning != "$warned" ]] && tb_attention; then
warned=$warning
fi
else
warned=''
fi
else
failures=$((failures + 1))
if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then
warned=unavailable
fi
fi
sleep 2
done
}
tb_find_device() {
local snapshot
snapshot=$(tb_public_snapshot) || return 1
jq -ce --argjson identity "$1" '.devices[] | select(.identity==$identity)' <<< "$snapshot" ||
tb_fail 'That Thunderbolt device was removed or changed. Open its latest notification.'
}
tb_review() {
local token=$1 path request identity device choice latest index
[[ $token =~ ^request-[0-9a-f]{64}$ ]] || { tb_fail 'Invalid or expired Thunderbolt request'; return 1; }
tb_requests
path="$TB_REQUESTS/$token.json"
request=$(tb_request_read "$path") || return 1
identity=$(jq -c .identity <<< "$request")
device=$(tb_find_device "$identity") || return 1
choice=$(jq -r '.approval // empty' <<< "$request")
if [[ -z $choice ]]; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || { tb_fail 'This device is no longer blocked'; return 1; }
gum style --foreground 212 --bold 'Blocked Thunderbolt accessory'
printf '\nThese details come from the device and can be forged:\n'
for index in Name Vendor Uid; do
printf '%s: ' "$index"
jq --arg key "$index" '.[$key]' <<< "$identity" | tb_safe_text
done
printf '\nApproval gives this accessory access to its PCIe drivers.\n\n'
choice=$(gum choose 'Allow once' 'Always allow this device' 'Keep blocked') || return 0
fi
case "$choice" in
'Allow once'|'Always allow this device'|'Keep blocked') ;;
*) tb_fail 'Invalid approval choice'; return 1 ;;
esac
(
flock -x 9
latest=$(tb_request_read "$path") || exit 1
[[ $latest == "$request" ]] || { tb_fail 'This request changed. Open the latest notification.'; exit 1; }
device=$(tb_find_device "$identity") || exit 1
if [[ $choice == "Keep blocked" ]]; then
printf 'Thunderbolt accessory remains blocked.\n'
else
if ! jq -e '.approval != null' <<< "$request" >/dev/null; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || exit 1
fi
jq --arg choice "$choice" '.approval=$choice | .pending=true | del(.error,.result)' <<< "$request" |
tb_json_write "$path"
fi
) 9> "$TB_REQUESTS/.lock" || return 1
if [[ $choice != "Keep blocked" ]]; then
for ((index=0; index<60; index++)); do
latest=$(tb_request_read "$path") || return 1
if jq -e '.result != null' <<< "$latest" >/dev/null; then
rm -- "$path"
if [[ $choice == "Always allow this device" ]]; then
printf 'Thunderbolt accessory trusted.\n'
else
printf 'Thunderbolt accessory allowed for this connection.\n'
fi
return 0
elif jq -e '.error != null' <<< "$latest" >/dev/null; then
jq .error <<< "$latest" | tb_safe_text >&2
return 1
fi
sleep 1
done
tb_fail 'Approval is still pending. The watcher will retain and retry the request.'
fi
}
tb_status() {
local snapshot
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
printf 'Thunderbolt protection is deferred. Previous Bolt behavior remains active.\nKeep a keyboard/input device available independently of Thunderbolt. Safely disconnect the Thunderbolt accessories, then run Setup > Security > Thunderbolt. Reconnect each accessory and choose Always allow to save its secure key.\n'
return 0
fi
snapshot=$(tb_public_snapshot) || return 1
printf 'Thunderbolt device approval is enabled.\nTrusted accessories reconnect automatically; new accessories require approval.\n'
if jq -e '.domains | length == 0' <<< "$snapshot" >/dev/null; then
printf 'No Thunderbolt controller is currently visible. Its firmware policy will be checked when it appears.\n'
fi
jq '.warnings[], (.error | select(. != ""))' <<< "$snapshot" | while IFS= read -r warning; do
printf 'Attention: '
tb_safe_text <<< "$warning"
done
printf 'Firmware boot access is separate; see the Security manual before relying on pre-boot protection.\n'
}
+398
View File
@@ -0,0 +1,398 @@
# Root setup functions. The caller sources thunderbolt-policy.sh first.
tb_config_authmode() {
local path=$1
if [[ -f $path ]]; then
awk '
/^[[:space:]]*\[/ { section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/) }
section && /^[[:space:]]*AuthMode[[:space:]]*=/ {
sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); mode=$0
}
END { print mode == "" ? "enabled" : mode }
' "$path"
else
printf 'enabled\n'
fi
}
tb_config_write() {
local path=$1 mode=$2 temporary
[[ $mode == "enabled" || $mode == "disabled" ]] || return 1
install -d -m 700 "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.config.XXXXXX") || return 1
if awk -v mode="$mode" '
/^[[:space:]]*\[/ {
section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/)
if (section) { found=1; print; print "AuthMode = " mode; next }
}
section && /^[[:space:]]*AuthMode[[:space:]]*=/ { next }
{ print }
END { if (!found) print "\n[config]\nAuthMode = " mode }
' "$(if [[ -e $path ]]; then printf '%s' "$path"; else printf /dev/null; fi)" > "$temporary" &&
sync "$temporary" && mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_guard() {
if [[ -f $TB_MARKER ]]; then
tb_state | jq -e .enabled >/dev/null || { tb_fail 'Thunderbolt policy and enable marker disagree'; return 1; }
tb_config_write "$TB_CONFIG" disabled
fi
}
tb_capture() {
local path uid name vendor trusted='{}'
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 ]] || continue
uid=$(cat "$path/unique_id") || return 1
# Bolt uses the numeric attribute when a descriptive name is absent.
if [[ -e $path/device_name ]]; then
name=$(cat "$path/device_name") || return 1
else
name=$(cat "$path/device") || return 1
fi
if [[ -e $path/vendor_name ]]; then
vendor=$(cat "$path/vendor_name") || return 1
else
vendor=$(cat "$path/vendor") || return 1
fi
[[ -n $uid ]] || { tb_fail 'A connected Thunderbolt device has no identity'; return 1; }
trusted=$(jq -c --arg uid "$uid" --arg name "$name" --arg vendor "$vendor" \
'.[$uid] = {Uid:$uid,Name:$name,Vendor:$vendor}' <<< "$trusted") || return 1
done
printf '%s\n' "$trusted"
}
tb_capture_ready() {
local fresh=${1:-false} offline=${2:-false} path uid inode inventory=''
# Do not relax an active policy, or capture new devices when re-enabling an
# existing policy. This preflight only guards the initial trust inventory.
[[ ! -f $TB_MARKER ]] || return 0
if [[ -f $TB_STATE && ! -f $TB_PENDING && $fresh != "true" ]]; then
tb_state | jq -e '.enrollment_pending // false' >/dev/null || return 0
fi
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 && -e $path/key ]] || continue
uid=$(cat "$path/unique_id") || return 1
[[ $uid =~ ^[0-9a-fA-F-]{36}$ ]] || { tb_fail 'A connected Thunderbolt device has an invalid identity'; return 1; }
path=$(realpath -e -- "$path") || return 1
inode=$(stat -Lc '%i' -- "$path") || return 1
# A key file alone can be orphaned after a failed Bolt record write. Ask
# Bolt whether this exact connection has a stored record and key. Offline
# prepare must not accept the live installer's daemon as the target store.
if [[ $offline != "true" && -z $inventory ]]; then
inventory=$(tb_inventory) || inventory='{}'
fi
if [[ $offline == "true" ]] || ! jq -e --arg uid "$uid" --arg path "$path" --arg inode "$inode" \
'any(.devices[]?; .Uid==$uid and .SysfsPath==$path and .inode==$inode and .Stored and (.Key | IN("have","new")))' <<< "$inventory" >/dev/null; then
# An active keyless dock cannot receive a key without disconnecting it.
# Preserve input/storage and finish installation with setup deferred.
install -d -m 755 "${TB_PENDING%/*}" || return 1
install -m 644 /dev/null "$TB_PENDING" || return 1
printf 'Thunderbolt protection is deferred: safely disconnect accessories without saved secure keys, keep independent input available, then run Setup > Security > Thunderbolt.\n' >&2
return 2
fi
done
}
tb_prepare() {
local fresh=${1:-false} state='{}' trusted mode
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
fi
if [[ ! -f $TB_STATE || -f $TB_PENDING || $fresh == "true" ]] || jq -e '.enrollment_pending // false' <<< "$state" >/dev/null; then
trusted=$(tb_capture) || return 1
mode=$(jq -r '.original_authmode // empty' <<< "$state") || return 1
[[ -n $mode ]] || mode=$(tb_config_authmode "$TB_CONFIG") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c --arg mode "$mode" --argjson trusted "$trusted" \
'del(.enrollment_pending) | . + {version:1,enabled:true,trusted:$trusted,original_authmode:$mode}' <<< "$state") || return 1
else
mode=$(jq -r .original_authmode <<< "$state") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c '.enabled=true' <<< "$state") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_config_write "$TB_CONFIG" disabled || return 1
install -d -m 755 "${TB_MARKER%/*}" || return 1
install -m 644 /dev/null "$TB_MARKER"
}
tb_boot_saved() {
jq -c '{policies:(.stored | map({key:.Uid,value:.Policy}) | from_entries),
acls:(.domains | map({key:.Uid,value:.BootACL}) | from_entries)}'
}
tb_boot_change() {
local enabled=$1 state inventory saved after desired backup="${TB_STATE%/*}/boot-recovery.json"
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail 'Enable Thunderbolt device authorization first'; return 1; }
if [[ $enabled == "false" ]] && ! jq -e .boot_protection <<< "$state" >/dev/null; then
return 0
fi
[[ ! -e $backup ]] || { tb_fail 'Recover the previous firmware boot-access change before retrying'; return 1; }
inventory=$(tb_read_inventory) || return 1
jq -e '.domains | length > 0 and all(.[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Connect every Thunderbolt controller before changing firmware boot access'; return 1;
}
saved=$(tb_boot_saved <<< "$inventory" | jq -c --argjson state "$state" '. + {state:$state}') || return 1
if [[ $enabled == "true" ]]; then
jq -e 'all(.domains[]; (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length > 0))' <<< "$inventory" >/dev/null || {
tb_fail 'This controller has no supported firmware boot allowlist. Disable Thunderbolt pre-boot/PCIe boot support in firmware settings; Omarchy cannot verify that setting.'; return 1;
}
desired=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
after=$(jq -c --argjson saved "$saved" --argjson devices "$(jq -c .devices <<< "$inventory")" '
if .boot_protection then . else .boot_original=($saved | {policies,acls}) end |
.boot_protection=true | .trusted += ($devices | map({key:.Uid,value:{Uid,Name,Vendor}}) | from_entries)' <<< "$state") || return 1
else
desired=$(jq -ce .boot_original <<< "$state") || return 1
after=$(jq -c 'del(.boot_original) | .boot_protection=false' <<< "$state") || return 1
fi
tb_json_write "$backup" <<< "$saved" || return 1
if tb_boot_apply "$desired" && tb_json_write "$TB_STATE" <<< "$after"; then
rm -- "$backup"
else
if tb_boot_apply "$saved" && tb_json_write "$TB_STATE" <<< "$state"; then
rm -- "$backup" || return 1
tb_fail 'Firmware change failed; previous state restored'
else
tb_fail "Firmware change failed; recovery is incomplete. Recovery data retained at $backup"
fi
return 1
fi
}
tb_boot_recover() {
local backup="${TB_STATE%/*}/boot-recovery.json" saved
saved=$(jq -ce . "$backup") || return 1
tb_boot_apply "$saved" || return 1
jq .state <<< "$saved" | tb_json_write "$TB_STATE" || return 1
rm -- "$backup"
}
tb_unit_state() {
local properties
properties=$(systemctl show -p ActiveState -p UnitFileState "$1") || return 1
jq -Rn '[inputs | split("=") | {key:.[0],value:.[1]}] | from_entries |
{active:(.ActiveState == "active"),enabled:(.UnitFileState == "enabled")}' <<< "$properties"
}
tb_handoff_policies() {
local state=$1
# StoreTime distinguishes a record recreated outside Omarchy. A completed
# enrollment and matching consent are both required, including offline
# accessories. Never promote somebody else's manual policy.
jq -c --argjson state "$state" '{policies:([.stored[] |
select(.Policy == "manual" and .StoreTime > 0 and
$state.enrolled[.Uid] == {Uid,Name,Vendor,StoreTime} and
$state.trusted[.Uid] == {Uid,Name,Vendor}) |
{key:.Uid,value:"auto"}] | from_entries),acls:{}}'
}
tb_config_checkpoint() {
local backup="${TB_STATE%/*}/setup-recovery.json" path original files='{}' units='{}' unit value saved inventory data mode state
[[ ! -e $backup && ! -e ${TB_STATE%/*}/boot-recovery.json ]] || {
tb_fail 'A previous setup change needs recovery before continuing'; return 1;
}
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=null
if [[ -e $path ]]; then
data=$(base64 -w0 -- "$path") || return 1
mode=$(stat -c '%a' -- "$path") || return 1
original=$(jq -cn --arg data "$data" --arg mode "$mode" '{data:$data,mode:$mode}') || return 1
fi
files=$(jq -c --arg path "$path" --argjson original "$original" '.[$path]=$original' <<< "$files") || return 1
done
for unit in bolt.service "$TB_SERVICE"; do
value=$(tb_unit_state "$unit") || return 1
units=$(jq -c --arg unit "$unit" --argjson value "$value" '.[$unit]=$value' <<< "$units") || return 1
done
saved=$(jq -cn --argjson files "$files" --argjson units "$units" '{files:$files,units:$units}') || return 1
state='{}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
# A handoff to auto also changes Bolt's firmware allowlists. Checkpoint the
# policies and ACLs even when the optional boot protection was never used.
if jq -e '.enabled and (.boot_protection or ((.enrolled // {}) | length > 0))' <<< "$state" >/dev/null; then
inventory=$(tb_read_inventory) || return 1
jq -e 'all(.domains[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Reconnect every Thunderbolt controller before changing device policies'; return 1;
}
saved=$(jq -c --argjson firmware "$(tb_boot_saved <<< "$inventory")" '.firmware=$firmware' <<< "$saved") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$backup" <<< "$saved"
}
tb_config_restore() {
local backup="${TB_STATE%/*}/setup-recovery.json" saved path original temporary unit
saved=$(jq -ce . "$backup") || return 1
systemctl stop "$TB_SERVICE" bolt.service || return 1
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=$(jq -c --arg path "$path" '.files[$path]' <<< "$saved") || return 1
if [[ $original == "null" ]]; then
rm -f -- "$path" || return 1
else
mkdir -p -- "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.restore.XXXXXX") || return 1
if jq -r .data <<< "$original" | base64 -d > "$temporary" &&
chmod "$(jq -r .mode <<< "$original")" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
:
else
rm -f -- "$temporary"
return 1
fi
fi
done
if jq -e .firmware <<< "$saved" >/dev/null; then
systemctl start bolt.service || return 1
tb_boot_apply "$(jq -c .firmware <<< "$saved")" || return 1
fi
rm -f -- "${TB_STATE%/*}/boot-recovery.json" || return 1
if jq -e --arg unit "$TB_SERVICE" '.units[$unit].enabled' <<< "$saved" >/dev/null; then
systemctl enable "$TB_SERVICE" || return 1
else
systemctl disable "$TB_SERVICE" || return 1
fi
for unit in bolt.service "$TB_SERVICE"; do
if jq -e --arg unit "$unit" '.units[$unit].active' <<< "$saved" >/dev/null; then
systemctl start "$unit" || return 1
else
systemctl stop "$unit" || return 1
fi
done
rm -- "$backup"
}
tb_transaction() {
tb_config_checkpoint || return 1
if "$@"; then
rm -- "${TB_STATE%/*}/setup-recovery.json"
else
if tb_config_restore; then
tb_fail 'Thunderbolt setup failed; previous configuration restored'
else
tb_fail 'Thunderbolt setup failed; restoration is incomplete. Run the setup recovery command.'
fi
return 1
fi
}
tb_enable() {
systemctl stop "$TB_SERVICE" bolt.service || return 1
tb_prepare "${1:-false}" || return 1
systemctl daemon-reload || return 1
systemctl start bolt.service || return 1
tb_read_inventory >/dev/null || return 1
systemctl enable --now "$TB_SERVICE"
}
tb_disable() {
local state mode policies
systemctl stop "$TB_SERVICE" || return 1
tb_boot_change false || return 1
state=$(tb_state) || return 1
mode=$(jq -er .original_authmode <<< "$state") || return 1
policies=$(tb_read_inventory | tb_handoff_policies "$state") || return 1
tb_boot_apply "$policies" || return 1
systemctl stop bolt.service || return 1
tb_config_write "$TB_CONFIG" "$mode" || return 1
rm -f -- "$TB_MARKER" || return 1
jq '.enabled=false | del(.enrolled)' <<< "$state" | tb_json_write "$TB_STATE" || return 1
systemctl disable "$TB_SERVICE" || return 1
systemctl start bolt.service || return 1
tb_inventory | jq -e --arg mode "$mode" --argjson policies "$policies" '
.manager.AuthMode==$mode and
(([.stored[] | {key:.Uid,value:.Policy}] | from_entries) as $actual |
all($policies.policies | to_entries[]; $actual[.key]==.value))' >/dev/null
}
tb_cancel_pending() {
local state
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
else
state=$(jq -cn --arg mode "$(tb_config_authmode "$TB_CONFIG")" \
'{version:1,enabled:false,trusted:{},enrollment_pending:true,original_authmode:$mode}') || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
jq '.enabled=false | .enrollment_pending=true' <<< "$state" | tb_json_write "$TB_STATE" || return 1
rm -- "$TB_PENDING"
}
tb_reset_root() {
local root directory
root=$(realpath -e -- "$1") || return 1
[[ $root != "/" && -d $root ]] || { tb_fail 'Refusing to reset the running system'; return 1; }
[[ -e $root$TB_MARKER || -e $root$TB_PENDING || -d $root${TB_STATE%/*} ]] || return 0
# Resolve fixed paths inside the cloned root, never through an outside link.
for directory in etc etc/omarchy var var/lib var/lib/omarchy var/lib/boltd; do
[[ ! -L $root/$directory ]] || { tb_fail 'Refusing a linked factory policy directory'; return 1; }
done
systemctl --root="$root" disable "$TB_SERVICE" || return 1
rm -f -- "$root$TB_MARKER" || return 1
rm -f -- "$root$TB_PENDING" || return 1
rm -rf -- "$root${TB_STATE%/*}" || return 1
for directory in devices keys domains; do
rm -rf -- "$root/var/lib/boltd/$directory" || return 1
done
tb_config_write "$root/var/lib/boltd/boltd.conf" enabled
}
tb_admin() {
local action=${1:-} status=0 state
if [[ $action != "guard" ]]; then
exec 9> "$TB_LOCK"
flock -x 9 || return 1
fi
case "$action" in
prepare|enable|owner|migrate)
# A deferred setup needs explicit consent after the accessories have
# been disconnected; a later migration must not silently finish it.
if [[ -f $TB_PENDING && $action != "enable" ]]; then return 0; fi
if [[ $action == "migrate" && -f $TB_STATE ]] && tb_state | jq -e '.enabled == false' >/dev/null; then return 0; fi
if tb_capture_ready "$(if [[ $action == "owner" ]]; then echo true; else echo false; fi)" \
"$(if [[ $action == "prepare" ]]; then echo true; else echo false; fi)"; then
:
else
status=$?
if (( status == 2 )); then return 0; else return "$status"; fi
fi ;;
esac
case "$action" in
guard) tb_guard ;;
prepare) tb_prepare && rm -f -- "$TB_PENDING" ;;
enable) tb_transaction tb_enable && rm -f -- "$TB_PENDING" ;;
owner) tb_transaction tb_enable true && rm -f -- "$TB_PENDING" ;;
migrate)
state='{"enabled":true}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
if jq -e .enabled <<< "$state" >/dev/null; then
tb_transaction tb_enable
fi ;;
disable)
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
tb_cancel_pending
else
tb_transaction tb_disable
fi ;;
recover) tb_config_restore ;;
boot-enable|boot-disable|boot-recover)
systemctl stop "$TB_SERVICE" || return 1
case "$action" in
boot-enable) tb_boot_change true || status=$? ;;
boot-disable) tb_boot_change false || status=$? ;;
boot-recover) tb_boot_recover || status=$? ;;
esac
systemctl start "$TB_SERVICE" || return 1
return "$status" ;;
reset-root) [[ $# == 2 ]] && tb_reset_root "$2" ;;
*) tb_fail 'Unknown Thunderbolt setup action' ;;
esac
}
@@ -0,0 +1,95 @@
# Device identity is portable; the full live rule is still used when granting
# access to the connection shown in the approval dialog.
usb_authorization_portable_rule() {
local rule="$1" token="" char quoted=0 escaped=0 index
local -a tokens=() portable=()
# Split canonical USBGuard output without evaluating device-controlled text.
# An attribute name inside a quoted name/serial is never a rule attribute.
for (( index=0; index<${#rule}; index++ )); do
char=${rule:index:1}
if (( escaped )); then
token+="$char"
escaped=0
elif (( quoted )) && [[ $char == '\' ]]; then
token+="$char"
escaped=1
elif [[ $char == '"' ]]; then
token+="$char"
quoted=$((1 - quoted))
elif (( ! quoted )) && [[ $char == " " ]]; then
if [[ -n $token ]]; then tokens+=("$token"); token=""; fi
else
token+="$char"
fi
done
(( ! quoted && ! escaped )) || return 1
if [[ -n $token ]]; then tokens+=("$token"); fi
[[ ${tokens[0]:-} == "allow" || ${tokens[0]:-} == "block" ]] || return 1
portable=(allow)
for (( index=1; index<${#tokens[@]}; index++ )); do
case "${tokens[index]}" in
via-port | parent-hash)
(( ++index < ${#tokens[@]} )) || return 1
[[ ${tokens[index]} == \"*\" ]] || return 1
;;
label) return 1 ;;
*) portable+=("${tokens[index]}") ;;
esac
done
local IFS=" "
printf '%s label "omarchy-usb-authorization-v1"\n' "${portable[*]}"
}
usb_authorization_permanent() {
local id="$1" expected="$2" devices current="" allowed portable policy line target new_id matches rule saved=0
[[ $id =~ ^[0-9]+$ && ( $expected == block\ * || $expected == allow\ * ) ]] || return 1
(( ${#expected} <= 16384 )) || return 1
devices=$(usbguard list-devices) || return 1
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then current=${line#"$id: "}; fi
done <<<"$devices"
[[ $current == "$expected" ]] || return 1
allowed="allow ${expected#* }"
portable=$(usb_authorization_portable_rule "$current") || return 1
usbguard-rule-parser "$portable" >/dev/null || return 1
# Respect earlier administrator deny rules. Do not report permanent success
# when a saved allow would lose to an explicit block/reject on reconnect.
policy=$(usbguard list-rules) || return 1
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]](allow|block|reject)([[:space:]]|$) ]]; then
target=${BASH_REMATCH[1]}
rule=${line#*: }
# --show-devices filters by current target, hiding deny rules for a
# temporarily allowed device. A match query compares identity instead.
matches=$(usbguard list-devices "match${rule#"$target"}") || return 1
if grep -Fqx -- "$id: $current" <<<"$matches"; then
if [[ $target != "allow" ]]; then
echo "An administrator rule blocks this device. Review that rule before saving approval." >&2
return 1
fi
break
fi
fi
done <<<"$policy"
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]] && ${line#*: } == "$portable" ]]; then
saved=1
fi
done <<<"$policy"
# A retry can reuse only an exact owned rule present in both the daemon and
# its persistent file. Never remove/reorder rules around manual restrictions.
if (( ! saved )) || ! grep -Fqx -- "$portable" /etc/usbguard/rules.conf; then
new_id=$(usbguard append-rule "$portable") || return 1
[[ $new_id =~ ^[0-9]+$ ]] || return 1
grep -Fqx -- "$portable" /etc/usbguard/rules.conf || return 1
fi
if [[ $current == block\ * ]]; then
usbguard allow-device "$expected" || return 1
fi
devices=$(usbguard list-devices --allowed) || return 1
grep -Fqx -- "$1: $allowed" <<<"$devices"
}
+146
View File
@@ -0,0 +1,146 @@
source "${BASH_SOURCE[0]%/*}/usb-authorization-policy.sh"
usb_authorization_config_value() {
local config="$1" key="$2"
local awk_command=(awk)
if [[ ! -r $config ]]; then
if [[ $config == "/etc/usbguard/usbguard-daemon.conf" ]]; then
awk_command=(sudo awk)
else
echo "Cannot read USBGuard configuration: $config" >&2
return 1
fi
fi
"${awk_command[@]}" -F= -v key="$key" '
/^[[:space:]]*#/ { next }
$1 ~ "^[[:space:]]*" key "[[:space:]]*$" {
value=$0
sub(/^[^=]*=[[:space:]]*/, "", value)
sub(/[[:space:]]*$/, "", value)
print value
exit
}
' "$config"
}
usb_authorization_rules_present() {
local rules="$1"
if [[ -r $rules ]]; then
[[ -s $rules ]]
elif [[ $rules == "/etc/usbguard/rules.conf" ]]; then
sudo test -s "$rules"
else
return 1
fi
}
usb_authorization_require_secure_setting() {
local config="$1" key="$2" expected="$3" actual
actual=$(usb_authorization_config_value "$config" "$key")
if [[ $actual != "$expected" ]]; then
echo "USBGuard's $key must be '$expected' before Omarchy enables it." >&2
echo "Current value: ${actual:-<unset>} in $config" >&2
return 1
fi
}
usb_authorization_require_secure_settings() {
local config="$1"
usb_authorization_require_secure_setting "$config" ImplicitPolicyTarget block || return 1
usb_authorization_require_secure_setting "$config" PresentDevicePolicy apply-policy || return 1
usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy || return 1
usb_authorization_require_secure_setting "$config" AuthorizedDefault none || return 1
usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false || return 1
usb_authorization_require_secure_setting "$config" RuleFile /etc/usbguard/rules.conf || return 1
}
usb_authorization_generate_policy() {
local policy="$1" generated line
if ! generated=$(usbguard generate-policy); then
echo "USBGuard could not enumerate devices; nothing was enabled." >&2
return 1
fi
: >"$policy"
while IFS= read -r line; do
if [[ $line == allow\ * ]]; then
usb_authorization_portable_rule "$line" >>"$policy" || return 1
elif [[ -n $line ]]; then
printf '%s\n' "$line" >>"$policy"
fi
done <<<"$generated"
if ! grep -q '^allow ' "$policy"; then
if grep -q '^[[:space:]]*[^#[:space:]]' "$policy"; then
echo "USBGuard did not generate a usable policy; nothing was enabled." >&2
return 1
fi
# A successful empty inventory is valid on machines without USB controllers.
# Keep the file nonempty so re-enabling preserves this default-deny policy.
echo '# No USB devices were present during enrollment.' >>"$policy"
fi
usbguard-rule-parser -f "$policy" >/dev/null
}
usb_authorization_add_user() {
local user="$1"
# This user can inspect USBGuard events and approve the device named by an
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
usbguard add-user "$user" \
--devices=list,listen,modify \
--policy=list \
--exceptions=listen
}
usb_authorization_install_policy() {
local rules="$1" policy status=0
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX") || return 1
if usb_authorization_generate_policy "$policy"; then
install -Dm600 -o root -g root "$policy" "$rules" || status=$?
else
status=1
fi
rm -f "$policy"
return "$status"
}
usb_authorization_provision_owner() {
local user="$1"
local rules="${2:-/etc/usbguard/rules.conf}"
local config="${3:-/etc/usbguard/usbguard-daemon.conf}"
usb_authorization_require_secure_settings "$config" || return 1
# Enroll the owner's hardware, replacing any policy inherited from the
# builder. Do this only after the interactive provisioning steps finish.
usb_authorization_install_policy "$rules" || return 1
usb_authorization_add_user "$user" || return 1
systemctl enable usbguard.service || return 1
systemctl restart usbguard.service
}
usb_authorization_trust_present_devices() {
local line id devices
local count=0
devices=$(usbguard list-devices) || return 1
local -a inventory
mapfile -t inventory <<<"$devices"
for line in "${inventory[@]}"; do
if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then
id="${BASH_REMATCH[1]}"
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "${line#*: }" || return 1
(( ++count ))
fi
done
if (( count == 0 )); then
echo "USBGuard did not report any connected USB devices; boot authorization was not enabled." >&2
return 1
fi
}
+1
View File
@@ -144,6 +144,7 @@ udiskie
ufw
ufw-docker
unzip
usbguard
usage
uwsm
vi
+3 -1
View File
@@ -19,6 +19,8 @@ systemctl --user enable --now \
omarchy-sleep-lock.service \
omarchy-migrate-notify.service \
omarchy-fcitx5.service \
omarchy-crash-watch.service
omarchy-crash-watch.service \
omarchy-usb-authorization.service \
omarchy-thunderbolt-authorization.service
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync
+44 -1
View File
@@ -6,7 +6,30 @@ Omarchy takes security extremely seriously. This is meant to be an operating sys
2. *Firewall is enabled by default*: All incoming traffic is blocked by default except for port 53317 for [LocalSend](https://localsend.org/). Even ssh is off until you turn it on via _Setup > Security > SSHD_, which opens port 22 (rate limited against brute force) as part of the setup. We even lock down Docker access using the [ufw-docker](https://github.com/chaifeng/ufw-docker) setup to prevent that your containers are accidentally exposed to the world.
3. *Arch always have the latest updates*: Arch, the underlying distro that Omarchy is built on, is a rolling distribution. This means that any security vulnerability that's discovered and patched in any package is quickly available for install using `omarchy-update`. You're always running the latest, most secure versions of everything that way.
4. *Omarchy maintains its own packages and mirror*: Omarchy only relies on packages from Arch's own core/extra/multilib repositories and its own Omarchy Package Repository by default. You can install software directly from AUR, but the base install doesn't — only a few optional installs, like the third-party browsers, pull from the AUR.
5. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability.
5. *New USB devices require approval*: Omarchy trusts the USB devices present during installation, then blocks newly connected accessories until you approve them. This keeps an unknown accessory from reaching a matching kernel driver merely because it claims a supported device identity.
6. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability.
## USB Device Authorization
USB device authorization is enabled by default. Omarchy uses USBGuard to trust the devices connected during installation and block USB devices it has not seen before. When a device is blocked, click the persistent notification and choose whether to allow it once, always allow it, or keep it blocked.
**Always allow this device** trusts the same device identity across USB ports and hubs, including after reboot. Changes to its identity still require approval. A hub may need its own approval before devices connected through it become available.
Older approvals can be tied to a port or hub. Run `omarchy-setup-security-usb-authorization --review-existing` to review connected devices and confirm which should be trusted on any port. Existing rules are preserved. For a disconnected device, reconnect it and choose **Always allow this device** if prompted.
On a preinstalled machine that asks you to create your account at first boot, USB protection starts at the end of that setup. It trusts the devices connected then, so your keyboard works throughout account setup even if it differs from the installer's keyboard.
You can turn the protection off from _Remove > Security > USB Device Authorization_. _Setup > Security > USB Device Authorization_ turns it back on with the existing trusted-device policy.
Only approve hardware you recognize. A malicious USB device can forge the manufacturer, model, serial number, and device class shown during review. Permanent approval is a convenience for accessories you control, not proof of the device's identity.
The policy protects devices connected after USBGuard starts during boot. It does not replace operating-system and driver security updates, and USB devices needed before the service starts, such as a keyboard used to unlock an encrypted disk, remain governed by the kernel's boot-time policy.
For machines whose disk unlock and recovery never depend on any USB devices, _Setup > Security > USB at Boot_ can extend the block to the beginning of kernel boot. Omarchy adds every currently connected device to USBGuard's trusted policy, sets `usbcore.authorized_default=0`, and updates current and Limine snapshot boot entries. The trusted policy applies only once USBGuard starts: even a trusted USB keyboard remains unavailable at an earlier encrypted-disk password prompt.
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 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
@@ -33,3 +56,23 @@ Be clear-eyed about this one: while it's on, anything running as your user can d
The public key for all ISO signatures and Omarchy repo package is `40DFB630FF42BCFFB047046CF0134EE680CAC571` ([verify at openpgp.org](https://keys.openpgp.org/search?q=pkgs%40omarchy.org)). The `omarchy/omarchy-keyring` package contains this as well and will be used to rollout any potential updates seamlessly.
You can find the signature for any ISO release by adding .sig to the URL. Like https://iso.omarchy.org/omarchy-x.x.x.iso.sig.
## Thunderbolt device authorization
Omarchy requires approval before connecting new Thunderbolt and USB4 PCIe accessories on controllers that support authorization. Devices connected during initial setup are trusted. New devices show a persistent notification with **Allow once**, **Always allow this device**, and **Keep blocked**. Trusted devices reconnect automatically, including while the screen is locked. IOMMU memory protection stays enabled; it does not count as consent to load a device's driver.
On controllers in secure mode, permanent approval also saves the accessory's authentication key. If a connected accessory has no saved key during initial setup, Omarchy defers Thunderbolt protection and keeps the previous Bolt behavior so your keyboard and storage remain usable. A **Finish Thunderbolt protection setup** notification explains how to continue: keep input available independently of Thunderbolt, safely disconnect the accessories, enable protection, then reconnect and approve each one. Losing a previously saved secure key requires a new explicit approval; Omarchy does not silently create a replacement key while restoring trusted devices.
_Setup > Security > Thunderbolt_ enables protection. _Remove > Security > Thunderbolt_ restores the previous Bolt behavior, which may automatically approve new devices when IOMMU protection is available. Re-enabling keeps your saved trust list. Omarchy never disconnects active storage when changing these settings.
When you remove protection, devices enrolled by Omarchy for permanent approval reconnect through Bolt's automatic policy, keeping their secure keys. Devices with a manual Bolt policy from before setup retain that policy. Bolt also resumes managing firmware boot allowlists for automatically approved devices. Keep all Thunderbolt controllers connected during removal so Omarchy can restore their settings if the change fails.
Firmware can authorize PCIe accessories before Linux runs. A controller configured with security mode `none` bypasses software approval; select user authorization or secure authorization in its firmware settings. Omarchy reports these limitations instead of claiming that the controller is protected. Existing firmware boot allowlists can also authorize devices early. USB accessories attached through a dock require separate USB authorization.
Approval does not repair a vulnerable driver. Device names and identifiers can be forged; secure connection keys are used where Bolt and the hardware support them.
_Setup > Security > Thunderbolt Boot_ can clear and verify supported firmware boot allowlists. It captures connected accessories for authorization after Linux starts, prevents Bolt from repopulating those allowlists from automatic device policies, and retains the previous lists for _Remove > Security > Thunderbolt Boot_. This option requires every controller to be connected and to expose a boot allowlist in `user` or `secure` mode. Other hardware needs firmware settings instead; Omarchy cannot verify those settings automatically.
Enable this only if you can unlock and recover without any Thunderbolt accessories. Even a trusted keyboard or disk may remain unavailable until the policy service starts. Older snapshots and other operating systems can restore their own firmware permissions. A failed change restores the previous settings when possible; an incomplete restoration retains recovery data. Reconnect the original controllers and run `omarchy setup security thunderbolt-boot recover` to retry that recovery.
If enabling or removing device authorization fails and reports incomplete restoration, run `omarchy setup security thunderbolt-authorization recover` before trying again. This restores the configuration and service state saved before that change.
+3
View File
@@ -0,0 +1,3 @@
echo "Enable approval for newly connected USB devices"
omarchy-setup-security-usb-authorization --yes
+2
View File
@@ -0,0 +1,2 @@
echo "Require approval for new Thunderbolt accessories"
omarchy-setup-security-thunderbolt-authorization --quiet
+7
View File
@@ -0,0 +1,7 @@
echo "Repair USB approval prompts and enable portable device approvals"
# Preserve an explicit opt-out. Existing users with an active/enabled daemon
# need a working watcher even if their old checkout link has become dangling.
if systemctl is-enabled --quiet usbguard.service || systemctl is-active --quiet usbguard.service; then
omarchy-setup-security-usb-authorization --yes
fi
@@ -0,0 +1,87 @@
#!/bin/bash
# Runs against the private real boltd created by bolt-integration.py. No policy,
# busctl, firmware reads, or writes are mocked here.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json"
TB_RUNTIME="$TB_TEST_DIR/runtime"
mkdir -p "$TB_RUNTIME"
check() { jq -e "$1" "$2" >/dev/null; }
reject() { if "$@"; then echo 'Unexpected approval success' >&2; exit 1; fi; }
case "$1" in
approve)
echo '{"version":1,"enabled":true,"trusted":{}}' | tb_json_write "$TB_STATE"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
tb_snapshot > "$TB_TEST_DIR/before.json"
capture=$(tb_capture)
tb_inventory | jq -e --arg uid "$TB_TEST_UID" --argjson capture "$capture" \
'.devices[] | select(.Uid==$uid) | $capture[$uid]=={Uid,Name,Vendor}' >/dev/null
check 'all(.devices[]; .status=="connected" and (.stored|not))' "$TB_TEST_DIR/before.json"
id=$(jq -c --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .identity' "$TB_TEST_DIR/before.json")
printf '%s\n' "$id" > "$TB_TEST_DIR/identity.json"
tb_approve "$id" true true > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized" and .policy=="manual"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
check 'all(.domains[0].BootACL[]; .=="")' "$TB_TEST_DIR/after.json"
inventory=$(tb_inventory)
if [[ $TB_TEST_SECURITY == "secure" ]]; then
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.Uid==$uid) | .Stored and .Key=="have"' <<< "$inventory" >/dev/null
[[ -s $TB_TEST_KEYS/$TB_TEST_UID ]]
sha256sum "$TB_TEST_KEYS/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi
owner=$(jq -r .owner <<< "$inventory")
device=$(jq -c --arg uid "$TB_TEST_UID" '.stored[] | select(.Uid==$uid)' <<< "$inventory")
domain=$(jq -c '.domains[0]' <<< "$inventory")
device_path=$(jq -r .path <<< "$device"); domain_path=$(jq -r .path <<< "$domain")
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"auto"'
acl=$(jq -c --arg uid "$TB_TEST_UID" '.BootACL | map("") | .[0]=$uid' <<< "$domain")
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$acl" as
tb_boot_change true
check .boot_protection "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
check 'all(.domains[0].BootACL[]; .=="") and .stored[0].Policy=="manual"' "$TB_TEST_DIR/inventory.json"
tb_boot_change false
check '.boot_protection==false' "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
jq -e --argjson acl "$acl" '.domains[0].BootACL==$acl and .stored[0].Policy=="auto"' "$TB_TEST_DIR/inventory.json" >/dev/null
jq --arg other "$TB_TEST_OTHER" 'del(.trusted[$other])' "$TB_STATE" | tb_json_write "$TB_STATE"
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"manual"'
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$(jq -c 'map("")' <<< "$acl")" as
;;
restart)
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
reject tb_approve "$(cat "$TB_TEST_DIR/identity.json")" true
tb_reconcile > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
tb_inventory | jq -e '.manager.AuthMode=="disabled"' >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
# Bolt must challenge with the originally saved key, not provision a new
# key merely because a reconnecting device advertises the same identity.
tb_inventory | jq -e --arg uid "$TB_TEST_UID" \
'.devices[] | select(.Uid==$uid) | .Key=="have" and (.AuthFlags | contains("secure"))' >/dev/null
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
;;
daemon)
TB_MARKER="$TB_TEST_DIR/enabled" TB_LOCK="$TB_TEST_DIR/root.lock"
touch "$TB_MARKER"
tb_daemon
;;
status)
snapshot=$(tb_public_snapshot)
jq -e --arg owner "$(tb_owner)" '.owner==$owner' <<< "$snapshot" >/dev/null
printf '%s\n' "$snapshot"
;;
once)
snapshot=$(tb_public_snapshot)
id=$(jq -c --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .identity' <<< "$snapshot")
exec 9> "$TB_TEST_DIR/root.lock"
flock -x 9
tb_approve "$id" false > "$TB_TEST_DIR/once.json"
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="authorized" and (.trusted|not)' "$TB_TEST_DIR/once.json" >/dev/null
;;
*) exit 64 ;;
esac
@@ -0,0 +1,185 @@
"""Only the hardware fixture is Python: upstream Bolt uses UMockdev/GObject.
All Omarchy behavior and assertions run in bolt-exercise.sh against real boltd,
real busctl, a private system bus, and disposable sysfs/firmware/store state.
"""
import importlib.machinery
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import time
import unittest
root = Path(sys.argv.pop(1))
loader = importlib.machinery.SourceFileLoader('bolt_upstream', str(Path(os.environ['BOLT_TEST_SOURCE']) / 'tests/test-integration'))
spec = importlib.util.spec_from_loader(loader.name, loader)
u = importlib.util.module_from_spec(spec)
loader.exec_module(u)
class ApprovalIntegration(u.BoltTest):
def exercise(self, security, numeric=False):
self.user_config(AuthMode='disabled', DefaultPolicy='manual')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
_, other = self.add_device(host, 2, 'Unknown', 'Example', authorized=0, key=None, boot='0')
if numeric:
# Linux omits these files when the peripheral has no descriptive names.
for attribute in ('device_name', 'vendor_name'):
Path(self.testbed.get_root_dir(), path.lstrip('/'), attribute).unlink()
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_KEYS': str(Path(self.dbpath, 'keys')), 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh')]
subprocess.run(command + ['approve'], env=env, check=True)
self.daemon_stop()
self.testbed.set_attribute(path, 'authorized', '0')
if security == 'secure':
self.testbed.set_attribute(path, 'key', '\n')
self.daemon_start()
subprocess.run(command + ['restart'], env=env, check=True)
# The real Bash daemon publishes a fresh readable snapshot and reconnects
# to Bolt after a restart. No custom Omarchy D-Bus server is involved.
child = subprocess.Popen(command + ['daemon'], env=env)
try:
for _ in range(100):
result = subprocess.run(command + ['status'], env=env, capture_output=True)
if result.returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish a valid snapshot')
subprocess.run(command + ['once'], env=env, check=True)
self.daemon_stop()
self.daemon_start()
for _ in range(100):
if subprocess.run(command + ['status'], env=env, capture_output=True).returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish the new Bolt generation')
finally:
child.terminate()
child.wait(timeout=15)
self.daemon_stop()
def test_user_approval(self):
self.exercise('user')
def test_secure_approval(self):
self.exercise('secure')
def test_numeric_identity(self):
self.exercise('user', numeric=True)
def removal(self, security, boot=False, disconnected=False):
self.user_config(AuthMode='disabled')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
other_path, other = self.add_device(host, 2, 'Existing', 'Example', authorized=0, key=None, boot='0')
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_STORE': self.dbpath, 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-removal.sh')]
service = 'omarchy-thunderbolt-authorization.service'
child = None
def operation(action, unit):
nonlocal child
if unit == 'bolt.service':
if action == 'stop':
self.daemon_stop()
elif self.daemon is None:
self.daemon_start()
elif unit == service:
if action == 'stop' and child is not None:
child.terminate()
child.wait(timeout=15)
child = None
elif action == 'start' and child is None:
child = subprocess.Popen(['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh'), 'daemon'], env=env)
else:
raise RuntimeError('Unexpected private service: ' + unit)
marker = Path(self.rundir, unit + '.active')
if action == 'start':
marker.touch()
else:
marker.unlink(missing_ok=True)
def run(action):
process = subprocess.Popen(command + [action], env=env)
control = Path(self.rundir, 'control')
deadline = time.monotonic() + 90
try:
while process.poll() is None:
if time.monotonic() > deadline:
raise RuntimeError('Private setup timed out: ' + action)
if control.exists():
operation(*control.read_text().strip().split())
control.unlink()
time.sleep(0.05)
if process.returncode:
raise subprocess.CalledProcessError(process.returncode, command + [action])
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=15)
def wait_for(action):
for _ in range(50):
result = subprocess.run(command + [action], env=env, capture_output=True, text=True)
if result.returncode == 0:
print(result.stdout, end='', flush=True)
return
time.sleep(0.1)
raise RuntimeError(action + ': ' + result.stdout + result.stderr)
def unplug():
for device_path in (path, other_path):
self.testbed.uevent(device_path, 'remove')
self.testbed.remove_device(device_path)
wait_for('disconnected')
try:
Path(self.rundir, 'bolt.service.active').touch()
run('approve')
operation('start', service)
Path(self.rundir, service + '.enabled').touch()
if boot:
run('boot-enable')
run('rollback')
if disconnected:
unplug()
run('disable')
if not disconnected:
unplug()
for number, device_uid, name in ((1, uid, 'Dock'), (2, other, 'Existing')):
attributes = ['device_name', name, 'device', '0x23', 'vendor_name', 'Example',
'vendor', '0x23', 'authorized', '0', 'unique_id', device_uid, 'boot', '0']
if number == 1 and security == 'secure':
attributes += ['key', '\n']
self.testbed.add_device('thunderbolt', '0-' + str(number), host, attributes, ['DEVTYPE', 'thunderbolt_device'])
wait_for('reconnected')
finally:
operation('stop', service)
self.daemon_stop()
def test_user_removal(self):
self.removal('user')
def test_secure_removal(self):
self.removal('secure', boot=True, disconnected=True)
names = sys.argv[1:] or ('test_user_approval', 'test_secure_approval', 'test_numeric_identity', 'test_user_removal', 'test_secure_removal')
suite = unittest.TestSuite(ApprovalIntegration(name) for name in names)
result = unittest.TextTestRunner(verbosity=2).run(suite)
sys.exit(not result.wasSuccessful())
@@ -0,0 +1,95 @@
#!/bin/bash
# Real Bolt policy/approval/setup calls. Only systemd is bridged to the private
# daemon processes owned by bolt-integration.py; never contact host systemd.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json" TB_CONFIG="$TB_TEST_STORE/boltd.conf"
TB_MARKER="$TB_TEST_DIR/enabled" TB_PENDING="$TB_TEST_DIR/pending"
TB_RUNTIME="$TB_TEST_DIR/runtime" TB_LOCK="$TB_TEST_DIR/root.lock"
mkdir -p "$TB_RUNTIME"
service_request() {
printf '%s\n' "$*" > "$TB_TEST_DIR/control.tmp"
mv "$TB_TEST_DIR/control.tmp" "$TB_TEST_DIR/control"
for (( attempt=0; attempt<300; attempt++ )); do
[[ -f $TB_TEST_DIR/control ]] || return 0
sleep 0.05
done
echo "Private service operation timed out: $*" >&2
return 1
}
systemctl() {
local action=$1 unit
shift
printf '%s\n' "$action $*" >> "$TB_TEST_DIR/services.log"
case "$action" in
show)
unit=${!#}
if [[ -f $TB_TEST_DIR/$unit.active ]]; then echo ActiveState=active; else echo ActiveState=inactive; fi
if [[ -f $TB_TEST_DIR/$unit.enabled ]]; then echo UnitFileState=enabled; else echo UnitFileState=disabled; fi ;;
start|stop)
for unit in "$@"; do service_request "$action" "$unit" || return 1; done ;;
enable)
unit=${!#}
touch "$TB_TEST_DIR/$unit.enabled"
if [[ $1 == "--now" ]]; then service_request start "$unit"; fi ;;
disable)
if [[ -f $TB_TEST_DIR/fail-disable ]]; then rm "$TB_TEST_DIR/fail-disable"; return 1; fi
rm -f -- "$TB_TEST_DIR/$1.enabled" ;;
daemon-reload) ;;
*) echo "Unexpected systemctl action: $action $*" >&2; return 1 ;;
esac
}
check_inventory() { tb_inventory | jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" "$1" >/dev/null; }
case "$1" in
approve)
jq -cn '{version:1,enabled:true,trusted:{},original_authmode:"enabled"}' > "$TB_STATE"
echo removal-test > "$TB_RUNTIME/generation"
touch "$TB_MARKER"
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_UID" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
# An existing manual record, even explicitly trusted through Omarchy,
# must keep the policy its administrator chose before enrollment.
inventory=$(tb_inventory)
owner=$(jq -r .owner <<< "$inventory")
path=$(jq -r --arg uid "$TB_TEST_OTHER" '.devices[]|select(.Uid==$uid)|.path' <<< "$inventory")
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$TB_TEST_OTHER" manual '' >/dev/null
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_OTHER" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" \
'.enrolled[$uid].StoreTime>0 and .enrolled[$other]==null and .trusted[$other]!=null' "$TB_STATE" >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
sha256sum "$TB_TEST_STORE/keys/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi ;;
boot-enable) tb_admin boot-enable ;;
rollback)
cp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
tb_inventory | tb_boot_saved > "$TB_TEST_DIR/old-firmware.json"
touch "$TB_TEST_DIR/fail-disable"
if tb_admin disable; then echo 'Failed removal unexpectedly succeeded' >&2; exit 1; fi
cmp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
cmp <(tb_inventory | tb_boot_saved | jq -cS .) <(jq -cS . "$TB_TEST_DIR/old-firmware.json")
[[ -f $TB_MARKER && -f $TB_TEST_DIR/$TB_SERVICE.active && -f $TB_TEST_DIR/$TB_SERVICE.enabled && ! -f $TB_TEST_DIR/setup-recovery.json ]]
check_inventory '.manager.AuthMode=="disabled"'
echo 'Removal failure restored policy, firmware ACLs and approval service' ;;
disable)
tb_admin disable
[[ ! -f $TB_MARKER && ! -f $TB_TEST_DIR/$TB_SERVICE.active && ! -f $TB_TEST_DIR/$TB_SERVICE.enabled ]]
jq -e '.enabled==false and .enrolled==null' "$TB_STATE" >/dev/null
check_inventory '.manager.AuthMode=="enabled" and any(.stored[]; .Uid==$uid and .Policy=="auto") and any(.stored[]; .Uid==$other and .Policy=="manual")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then sha256sum --status -c "$TB_TEST_DIR/key-checksum"; fi
echo 'Removal handed our record back to Bolt and preserved existing manual policy' ;;
disconnected)
check_inventory 'any(.stored[]; .Uid==$uid and .Status=="disconnected") and any(.stored[]; .Uid==$other and .Status=="disconnected")' ;;
reconnected)
check_inventory 'any(.devices[]; .Uid==$uid and .Policy=="auto" and (.Status|startswith("authorized"))) and any(.devices[]; .Uid==$other and .Policy=="manual" and .Status=="connected")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then
check_inventory 'any(.devices[]; .Uid==$uid and .Key=="have" and (.AuthFlags|contains("secure")))'
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
echo "Automatic reconnect succeeded in $TB_TEST_SECURITY mode; pre-existing manual device stayed blocked" ;;
*) exit 64 ;;
esac
@@ -0,0 +1,640 @@
#!/bin/bash
# These behavior cases were first run as the existing Python assertions against
# the Bash implementation. Fixtures replace only Bolt, systemd and desktop I/O.
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
source "$ROOT/install/helpers/thunderbolt-review.sh"
fixture() {
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
export HOME="$T/home"
TB_STATE="$T/policy.json" TB_CONFIG="$T/boltd.conf" TB_MARKER="$T/enabled"
TB_PENDING="$T/pending" TB_SYSFS="$T/sysfs" TB_KEYS="$T/keys"
TB_RUNTIME="$T/runtime" TB_LOCK="$T/root.lock"
mkdir -p "$TB_RUNTIME" "$HOME" "$TB_SYSFS" "$TB_KEYS"
echo generation-a > "$TB_RUNTIME/generation"
echo '{"version":1,"enabled":true,"trusted":{},"original_authmode":"enabled"}' > "$TB_STATE"
cat > "$T/inventory" <<'JSON'
{"owner":":1.5","manager":{"AuthMode":"disabled"},"domains":[{"Uid":"host","SecurityLevel":"user","IOMMU":true,"SysfsPath":"/sys/domain","path":"/domain","BootACL":["device-1",""]}],"devices":[{"Uid":"device-1","Name":"Dock","Vendor":"Vendor","Parent":"host","SysfsPath":"/sys/devices/test/0-1","ConnectTime":1,"Status":"connected","Stored":false,"Policy":"default","AuthFlags":"","path":"/devices/1","inode":"1"}]}
JSON
: > "$T/operations"; : > "$T/units"; : > "$T/notifications"
echo '{}' > "$T/capture"
tb_requests
}
edit_inventory() { jq "$@" "$T/inventory" > "$T/next"; mv "$T/next" "$T/inventory"; }
edit_state() { jq "$@" "$TB_STATE" | tb_json_write "$TB_STATE"; }
check() { jq -e "$1" "$2" >/dev/null || fail "$1" "$(cat "$2")"; }
reject() { if ("$@") > "$T/output" 2> "$T/error"; then fail "unexpected success: $*"; fi; }
identity() { tb_snapshot | jq -c '.devices[0].identity'; }
tb_inventory() {
[[ ! -f $T/inventory-failure ]] || { tb_fail 'inventory unavailable'; return 1; }
jq '. + {stored:[.devices[] | select(.Stored)]}' "$T/inventory"
}
tb_properties() { jq -c --arg path "$2" '(.devices + .domains)[] | select(.path==$path)' "$T/inventory"; }
tb_bus() {
local command=$1 path=$3 member=$5 value
printf '%s\n' "$member" >> "$T/operations"
case "$member" in
Authorize)
[[ ! -f $T/authorize-failure ]] || return 1
if [[ ! -f $T/authorize-noop ]]; then edit_inventory --arg path "$path" '(.devices[] | select(.path==$path)).Status="authorized"'; fi ;;
EnrollDevice)
if [[ ! -f $T/enroll-noop ]]; then
edit_inventory '(.devices[0] | .Stored=true | .Policy="manual" | .StoreTime=100) as $d | .devices[0]=$d'
if [[ -f $TB_SYSFS/0-1/key && $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ && ! -f $T/key-store-failure ]]; then
edit_inventory '.devices[0].Key="have"'
fi
fi
if [[ -f $T/enroll-lost-reply ]]; then rm "$T/enroll-lost-reply"; return 1; fi ;;
Policy|BootACL)
if [[ $member == "BootACL" && -f $T/boot-failure ]] || [[ $member == "Policy" && ${7:-} == "auto" && -f $T/restore-failure ]]; then
tb_fail 'firmware rejected write'; return 1
fi
[[ $member != "Policy" || ! -f $T/policy-noop ]] || return 0
if [[ $member == "BootACL" ]]; then
shift 7
value=$(jq -cn '$ARGS.positional' --args "$@")
else
value=$(jq -cn --arg value "$7" '$value')
fi
edit_inventory --arg path "$path" --arg key "$member" --argjson value "$value" \
'((.devices[],.domains[]) | select(.path==$path))[$key]=$value'
if [[ $member == "Policy" ]]; then
# Real Bolt changes firmware ACLs when a stored policy changes.
edit_inventory --arg path "$path" --arg value "$7" '
(.devices[] | select(.path==$path) | .Uid) as $uid |
.domains[].BootACL |= (if $value=="auto" and (index($uid)==null) then
(index("") // 0) as $slot | .[$slot]=$uid
elif $value!="auto" then map(if .==$uid then "" else . end) else . end)'
fi ;;
*) tb_fail "Unexpected Bolt call: $*"; return 1 ;;
esac
}
tb_capture() { [[ ! -f $T/capture-failure ]] && cat "$T/capture"; }
tb_unit_state() { jq -cn --arg unit "$1" '{active:($unit=="bolt.service"),enabled:false}'; }
systemctl() {
if [[ $1 == --root=* ]]; then command systemctl "$@"; return; fi
printf '%s\n' "$*" >> "$T/units"
if [[ -f $T/unit-failure && $* == "$(cat "$T/unit-failure")" ]]; then
[[ -f $T/repeat-failure ]] || rm "$T/unit-failure"
tb_fail 'systemd failed'; return 1
fi
if [[ $* == "start bolt.service" && -f $TB_CONFIG ]]; then
edit_inventory --arg mode "$(tb_config_authmode "$TB_CONFIG")" '.manager.AuthMode=$mode'
if [[ ! -f $TB_MARKER && -f $T/restart-policy-failure ]]; then
rm "$T/restart-policy-failure"
edit_inventory '.devices[0].Policy="manual"'
fi
fi
}
tb_public_snapshot() { cat "$T/public"; }
tb_notify() {
echo "$1" >> "$T/notifications"
if [[ -f $T/notify-failure ]]; then rm "$T/notify-failure"; return 1; fi
}
tb_attention() { echo attention >> "$T/notifications"; }
gum() { if [[ $1 == "choose" ]]; then cat "$T/choice"; fi; }
tb_request_apply() {
jq --argjson identity "$2" --argjson permanent "$1" \
'(.devices[] | select(.identity==$identity)) |= (.status="authorized" | .trusted=$permanent | .stored=$permanent)' "$T/public" > "$T/next"
mv "$T/next" "$T/public"
[[ ! -f $T/lost-reply ]] || { tb_fail 'reply lost'; return 1; }
cat "$T/public"
}
request() { local paths=("$TB_REQUESTS"/request-*.json); printf '%s\n' "${paths[0]}"; }
request_count() { local paths; shopt -s nullglob; paths=("$TB_REQUESTS"/request-*.json); echo "${#paths[@]}"; }
public_fixture() { tb_snapshot > "$T/public"; }
boot_fixture() { edit_inventory '.devices[0] |= (.Stored=true | .Policy="auto")'; }
review_tick() {
local path watch
path=$(request); watch=$(jq -r .watch "$path")
tb_scan "$watch" >/dev/null
}
test_iommu_is_not_consent() {
edit_inventory '.devices[0] |= (.Stored=true | .Policy="iommu")'
tb_reconcile > "$T/result"
[[ ! -s $T/operations ]]; check '.devices[0].trusted==false' "$T/result"
}
test_once_is_not_saved() {
tb_approve "$(identity)" false > "$T/result"
check '.warnings==[]' "$T/result"; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "Authorize" ]]
}
test_always_survives_restart() {
tb_approve "$(identity)" true >/dev/null
edit_inventory '.owner=":1.9" | .devices[0] |= (.Status="connected" | .inode="2")'
echo generation-b > "$TB_RUNTIME/generation"
tb_reconcile > "$T/result"
check '.devices[0].status=="authorized" and .devices[0].trusted' "$T/result"
check '.manager.AuthMode=="disabled"' "$T/inventory"
}
test_enrollment_lost_reply_retains_ownership() {
touch "$T/enroll-lost-reply"
reject tb_approve "$(identity)" true
check '.trusted=={} and .enrolled["device-1"].StoreTime==null' "$TB_STATE"
tb_approve "$(identity)" true >/dev/null
check '.enrolled["device-1"].StoreTime==100 and .trusted["device-1"]!=null' "$TB_STATE"
}
secure_fixture() {
mkdir -p "$TB_SYSFS/0-1"
echo 11111111-2222-3333-4444-555555555555 > "$TB_SYSFS/0-1/unique_id"
echo 0 > "$TB_SYSFS/0-1/authorized"
: > "$TB_SYSFS/0-1/key"
edit_inventory --arg path "$TB_SYSFS/0-1" --arg inode "$(stat -Lc %i "$TB_SYSFS/0-1")" \
'.domains[0].SecurityLevel="secure" | .devices[0] |= (.Secure=true | .Key="missing" |
.Uid="11111111-2222-3333-4444-555555555555" | .SysfsPath=$path | .inode=$inode)'
}
test_secure_approval_initializes_before_trust() {
secure_fixture
tb_approve "$(identity)" true true > "$T/result"
[[ $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ ]]
check '.devices[0].trusted and .devices[0].stored' "$T/result"
cp "$TB_SYSFS/0-1/key" "$T/original-key"
tb_approve "$(identity)" true true >/dev/null
cmp "$TB_SYSFS/0-1/key" "$T/original-key"
}
test_secure_automatic_restore_cannot_initialize_keys() {
secure_fixture
jq '.devices[0] | {(.Uid):{Uid,Name,Vendor}}' "$T/inventory" > "$T/trusted"
edit_state --slurpfile trusted "$T/trusted" '.trusted=$trusted[0]'
tb_reconcile > "$T/result"
check '.devices[0].trusted==false' "$T/result"
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
reject tb_approve "$(identity)" true
[[ ! -s $T/operations ]]
}
test_secure_missing_saved_key_cannot_claim_success() {
secure_fixture
touch "$T/key-store-failure"
reject tb_approve "$(identity)" true true
check '.trusted=={}' "$TB_STATE"
tb_snapshot > "$T/result"; check '.devices[0].trusted==false' "$T/result"
}
test_secure_key_failure_does_not_authorize() {
secure_fixture
openssl() { return 1; }
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
unset -f openssl
echo invalid > "$TB_SYSFS/0-1/key"
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations ]]
}
test_authorized_keyless_device_requires_explicit_reconnect() {
secure_fixture
edit_inventory '.devices[0].Status="authorized"'
echo 1 > "$TB_SYSFS/0-1/authorized"
reject tb_approve "$(identity)" true true
rg -q 'disconnect, reconnect' "$T/error"
[[ ! -s $TB_SYSFS/0-1/key ]]; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "EnrollDevice" ]]
}
test_stale_identity_rejected() {
local id field
id=$(identity)
for field in inode Name ConnectTime; do
cp "$T/inventory" "$T/original"
edit_inventory --arg field "$field" '.devices[0][$field]="replacement"'
reject tb_approve "$id" true
mv "$T/original" "$T/inventory"
done
edit_inventory '.owner=":1.9"'; reject tb_approve "$id" true
[[ ! -s $T/operations ]]
}
test_false_success_rejected() {
local id; id=$(identity)
touch "$T/authorize-noop"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/authorize-noop"; touch "$T/enroll-noop"
reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/enroll-noop"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 2 ]]
}
test_write_failure_retains_retry() {
local id; id=$(identity)
eval "$(declare -f tb_json_write | sed '1s/tb_json_write/disk_write/')"
tb_json_write() { [[ ! -f $T/disk-full ]] && disk_write "$@"; }
touch "$T/disk-full"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/disk-full"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 1 ]]
}
test_firmware_bypass_warns() {
edit_inventory '.domains[0].SecurityLevel="none" | .devices[0] |= (.Status="authorized" | .AuthFlags="boot")'
tb_snapshot > "$T/result"; check '.warnings|length==2' "$T/result"; [[ ! -s $T/operations ]]
}
test_global_enable_not_protected() {
edit_inventory '.manager.AuthMode="enabled"'
reject tb_snapshot; rg -q 'not active' "$T/error"
reject tb_reconcile
}
test_display_only_needs_no_approval() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
edit_inventory '.devices[0].AuthFlags="nopcie"'
tb_reconcile > "$T/result"; check '.error==""' "$T/result"; [[ ! -s $T/operations ]]
}
test_empty_capture_preserved() {
rm "$TB_STATE"; tb_prepare
touch "$T/capture-failure"; tb_prepare
check '.trusted=={}' "$TB_STATE"; [[ $(tb_config_authmode "$TB_CONFIG") == "disabled" ]]
}
test_capture_failure_does_not_publish() {
rm "$TB_STATE"; touch "$T/capture-failure"; reject tb_prepare
[[ ! -e $TB_STATE && ! -e $TB_MARKER ]]
}
test_capture_optional_names_match_numeric_identity() {
source "$ROOT/install/helpers/thunderbolt-setup.sh"
mkdir -p "$TB_SYSFS/0-1"
echo device-1 > "$TB_SYSFS/0-1/unique_id"
echo 0x1234 > "$TB_SYSFS/0-1/device"
echo 0xab > "$TB_SYSFS/0-1/vendor"
tb_capture > "$T/result"
check '.["device-1"]=={Uid:"device-1",Name:"0x1234",Vendor:"0xab"}' "$T/result"
echo Dock > "$TB_SYSFS/0-1/device_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="Dock" and .["device-1"].Vendor=="0xab"' "$T/result"
rm "$TB_SYSFS/0-1/device_name"; echo Vendor > "$TB_SYSFS/0-1/vendor_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="0x1234" and .["device-1"].Vendor=="Vendor"' "$T/result"
mkdir "$TB_SYSFS/0-1/device_name"
reject tb_capture
}
test_first_activation_defers_without_touching_bolt() {
secure_fixture; rm "$TB_STATE"
printf '[config]\nAuthMode=enabled\n' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_STATE && ! -f $TB_MARKER && ! -s $T/units ]]
cmp "$TB_CONFIG" "$T/original"
rm -rf "$TB_SYSFS/0-1"
tb_admin migrate
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
tb_admin enable
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_saved_secure_binding_allows_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_key_without_record_defers_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0].Key="have"'
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_offline_prepare_cannot_trust_installers_bolt() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin prepare
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_deferred_opt_out_retains_initial_enrollment() {
secure_fixture; rm "$TB_STATE"
tb_admin owner
tb_admin disable
[[ ! -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
check '.enabled==false and .enrollment_pending==true' "$TB_STATE"
tb_admin migrate
[[ ! -f $TB_PENDING && ! -s $T/units ]]
tb_admin enable
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
rm -rf "$TB_SYSFS/0-1"
echo '{"new-device":{}}' > "$T/capture"
tb_admin enable
check '.enabled and .enrollment_pending==null and .trusted=={"new-device":{}}' "$TB_STATE"
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_notice_retries_without_claiming_protection() {
touch "$TB_PENDING"
local count=0 notices=0
sleep() { count=$((count + 1)); (( count < 3 )) || exit 99; }
tb_setup_notice() { notices=$((notices + 1)); (( notices > 1 )) && echo pending >> "$T/notifications"; }
reject tb_watch
[[ $(cat "$T/notifications") == "pending" ]]
tb_status > "$T/result"; rg -q 'protection is deferred' "$T/result"
}
test_active_policy_is_not_deferred() {
secure_fixture; touch "$TB_MARKER"
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_only_factory_reset_reaches_both_roots() {
local function_name root="$T/top/@factory"
local TOP_MNT="$T/top" NEXT_NAME=@next PROVISIONING_DIR=/var/lib/omarchy/provisioning LOG_FILE="$T/reset.log" swap_done=0
mkdir -p "$root/etc/omarchy" "$root/etc/systemd/system" "$root/usr/bin" \
"$root/usr/share/omarchy/install/provisioning" "$T/top/@"
touch "$root/etc/omarchy/thunderbolt-authorization.pending" "$root/usr/bin/omarchy-provision-owner"
chmod +x "$root/usr/bin/omarchy-provision-owner"
echo 'root:x:0:0:root:/root:/bin/bash' > "$root/etc/passwd"
cp "$ROOT/install/provisioning/omarchy-provision-owner.service" \
"$ROOT/install/provisioning/omarchy-system-factory-reset-finish.service" "$root/usr/share/omarchy/install/provisioning/"
cp "$ROOT/etc/systemd/system/omarchy-thunderbolt-authorization.service" "$root/etc/systemd/system/"
for function_name in install_provisioning_units scrub_factory_accounts sanitize_factory_baseline stage_full_reset; do
eval "$(sed -n "/^$function_name() {$/,/^}/p" "$ROOT/bin/omarchy-system-factory-reset")"
done
log() { :; }
btrfs() {
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then cp -a "$3" "$4"
elif [[ $1 == "property" ]]; then :
else return 1; fi
}
usermod() { [[ $1 == "--root" && $2 == "$TOP_MNT/"* && $3 == "--password" && $4 == "!" && $5 == "root" ]]; }
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
sync() { :; }
systemctl() {
[[ ( $1 == "--root=$TOP_MNT/@next" || $1 == "--root=$TOP_MNT/@factory" ) && $2 == "disable" && $3 == "$TB_SERVICE" ]] || return 1
command systemctl "$@"
}
function /usr/bin/omarchy-thunderbolt-authorization-admin() {
[[ $1 == "reset-root" ]] || return 1
printf '%s\n' "$2" >> "$T/reset-roots"
(
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
tb_reset_root "$2"
)
}
stage_full_reset
[[ $(wc -l < "$T/reset-roots") == 2 && $swap_done == 1 ]]
[[ ! -f $TOP_MNT/@/etc/omarchy/thunderbolt-authorization.pending &&
! -f $TOP_MNT/@factory/etc/omarchy/thunderbolt-authorization.pending ]]
[[ -f $TOP_MNT/@/var/lib/omarchy/provisioning/pending ]]
}
test_config_preserves_settings() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
tb_prepare; rg -q '^DefaultPolicy=manual$' "$TB_CONFIG"
check '.original_authmode=="enabled"' "$TB_STATE"
}
test_fresh_owner_replaces_trust() {
rm "$TB_STATE"; echo '{"old":{}}' > "$T/capture"; tb_prepare
echo '{"new":{}}' > "$T/capture"; tb_prepare true
check '.trusted=={"new":{}} and .original_authmode=="enabled"' "$TB_STATE"
}
test_enable_rollback() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
cp "$TB_CONFIG" "$T/original"; echo 'start bolt.service' > "$T/unit-failure"
reject tb_transaction tb_enable
cmp "$TB_CONFIG" "$T/original"; [[ ! -e $TB_STATE && ! -e $TB_MARKER && ! -e $T/setup-recovery.json ]]
[[ $(tail -2 "$T/units") == $'start bolt.service\nstop omarchy-thunderbolt-authorization.service' ]]
}
test_failed_restore_keeps_checkpoint() {
rm "$TB_STATE"; echo '[config]' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
echo 'stop omarchy-thunderbolt-authorization.service bolt.service' > "$T/unit-failure"; touch "$T/repeat-failure"
reject tb_transaction tb_enable; rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]; reject tb_transaction tb_enable; rg -q 'needs recovery' "$T/error"
echo partial > "$TB_CONFIG"; rm "$T/unit-failure"
tb_config_restore; cmp "$TB_CONFIG" "$T/original"
}
test_disable_restores_matching_boot_state() {
boot_fixture; tb_prepare; tb_boot_change true; cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_transaction tb_disable
cmp "$TB_STATE" "$T/original"; [[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
}
test_disable_hands_off_only_our_records() {
tb_prepare
tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices += [(.devices[0] | .Uid="existing" | .path="/devices/2" | .Policy="manual")]'
tb_approve "$(tb_snapshot | jq -c '.devices[1].identity')" true >/dev/null
tb_admin disable
check '.enabled==false and .enrolled==null and (.trusted|length)==2' "$TB_STATE"
check '.manager.AuthMode=="enabled" and .devices[0].Policy=="auto" and .devices[1].Policy=="manual"' "$T/inventory"
[[ ! -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
# After a completed handoff, an administrator may choose manual themselves.
edit_inventory '.devices[0].Policy="manual"'
tb_admin enable
tb_admin disable
check 'all(.devices[]; .Policy=="manual")' "$T/inventory"
}
test_disable_hands_off_disconnected_device() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].Status="disconnected"'
tb_admin disable
check '.devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_preserves_recreated_manual_record() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].StoreTime=200'
tb_admin disable
check '.devices[0].Policy=="manual"' "$T/inventory"
}
test_disable_preserves_original_disabled_mode() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_state '.original_authmode="disabled"'
tb_admin disable
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_handoff_after_boot_protection() {
tb_prepare; tb_boot_change true
tb_approve "$(identity)" true >/dev/null
tb_admin disable
check '.enabled==false and .boot_protection==false' "$TB_STATE"
check '.devices[0].Policy=="auto" and (.domains[0].BootACL|index("device-1"))!=null' "$T/inventory"
}
test_disable_handoff_rolls_back_policies_and_acl() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].BootACL=["unrelated",""]'
cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_admin disable
cmp "$TB_STATE" "$T/original"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["unrelated",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_failure_keeps_protection() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
tb_unit_state() { echo '{"active":true,"enabled":true}'; }
touch "$T/restore-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
! rg -q '^disable ' "$T/units"
[[ $(tail -1 "$T/units") == "start omarchy-thunderbolt-authorization.service" ]]
}
test_disable_handoff_verifies_policy_writes() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/policy-noop"
reject tb_admin disable
rg -q 'did not retain the requested Policy' "$T/error"
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_verifies_policy_after_restart() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/restart-policy-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_incomplete_rollback_is_recoverable() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
touch "$T/boot-failure"
reject tb_admin disable
rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]
rm "$T/boot-failure"
tb_admin recover
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_offline_controller_is_unchanged() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].SysfsPath=""'
reject tb_admin disable
rg -q 'Reconnect every Thunderbolt controller' "$T/error"
check '.enabled' "$TB_STATE"
[[ ! -s $T/units && ! -f $T/setup-recovery.json ]]
}
test_boot_policy_reconnection_and_restore() {
boot_fixture; tb_boot_change true
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
check '.boot_protection and .trusted["device-1"]!=null' "$TB_STATE"
edit_inventory '.devices[0].Policy="auto" | .domains[0].BootACL=["device-1",""]'
tb_snapshot > "$T/result"; check '.warnings|length>0' "$T/result"
tb_reconcile > "$T/result"; check '.error==""' "$T/result"
check '.domains[0].BootACL==["",""]' "$T/inventory"
tb_boot_change false
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
}
test_boot_change_rollback() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure"
reject tb_boot_change true
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1","other"]' "$T/inventory"
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_boot_recovery_retained() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure" "$T/restore-failure"
reject tb_boot_change true; rg -q 'recovery is incomplete' "$T/error"; [[ -f $T/boot-recovery.json ]]
rm "$T/boot-failure" "$T/restore-failure"; tb_boot_recover
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_unsupported_boot_cannot_succeed() {
boot_fixture; cp "$T/inventory" "$T/original"
local expression
for expression in '.SysfsPath=""' '.SecurityLevel="none"' '.BootACL=[]'; do
cp "$T/original" "$T/inventory"; edit_inventory ".domains[0] |= ($expression)"
reject tb_boot_change true
done
[[ ! -s $T/operations ]]
}
test_install_defers_owner() {
install() { :; }
function /usr/bin/omarchy-thunderbolt-authorization-admin() { echo "helper $*"; }
export OMARCHY_PATH=$ROOT OMARCHY_INSTALL_USER=''
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
! rg -q helper "$T/output"; [[ ! -s $T/units ]]
OMARCHY_INSTALL_USER=lab
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
rg -q 'helper prepare' "$T/output"; rg -q '^enable omarchy-thunderbolt-authorization.service$' "$T/units"
}
test_offline_reset_scrubs_private_state() {
source "$ROOT/install/helpers/thunderbolt-policy.sh"
local root="$T/clone" unit="$TB_SERVICE"
mkdir -p "$root/etc/systemd/system" "$root${TB_STATE%/*}" "$root${TB_MARKER%/*}"
cp "$ROOT/etc/systemd/system/$unit" "$root/etc/systemd/system/"
command systemctl --root="$root" enable "$unit" >/dev/null 2>&1
touch "$root$TB_MARKER" "$root$TB_STATE" "$root${TB_STATE%/*}/setup-recovery.json"
local kind
for kind in devices keys domains; do mkdir -p "$root/var/lib/boltd/$kind"; touch "$root/var/lib/boltd/$kind/old"; done
tb_reset_root "$root"
[[ ! -e $root$TB_MARKER && ! -e $root${TB_STATE%/*} && ! -e $root/etc/systemd/system/multi-user.target.wants/$unit ]]
[[ $(ls "$root/var/lib/boltd") == "boltd.conf" && $(tb_config_authmode "$root/var/lib/boltd/boltd.conf") == "enabled" ]]
}
test_live_reset_refused() { reject tb_reset_root /; }
test_trusted_reconnect_no_alert() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
public_fixture; tb_scan watch1 >/dev/null; [[ ! -s $T/notifications ]]
}
test_unavailable_warns_once() {
tb_scan() { return 1; }
sleep() { count=$((count+1)); ((count<4)) || exit 99; }
local count=0
reject tb_watch
[[ $(cat "$T/notifications") == "attention" ]]
}
test_failed_delivery_retries_other_devices() {
edit_inventory '.devices += [(.devices[0] | .Uid="device-2")]'; public_fixture; touch "$T/notify-failure"
tb_scan watch1 >/dev/null; [[ $(request_count) == 1 ]]
tb_scan watch1 >/dev/null; [[ $(request_count) == 2 && $(wc -l < "$T/notifications") == 3 ]]
}
test_restarts_restore_requests() {
public_fixture; tb_scan watch1 >/dev/null; local old; old=$(request)
tb_scan watch1 >/dev/null; [[ $(wc -l < "$T/notifications") == 1 ]]
edit_inventory '.owner=":1.9"'; public_fixture
tb_scan watch1 >/dev/null; [[ $(request) != "$old" ]]
tb_scan watch2 >/dev/null; [[ $(request_count) == 1 && $(wc -l < "$T/notifications") == 3 ]]
}
test_inventory_failure_preserves_request() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
rm "$T/public"; reject tb_scan watch1; [[ -f $path ]]
}
test_replacement_during_dialog_rejected() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
gum() {
if [[ $1 == "choose" ]]; then
jq '.devices[0].identity.Uid="replacement"' "$T/public" > "$T/next"; mv "$T/next" "$T/public"
echo 'Always allow this device'
fi
}
reject tb_review "${path##*/}" # A filename is not a valid token.
reject tb_review "$(basename "$path" .json)"
check '.devices[0].status=="connected"' "$T/public"
}
test_lost_reply_keeps_approval_intent() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
echo 'Always allow this device' > "$T/choice"; touch "$T/lost-reply"
sleep() { review_tick; }
reject tb_review "$(basename "$path" .json)"
check '.approval=="Always allow this device"' "$path"
tb_scan watch1 >/dev/null; [[ -f $path && $(wc -l < "$T/notifications") == 2 ]]
tb_scan watch2 >/dev/null; [[ ! -f $path && $(wc -l < "$T/notifications") == 3 ]]; path=$(request)
rm "$T/lost-reply"; gum() { fail 'Saved approval must not ask again'; }
tb_review "$(basename "$path" .json)" >/dev/null
[[ ! -f $path ]]
}
test_disconnect_and_untrusted_text() {
edit_inventory '.devices[0].Name="$(touch /tmp/untrusted)\u001b[2J"'
public_fixture; tb_scan watch1 >/dev/null
[[ $(basename "$(request)") =~ ^request-[0-9a-f]{64}.json$ ]]
[[ $(jq '.devices[0].Name' "$T/inventory" | tb_safe_text) != *$'\e'* ]]
edit_inventory '.devices=[]'; public_fixture; tb_scan watch1 >/dev/null; [[ $(request_count) == 0 ]]
}
test_snapshot_freshness_and_disabled_state() {
# Exercise the real public reader, including freshness after daemon failure.
source "$ROOT/install/helpers/thunderbolt-policy.sh"
TB_RUNTIME="$T/runtime"
jq -cn --argjson now "$(date +%s)" '{available:true,time:$now,generation:"generation-a"}' > "$TB_RUNTIME/snapshot.json"
tb_public_snapshot >/dev/null
jq '.time-=30' "$TB_RUNTIME/snapshot.json" > "$T/next"; mv "$T/next" "$TB_RUNTIME/snapshot.json"
reject tb_public_snapshot
echo generation-b > "$TB_RUNTIME/generation"; reject tb_public_snapshot
}
test_invalid_state_not_skipped_by_migration() {
echo '{"enabled":false}' > "$TB_STATE"; reject tb_admin migrate
[[ ! -s $T/units ]]
}
for case in $(declare -F | awk '$3 ~ /^test_/ {print $3}'); do
(fixture; "$case")
pass "Thunderbolt ${case#test_}"
done
@@ -0,0 +1,31 @@
#!/bin/bash
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
fixture=$(mktemp -d)
trap 'rm -rf -- "$fixture"' EXIT
for name in admin approve daemon; do
# Map the security library and stop before service work, including when the
# suite itself runs as root. Startup checks remain the production code.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-thunderbolt-authorization-$name" > "$fixture/entry"
chmod 755 "$fixture/entry"
printf 'set -p\n' > "$fixture/decoy"
if BASH_ENV="$fixture/decoy" /usr/bin/bash "$fixture/entry" -p; then
fail "$name accepts ordinary Bash with a decoy -p"
fi
printf 'touch "%s"\n' "$fixture/injected" > "$fixture/startup"
# The entrypoint must suppress startup files and exported functions before
# checking EUID or reaching the fixture's service-work boundary.
if BASH_ENV="$fixture/startup" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture service-work boundary"
fi
[[ ! -e $fixture/injected ]] || fail "$name executed BASH_ENV"
if /usr/bin/env 'BASH_FUNC_source%%=() { echo injected > "$TB_TEST_INJECTED"; }' \
TB_TEST_INJECTED="$fixture/injected" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture boundary with an exported function"
fi
[[ ! -e $fixture/injected ]] || fail "$name imported an exported function"
pass "Thunderbolt $name rejects unsafe Bash startup"
done
@@ -0,0 +1,57 @@
#include <usbguard/Rule.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
static std::string read(const char* path) {
std::ifstream file(path);
std::string text;
std::getline(file, text);
if (text.empty()) throw std::runtime_error("empty rule");
return text;
}
static std::string replace(std::string text, const std::string& from, const std::string& to) {
const auto index = text.find(from);
if (index == std::string::npos) throw std::runtime_error("missing attribute");
text.replace(index, from.size(), to);
return text;
}
static void check(const char* name, const usbguard::Rule& policy, const usbguard::Rule& device, bool expected) {
if (policy.appliesTo(device) != expected) throw std::runtime_error(name);
std::cout << "ok - native USBGuard: " << name << '\n';
}
int main(int argc, char** argv) {
if (argc != 3) return 2;
// Test the policy produced by the real Bash implementation, not a C++ copy.
const auto policy = usbguard::Rule::fromString(read(argv[1]));
const auto text = read(argv[2]);
const auto device = usbguard::Rule::fromString(text);
check("original connection", policy, device, true);
auto moved = device;
moved.setViaPort("9-9.9");
check("another port", policy, moved, true);
moved.setParentHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("another hub", policy, moved, true);
auto reloaded = usbguard::Rule::fromString(policy.toString());
check("serialized policy reload on new hub", reloaded, moved, true);
check("reconnect on original port", reloaded, device, true);
auto different = moved;
different.setHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("different hash denied", policy, different, false);
different = moved;
different.setSerial("different");
check("different serial denied", policy, different, false);
different = moved;
different.setName("different");
check("different name denied", policy, different, false);
check("different product denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046d:c53b")), false);
check("different vendor denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046e:c53a")), false);
check("different interface denied", policy, usbguard::Rule::fromString(replace(text, "03:00:00", "08:06:50")), false);
check("different connection type denied", policy, usbguard::Rule::fromString(replace(text, "with-connect-type \"unknown\"", "with-connect-type \"hotplug\"")), false);
// Existing unmarked/manual rules remain location-bound.
check("legacy rule still restricts ports and hubs", device, moved, false);
}
+7
View File
@@ -343,6 +343,13 @@ assert(
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
'menu places Passwordless Sudo under Setup > Security'
)
assert(
defaultById['setup.security.usb-authorization'].action.includes('omarchy-setup-security-usb-authorization')
&& defaultById['remove.security.usb-authorization'].action.includes('omarchy-remove-security-usb-authorization')
&& defaultById['setup.security.usb-authorization-boot'].action.includes('omarchy-setup-security-usb-authorization --boot')
&& defaultById['remove.security.usb-authorization-boot'].action.includes('omarchy-remove-security-usb-authorization --boot-only'),
'menu can enable and remove USB device authorization under Security'
)
assert(
!defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'],
'menu removes the relocated toggles from Trigger > Toggle'
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/policy-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/startup-test.sh"
if [[ -n ${BOLT_TEST_SOURCE:-} ]]; then
/usr/bin/python3 "$ROOT/test/shell.d/fixtures/thunderbolt/bolt-integration.py" "$ROOT"
else
skip "real boltd integration needs BOLT_TEST_SOURCE and an UMockdev environment"
fi
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$ROOT/install/helpers/usb-authorization-policy.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
identity='id 046d:c53a serial "" name "USB Receiver" hash "KCUFt1MumW4Pfs/8YXWzGQB6Hsnm8qkDqFVjfY0NIBY="'
interfaces='with-interface { 03:01:01 03:01:02 03:00:00 } with-connect-type "unknown"'
location='parent-hash "m7yTNWlczwBbYn+uRP6TRsY5AceKmAZ0Et6mgy58+/o=" via-port "3-1.1.2.1"'
device="allow $identity $location $interfaces"
portable=$(usb_authorization_portable_rule "$device")
[[ $portable == "allow $identity $interfaces"' label "omarchy-usb-authorization-v1"' ]] || fail "only device location is removed"
[[ $(usb_authorization_portable_rule "${device/#allow/block}") == "$portable" ]] || fail "blocked approvals use the same portable identity"
pass "portable USB rules retain every device identity attribute"
# A canonical escaped name cannot cause its contents to become attributes.
name='name "Receiver \" via-port \"fake\" parent-hash \"fake\" $(touch /tmp/usb-injection)"'
hostile="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $location $interfaces"
expected="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $interfaces"' label "omarchy-usb-authorization-v1"'
[[ $(usb_authorization_portable_rule "$hostile") == "$expected" ]] || fail "quoted attribute text must remain data"
if usb_authorization_portable_rule 'allow id 046d:c53a name "unterminated' >/dev/null; then fail "reject unmatched quote"; fi
if usb_authorization_portable_rule 'allow id 046d:c53a via-port' >/dev/null; then fail "reject missing location value"; fi
pass "USB portable parsing preserves escaped and hostile descriptor text"
# Exercise the actual root entrypoint startup boundary without root work.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-usb-authorization-approve" >"$scratch/entry"
chmod 755 "$scratch/entry"
printf 'set -p\n' >"$scratch/decoy"
if BASH_ENV="$scratch/decoy" /usr/bin/bash "$scratch/entry" -p; then fail "ordinary Bash with decoy -p is rejected"; fi
printf 'touch "%s"\n' "$scratch/injected" >"$scratch/startup"
if BASH_ENV="$scratch/startup" "$scratch/entry" >/dev/null 2>&1; then fail "startup fixture boundary"; fi
if /usr/bin/env 'BASH_FUNC_source%%=() { touch "$USB_TEST_INJECTED"; }' USB_TEST_INJECTED="$scratch/injected" "$scratch/entry" >/dev/null 2>&1; then fail "exported function fixture boundary"; fi
[[ ! -e $scratch/injected ]] || fail "privileged entrypoint executes caller startup code"
pass "USB privileged approval rejects unsafe Bash startup"
if ! command -v g++ >/dev/null || [[ ! -f /usr/include/usbguard/Rule.hpp ]]; then
skip "native USBGuard matcher requires g++ and USBGuard development headers"
exit 0
fi
printf '%s\n' "$device" >"$scratch/device"
printf '%s\n' "$portable" >"$scratch/policy"
g++ -std=c++17 "$ROOT/test/shell.d/fixtures/usb-authorization/match.cpp" -lusbguard -o "$scratch/match"
"$scratch/match" "$scratch/policy" "$scratch/device"
+967
View File
@@ -0,0 +1,967 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
stub_bin="$scratch/bin"
home="$scratch/home"
rules="$scratch/etc/usbguard/rules.conf"
daemon_config="$scratch/etc/usbguard/usbguard-daemon.conf"
calls="$scratch/calls"
mkdir -p "$stub_bin" "$home" "${rules%/*}"
: >"$rules"
: >"$calls"
cat >"$daemon_config" <<'CONF'
ImplicitPolicyTarget=block
PresentDevicePolicy=apply-policy
InsertedDevicePolicy=apply-policy
AuthorizedDefault=none
RestoreControllerDeviceState=false
RuleFile=/etc/usbguard/rules.conf
CONF
cat >"$stub_bin/omarchy-pkg-missing" <<'STUB'
#!/bin/bash
exit 0
STUB
cat >"$stub_bin/omarchy-pkg-present" <<'STUB'
#!/bin/bash
exit 0
STUB
cat >"$stub_bin/omarchy-pkg-add" <<'STUB'
#!/bin/bash
printf 'pkg-add <%s>\n' "$*" >>"$CALLS"
STUB
cat >"$stub_bin/omarchy-pkg-drop" <<'STUB'
#!/bin/bash
printf 'pkg-drop <%s>\n' "$*" >>"$CALLS"
STUB
cat >"$stub_bin/install" <<'STUB'
#!/bin/bash
if [[ ${!#} == /usr/share/polkit-1/actions/org.omarchy.usb.policy ]]; then exit 0; fi
if [[ $1 == -Dm600 && $2 == -o && $3 == root && $4 == -g && $5 == root ]]; then
exec /usr/bin/install -Dm600 "$6" "$7"
fi
exec /usr/bin/install "$@"
STUB
cat >"$stub_bin/systemctl" <<'STUB'
#!/bin/bash
printf 'systemctl <%s>\n' "$*" >>"$CALLS"
if [[ $* == "show usbguard.service --property=InvocationID --value" ]]; then
printf '%s\n' "${TEST_DAEMON_INVOCATION:-11111111111111111111111111111111}"
elif [[ $* == "--user show-environment" ]]; then
echo "OMARCHY_PATH=${TEST_SESSION_ROOT:-$OMARCHY_PATH}"
elif [[ $* == "--user restart omarchy-usb-authorization.service" ]]; then
[[ ${TEST_WATCHER_FAIL:-0} == 0 ]]
elif [[ $* == "restart usbguard.service" ]]; then
touch "$TEST_GUARD_ACTIVE"
elif [[ $* == "disable --now usbguard.service" ]]; then
[[ ${TEST_STOP_FAIL:-0} == 0 ]] || exit 1
[[ ${TEST_STOP_LIES:-0} == 1 ]] || rm -f "$TEST_GUARD_ACTIVE"
elif [[ $* == "is-active --quiet usbguard.service" ]]; then
[[ -f $TEST_GUARD_ACTIVE ]]
elif [[ $* == "is-enabled --quiet usbguard.service" ]]; then
exit 1
fi
STUB
cat >"$stub_bin/usbguard" <<'STUB'
#!/bin/bash
case "$1" in
generate-policy)
echo 'usbguard <generate-policy>' >>"$CALLS"
[[ ${GENERATE_FAIL:-0} == 0 ]] || exit 1
if [[ ${GENERATE_EMPTY:-0} == 0 ]]; then
printf '%s\n' "${GENERATED_RULE:-allow id 1d6b:0002 name \"Linux Foundation root hub\" hash \"root\"}"
fi
;;
list-devices)
if [[ ${2:-} == "--allowed" ]]; then
[[ ${APPROVAL_QUERY_FAIL:-0} == 0 ]] || exit 1
if [[ -f $TEST_ALLOWED_RULE ]]; then
printf '17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
fi
elif [[ ${2:-} == "--blocked" ]]; then
[[ ${BLOCKED_QUERY_FAIL:-0} == 0 ]] || exit 1
[[ ! -f $TEST_ALLOWED_RULE ]] || exit 0
if [[ -n ${BLOCKED_DEVICES_FILE:-} ]]; then cat "$BLOCKED_DEVICES_FILE"; exit 0; fi
if [[ ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
rule="${BLOCKED_DEVICE_RULE:?}"
if [[ -n ${BLOCKED_DEVICE_RULE_FILE:-} && -f $BLOCKED_DEVICE_RULE_FILE ]]; then
rule=$(<"$BLOCKED_DEVICE_RULE_FILE")
fi
printf '17: %s\n' "$rule"
fi
else
[[ ${BLOCKED_QUERY_FAIL:-0} == 0 ]] || exit 1
if [[ -f $TEST_ALLOWED_RULE ]]; then
[[ ${APPROVAL_QUERY_FAIL:-0} == 0 ]] || exit 1
printf '17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
elif [[ -n ${BLOCKED_DEVICE_RULE:-} && ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
rule="$BLOCKED_DEVICE_RULE"
if [[ -n ${BLOCKED_DEVICE_RULE_FILE:-} && -f $BLOCKED_DEVICE_RULE_FILE ]]; then
rule=$(<"$BLOCKED_DEVICE_RULE_FILE")
fi
printf '17: %s\n' "$rule"
fi
echo '4: allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"'
echo '5: allow id 0627:0001 name "QEMU USB Tablet" hash "tablet"'
fi
;;
append-rule)
printf 'usbguard <append-rule> <%s>\n' "$2" >>"$CALLS"
[[ ${APPROVAL_EXIT_FAIL:-0} == 0 ]] || exit 1
if [[ ${APPROVAL_NO_POLICY:-0} == 0 ]]; then printf '%s\n' "$2" >"$TEST_SAVED_RULE"; fi
echo 40
;;
list-rules)
if [[ -f $TEST_ALLOWED_RULE && ${APPROVAL_POLICY_QUERY_FAIL:-0} == 1 ]]; then exit 1; fi
if [[ -n ${TEST_POLICY_LIST:-} ]]; then cat "$TEST_POLICY_LIST"; fi
if [[ -f $TEST_SAVED_RULE ]]; then
printf '40: %s\n' "$(<"$TEST_SAVED_RULE")"
if [[ -f $TEST_ALLOWED_RULE ]]; then
printf '\t17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
elif [[ -n ${BLOCKED_DEVICE_RULE:-} ]]; then
printf '\t17: %s\n' "$BLOCKED_DEVICE_RULE"
fi
fi
;;
*)
printf 'usbguard' >>"$CALLS"
printf ' <%s>' "$@" >>"$CALLS"
printf '\n' >>"$CALLS"
if [[ $1 == "watch" && -n ${TEST_WATCH_ID_FILE:-} ]]; then
printf '%s\n' "$OMARCHY_USB_AUTHORIZATION_WATCH_ID" >"$TEST_WATCH_ID_FILE"
fi
if [[ $1 == "allow-device" && ${!#} == block* ]]; then
[[ ${APPROVAL_EXIT_FAIL:-0} == 0 ]] || exit 1
if [[ ${APPROVAL_SILENT_FAIL:-0} == 0 ]]; then
rule="${!#}"
printf '%s\n' "${APPROVAL_REPLACEMENT_RULE:-allow ${rule#block }}" >"$TEST_ALLOWED_RULE"
if [[ $2 == "--permanent" && ${APPROVAL_NO_POLICY:-0} == 0 ]]; then
cp "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
fi
fi
fi
;;
esac
STUB
cat >"$stub_bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo' >>"$CALLS"
printf ' <%s>' "$@" >>"$CALLS"
printf '\n' >>"$CALLS"
if [[ $1 == awk && ${!#} == /etc/usbguard/usbguard-daemon.conf ]]; then
args=("$@")
args[-1]="$TEST_DAEMON_CONFIG"
exec "${args[@]}"
elif [[ $1 == test && $2 == -s && $3 == /etc/usbguard/rules.conf ]]; then
exec /usr/bin/test -s "$TEST_RULES"
elif [[ $1 == install && $2 == -Dm600 && $3 == -o && $4 == root && $5 == -g && $6 == root && $8 == /etc/usbguard/rules.conf ]]; then
exec /usr/bin/install -Dm600 "$7" "$TEST_RULES"
elif [[ $1 == install && ${!#} == /usr/share/polkit-1/actions/org.omarchy.usb.policy ]]; then
exit 0
elif [[ $1 == /usr/bin/omarchy-usb-authorization-approve ]]; then
shift
exec "$USB_APPROVE_FIXTURE" "$@"
elif [[ $1 == omarchy-usb-authorization-restore-default ]]; then
echo 1 >"$TEST_SYSFS/usb1/authorized_default"
echo 1 >"$TEST_SYSFS/1-2/authorized"
exit 0
elif [[ $1 == omarchy-usb-authorization-boot ]]; then
exit 0
fi
exec "$@"
STUB
cat >"$stub_bin/gum" <<'STUB'
#!/bin/bash
case "$1" in
style) exit 0 ;;
choose)
echo "gum choose" >>"$CALLS"
[[ ${GUM_CANCEL:-0} == 0 ]] || exit 130
if [[ -n ${GUM_REPLACE_REQUEST:-} ]]; then
jq '.generation = "replacement-generation"' "$GUM_REPLACE_REQUEST" >"$GUM_REPLACE_REQUEST.new"
mv "$GUM_REPLACE_REQUEST.new" "$GUM_REPLACE_REQUEST"
fi
if [[ -n ${GUM_REASSIGN_RULE:-} ]]; then
printf '%s\n' "$GUM_REASSIGN_RULE" >"$BLOCKED_DEVICE_RULE_FILE"
fi
printf '%s\n' "${GUM_CHOICE:-Keep blocked}"
;;
confirm)
if [[ ${GUM_REQUIRE_STDIN:-0} == 1 ]]; then
read -r consent
[[ $consent == yes ]] || exit 1
fi
[[ ${GUM_DECLINE:-0} == 0 ]]
;;
esac
STUB
cat >"$stub_bin/omarchy-notification-send" <<'STUB'
#!/bin/bash
printf 'notification' >>"$CALLS"
printf ' <%s>' "$@" >>"$CALLS"
printf '\n' >>"$CALLS"
if [[ -n ${NOTIFICATION_FAILURE_MARKER:-} && ! -e $NOTIFICATION_FAILURE_MARKER ]]; then
touch "$NOTIFICATION_FAILURE_MARKER"
exit 1
fi
STUB
cat >"$stub_bin/omarchy-notification-wait" <<'STUB'
#!/bin/bash
if [[ -n ${NOTIFICATION_READY_FILE:-} ]]; then
touch "$NOTIFICATION_READY_FILE.waited"
[[ -e $NOTIFICATION_READY_FILE ]]
fi
STUB
cat >"$stub_bin/pkexec" <<'STUB'
#!/bin/bash
[[ $1 == /usr/bin/omarchy-usb-authorization-approve ]] || exit 1
shift
exec "$USB_APPROVE_FIXTURE" "$@"
STUB
cat >"$stub_bin/usbguard-rule-parser" <<'STUB'
#!/bin/bash
exit 0
STUB
chmod +x "$stub_bin"/*
export HOME="$home"
export USER=tester
export OMARCHY_PATH="$ROOT"
export CALLS="$calls"
export PATH="$stub_bin:$ROOT/bin:$PATH"
export TEST_DAEMON_CONFIG="$daemon_config"
export TEST_RULES="$rules"
export TEST_ALLOWED_RULE="$scratch/allowed-rule"
export TEST_SAVED_RULE="$scratch/saved-rule"
export TEST_GUARD_ACTIVE="$scratch/guard-active"
export USB_APPROVE_FIXTURE="$scratch/approve"
export USB_POLICY_FIXTURE="$scratch/policy.sh"
sed "s|/etc/usbguard/rules.conf|$TEST_SAVED_RULE|g" "$ROOT/install/helpers/usb-authorization-policy.sh" >"$USB_POLICY_FIXTURE"
cat >"$USB_APPROVE_FIXTURE" <<'STUB'
#!/bin/bash
set -euo pipefail
source "$USB_POLICY_FIXTURE"
# Boot inventory uses the same helper; permanent behavior is exercised below.
if [[ $1 == 4 || $1 == 5 ]]; then
printf 'boot-approve <%s> <%s>\n' "$1" "$2" >>"$CALLS"
exit 0
fi
usb_authorization_permanent "$@"
STUB
chmod +x "$USB_APPROVE_FIXTURE"
sed -e "s|/usr/bin/omarchy-usb-authorization-approve|$USB_APPROVE_FIXTURE|g" \
-e "s|/usr/share/omarchy/install/helpers/usb-authorization-policy.sh|$USB_POLICY_FIXTURE|g" \
"$ROOT/bin/omarchy-setup-security-usb-authorization" >"$scratch/setup"
chmod +x "$scratch/setup"
if "$ROOT/bin/omarchy-usb-authorization-restore-default" >/dev/null 2>&1; then
fail "the fixed-path USB authorization restore helper requires root"
fi
if "$ROOT/bin/omarchy-usb-authorization-boot" enable >/dev/null 2>&1; then
fail "the fixed-path boot authorization helper requires root"
fi
! grep -q 'OMARCHY_USB_AUTHORIZATION_.*ROOT\|OMARCHY_USB_AUTHORIZATION_.*FILE\|OMARCHY_USB_AUTHORIZATION_.*CONFIG' \
"$ROOT/bin/omarchy-setup-security-usb-authorization" \
"$ROOT/bin/omarchy-remove-security-usb-authorization" \
"$ROOT/bin/omarchy-usb-authorization-boot" ||
fail "user-facing USB authorization commands do not elevate caller-selected paths"
pass "USB authorization keeps privileged paths fixed"
"$scratch/setup" --yes >"$scratch/setup-output"
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root" label "omarchy-usb-authorization-v1"' "$rules" ||
fail "USB authorization setup trusts devices present during enrollment"
[[ $(stat -c %a "$rules") == 600 ]] || fail "USB authorization policy is root-private"
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "USB authorization grants only the IPC access needed by the approval flow"
grep -Fqx 'systemctl <restart usbguard.service>' "$calls" ||
fail "USB authorization reloads its user ACL before the watcher starts"
[[ -f $home/.config/systemd/user/omarchy-usb-authorization.service && ! -L $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
fail "USB authorization installs the graphical-session watcher"
grep -Fqx 'pkg-add <usbguard>' "$calls" || fail "USB authorization ensures USBGuard is installed"
pass "USB authorization enrolls present devices before enabling default-deny"
generate_count=$(grep -c '^usbguard <generate-policy>$' "$calls")
"$scratch/setup" --yes >"$scratch/setup-existing-output"
generate_count_after=$(grep -c '^usbguard <generate-policy>$' "$calls")
(( generate_count_after == generate_count )) || fail "re-enabling preserves the existing trusted-device policy"
pass "USB authorization setup preserves an existing policy"
# Failures before the watcher is ready must never start enforcement. A copied
# unit also repairs the dangling checkout link reported on the affected host.
unit="$home/.config/systemd/user/omarchy-usb-authorization.service"
rm -f "$unit"
ln -s "$scratch/deleted-checkout/service" "$unit"
"$scratch/setup" --yes >/dev/null
[[ -f $unit && ! -L $unit ]] || fail "setup repairs a dangling watcher unit"
for failure in missing-unit missing-helper session-mismatch watcher-start; do
rm -f "$TEST_GUARD_ACTIVE"
: >"$calls"
cp "$scratch/setup" "$scratch/failing-setup"
options=()
case "$failure" in
missing-unit) sed -i "s|^unit_source=.*|unit_source=$scratch/missing-unit|" "$scratch/failing-setup" ;;
missing-helper) sed -i "s|$USB_POLICY_FIXTURE|$scratch/missing-helper|g" "$scratch/failing-setup" ;;
session-mismatch) options+=(TEST_SESSION_ROOT=/missing-runtime) ;;
watcher-start) options+=(TEST_WATCHER_FAIL=1) ;;
esac
if env "${options[@]}" "$scratch/failing-setup" --yes >"$scratch/$failure.log" 2>&1; then
fail "setup must reject $failure"
fi
[[ ! -e $TEST_GUARD_ACTIVE ]] || fail "$failure starts blocking without a watcher"
! grep -Eq 'systemctl <(enable|restart) usbguard.service>' "$calls" || fail "$failure enables enforcement"
done
"$scratch/setup" --yes >/dev/null
watch_line=$(grep -n '^systemctl <--user restart omarchy-usb-authorization.service>' "$calls" | tail -1 | cut -d: -f1)
guard_line=$(grep -n '^systemctl <restart usbguard.service>' "$calls" | tail -1 | cut -d: -f1)
(( watch_line < guard_line )) || fail "watcher must start before enforcement"
pass "USB setup repairs dangling units and refuses enforcement without a working watcher"
: >"$rules"
GENERATE_EMPTY=1 "$scratch/setup" --yes >"$scratch/setup-empty-output"
[[ -s $rules ]] || fail "empty USB inventory must persist an initialized policy"
! grep -q '^allow ' "$rules" || fail "empty inventory must not add permissive rules"
generate_count=$(grep -c '^usbguard <generate-policy>$' "$calls")
"$scratch/setup" --yes >"$scratch/setup-empty-again-output"
[[ $(grep -c '^usbguard <generate-policy>$' "$calls") == "$generate_count" ]] ||
fail "re-enabling an empty policy must not enroll newly attached devices"
: >"$rules"
if GENERATE_FAIL=1 "$scratch/setup" --yes >"$scratch/setup-failed-output" 2>&1; then
fail "enumeration failure must not be accepted as an empty USB inventory"
fi
[[ ! -s $rules ]] || fail "failed enumeration must not install a policy"
pass "USB setup accepts an empty inventory while rejecting enumeration failure"
: >"$calls"
"$scratch/setup" --boot --yes >"$scratch/setup-boot-output"
grep -Fq 'boot-approve <4>' "$calls" ||
fail "boot authorization trusts the connected root controller"
grep -Fq 'boot-approve <5>' "$calls" ||
fail "boot authorization trusts the connected USB devices"
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <enable>' "$calls" ||
fail "boot authorization uses the fixed-path boot-image helper"
grep -Fq 'takes effect after reboot' "$scratch/setup-boot-output" ||
fail "boot authorization explains when the change becomes active"
pass "USB authorization can snapshot connected devices for early-boot denial"
boot_conf="$scratch/limine.conf"
machine_id=$(</etc/machine-id)
touch "$scratch/vmlinuz-linux"
boot_hash=$(b2sum "$scratch/vmlinuz-linux")
boot_hash=${boot_hash%% *}
printf 'quiet splash\0' >"$scratch/embedded-cmdline"
historical_uki="$scratch/embedded.efi_sha256_fixture"
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
embedded_hash=$(b2sum "$historical_uki")
embedded_hash=${embedded_hash%% *}
cat >"$boot_conf" <<CONF
/+Omarchy
comment: machine-id=$machine_id
//linux
comment: kernel-id=linux
path: boot():/vmlinuz-linux#$boot_hash
cmdline: quiet splash
//linux-embedded
comment: kernel-id=linux-embedded
path: boot():/embedded.efi_sha256_fixture#$embedded_hash
cmdline: quiet splash
///Snapshots
////linux
comment: kernel-id=linux
path: boot():/vmlinuz-linux#$boot_hash
cmdline: rootflags=subvol=/snapshot quiet splash
/Other Linux
comment: machine-id=ffffffffffffffffffffffffffffffff
//linux
comment: kernel-id=linux
path: boot():/other-linux
cmdline: quiet splash
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"
fi
usb_authorization_rewrite_boot_cmdlines enabled "$boot_conf" "$machine_id"
usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" disabled ||
fail "a non-Secure-Boot snapshot uses its updated external command line"
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
fail "an external allow parameter cannot hide a stale embedded UKI command line"
fi
printf 'quiet splash usbcore.authorized_default=0\0' >"$scratch/embedded-cmdline"
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
updated_embedded_hash=$(b2sum "$historical_uki")
updated_embedded_hash=${updated_embedded_hash%% *}
sed -i "s/embedded.efi_sha256_fixture#$embedded_hash/embedded.efi_sha256_fixture#$updated_embedded_hash/" "$boot_conf"
usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled ||
fail "boot authorization updates current and snapshot entries for this machine"
[[ $(grep -c 'usbcore.authorized_default=0' "$boot_conf") == 3 ]] ||
fail "boot authorization leaves another operating system's kernel entries unchanged"
printf 'stale image' >"$scratch/vmlinuz-linux"
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
fail "boot authorization rejects a stale Limine verification hash"
fi
: >"$scratch/vmlinuz-linux"
if usb_authorization_rebuild_and_verify disabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
fail "boot authorization detects a stale deny-by-default boot entry during removal"
fi
usb_authorization_rewrite_boot_cmdlines disabled "$boot_conf" "$machine_id"
printf 'quiet splash\0' >"$scratch/embedded-cmdline"
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
disabled_embedded_hash=$(b2sum "$historical_uki")
disabled_embedded_hash=${disabled_embedded_hash%% *}
sed -i "s/embedded.efi_sha256_fixture#$updated_embedded_hash/embedded.efi_sha256_fixture#$disabled_embedded_hash/" "$boot_conf"
usb_authorization_rebuild_and_verify disabled "$boot_conf" "$scratch" "$machine_id" enabled ||
fail "boot authorization removes the parameter from current and snapshot entries"
snapshot_config="$scratch/limine-defaults"
printf 'TARGET_OS_NAME="Omarchy"\n' >"$snapshot_config"
usb_authorization_enable_snapshot_setting "$snapshot_config"
grep -Fqx 'SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0' "$snapshot_config" ||
fail "future Limine snapshots inherit boot-time USB authorization"
usb_authorization_enable_snapshot_setting "$snapshot_config"
[[ $(grep -Fxc '# Omarchy USB authorization begin' "$snapshot_config") == 1 ]] ||
fail "the snapshot setting remains idempotent"
usb_authorization_disable_snapshot_setting "$snapshot_config"
! grep -Fq 'usbcore.authorized_default=0' "$snapshot_config" ||
fail "removing boot authorization restores future snapshot defaults"
pass "boot authorization verifies generated entries despite Limine false-success exits"
manifest="$scratch/snapshots.json"
jq -n --arg hash "$boot_hash" '
def kernel:
{cmdlineDetails: [{limineKey: "CMDLINE", cmdline: "quiet rootflags=subvol=@", snapshotCmdline: "quiet rootflags=subvol=/snapshot"}],
allInConfig: [" cmdline: quiet rootflags=subvol=@"],
allInSnapshotConfig: ["comment: kernel-id=linux", "path: boot():/vmlinuz-linux#" + $hash, "cmdline: quiet rootflags=subvol=/snapshot"],
subKernels: [], properties: {untouched: "usbcore.authorized_default=0"}};
{jsonFormatVersion: "1.3.0", snapshotEntries: [{kernelEntries: [kernel | .subKernels = [kernel | .allInSnapshotConfig = []]]}], uuid: "preserve-me"}
' >"$manifest"
cp "$manifest" "$scratch/manifest-original.json"
for expected in enabled disabled; do
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest"
cp "$manifest" "$scratch/manifest-once.json"
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest"
cmp -s "$manifest" "$scratch/manifest-once.json" || fail "manifest updates are idempotent"
# Model both Limine render paths: cached lines and structured details.
for representation in cached structured; do
{
printf '/Omarchy\ncomment: machine-id=%s\n //linux\n' "$machine_id"
if [[ $representation == "cached" ]]; then
jq -r '.snapshotEntries[0].kernelEntries[0].allInSnapshotConfig[]' "$manifest"
else
printf 'comment: kernel-id=linux\npath: boot():/vmlinuz-linux#%s\n' "$boot_hash"
jq -r '"cmdline: " + .snapshotEntries[0].kernelEntries[0].subKernels[0].cmdlineDetails[0].snapshotCmdline' "$manifest"
fi
} >"$scratch/regenerated.conf"
usb_authorization_verify_boot_entries "$expected" "$scratch/regenerated.conf" "$scratch" "$machine_id" disabled ||
fail "snapshot synchronization preserves $expected policy from $representation entries"
done
jq -e '.snapshotEntries[0].kernelEntries[0].properties.untouched == "usbcore.authorized_default=0"' "$manifest" >/dev/null ||
fail "manifest changes preserve unrelated metadata"
done
jq -S . "$manifest" >"$scratch/manifest-after.json"
jq -S . "$scratch/manifest-original.json" >"$scratch/manifest-before.json"
cmp -s "$scratch/manifest-before.json" "$scratch/manifest-after.json" ||
fail "disable restores all stored command lines without changing other snapshot data"
printf '{bad json' >"$scratch/bad-manifest.json"
if usb_authorization_rewrite_snapshot_manifest enabled "$scratch/bad-manifest.json" 2>/dev/null; then
fail "invalid manifests must fail closed"
fi
[[ $(<"$scratch/bad-manifest.json") == '{bad json' ]] || fail "failed manifest updates preserve the original"
pass "USB boot policy survives snapshot regeneration from cached and structured manifests"
: >"$calls"
"$ROOT/bin/omarchy-remove-security-usb-authorization" --boot-only --yes >"$scratch/remove-boot-output"
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <disable>' "$calls" ||
fail "boot-only removal rebuilds the permissive boot policy"
! grep -Fq 'systemctl <--user disable --now omarchy-usb-authorization.service>' "$calls" ||
fail "boot-only removal leaves the userspace approval watcher enabled"
grep -Fq 'takes effect after reboot' "$scratch/remove-boot-output" ||
fail "boot-only removal explains when the change becomes active"
pass "early-boot denial can be removed without disabling USBGuard"
grep -qx 'usbguard' "$ROOT/install/omarchy-base.packages" || fail "USBGuard is installed by default"
grep -Fq 'config/usb-authorization.sh' "$ROOT/install/config/all.sh" ||
fail "fresh installs configure the USB authorization policy"
grep -Fq 'omarchy-usb-authorization.service' "$ROOT/install/user/first-run/enable-user-units.sh" ||
fail "fresh installs enable the USB approval watcher"
grep -Fq 'usb_authorization_provision_owner "$username"' "$ROOT/bin/omarchy-provision-owner" ||
fail "deferred provisioning enrolls the owner before finishing"
grep -Fqx 'omarchy-setup-security-usb-authorization --yes' "$ROOT/migrations/1789433473.sh" ||
fail "existing installs enable USB authorization during migration"
: >"$rules"
: >"$calls"
export OMARCHY_INSTALL="$ROOT/install"
export OMARCHY_INSTALL_USER=tester
export OMARCHY_USB_AUTHORIZATION_RULES_FILE="$rules"
export OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG="$daemon_config"
bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-output"
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root" label "omarchy-usb-authorization-v1"' "$rules" ||
fail "fresh installation enrolls devices present during installation"
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "fresh installation grants the owner narrowly scoped approval access"
grep -Fqx 'systemctl <enable usbguard.service>' "$calls" ||
fail "fresh installation enables USBGuard for the first boot"
pass "USB authorization is the default for fresh and existing installations"
: >"$rules"
GENERATE_EMPTY=1 bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-empty-output"
[[ -s $rules ]] || fail "fresh install accepts a successful empty USB inventory"
! grep -q '^allow ' "$rules" || fail "empty fresh-install inventory stays default-deny"
: >"$rules"
: >"$calls"
if GENERATE_FAIL=1 bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-enumeration-failed" 2>&1; then
fail "fresh install must reject a generator failure"
fi
! grep -Fq 'systemctl <enable usbguard.service>' "$calls" || fail "failed enumeration must not enable USBGuard"
pass "fresh installation distinguishes no USB hardware from enumeration failure"
: >"$rules"
: >"$calls"
OMARCHY_INSTALL_USER="" bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-deferred-output"
[[ ! -s $rules ]] || fail "deferred installation must not enroll the builder's hardware"
! grep -q '^usbguard' "$calls" || fail "deferred installation must not enumerate or grant owner ACLs"
grep -Fqx 'systemctl <disable usbguard.service>' "$calls" || fail "deferred installation leaves enforcement disabled"
! grep -Fq 'systemctl <enable usbguard.service>' "$calls" || fail "deferred installation must not enable enforcement"
source "$ROOT/install/helpers/usb-authorization.sh"
echo 'allow id 1234:0001 name "Builder keyboard" hash "builder"' >"$rules"
owner_rule='allow id 1234:0002 name "Owner keyboard" hash "owner"'
: >"$calls"
GENERATED_RULE="$owner_rule" usb_authorization_provision_owner owner "$rules" "$daemon_config"
[[ $(<"$rules") == "$owner_rule"' label "omarchy-usb-authorization-v1"' ]] || fail "owner enrollment must replace inherited builder trust"
grep -Fqx 'usbguard <add-user> <owner> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "owner provisioning grants approval access"
[[ $(tail -2 "$calls") == $'systemctl <enable usbguard.service>\nsystemctl <restart usbguard.service>' ]] ||
fail "enforcement starts only after owner policy and ACL setup"
: >"$calls"
if GENERATE_FAIL=1 usb_authorization_provision_owner owner "$rules" "$daemon_config" >"$scratch/owner-failed" 2>&1; then
fail "owner enrollment must reject enumeration failure"
fi
[[ $(<"$rules") == "$owner_rule"' label "omarchy-usb-authorization-v1"' ]] || fail "failed enrollment preserves the old policy"
! grep -q '^systemctl' "$calls" || fail "failed owner enrollment must not enable enforcement"
: >"$calls"
sed 's/ImplicitPolicyTarget=block/ImplicitPolicyTarget=allow/' "$daemon_config" >"$scratch/insecure-daemon.conf"
if usb_authorization_provision_owner owner "$rules" "$scratch/insecure-daemon.conf" >"$scratch/owner-insecure" 2>&1; then
fail "owner enrollment rejects insecure settings even when called in a conditional"
fi
[[ ! -s $calls ]] || fail "invalid settings must prevent all enrollment changes"
pass "deferred installs enroll owner hardware before enabling enforcement"
malicious_rule='block id 07a6:8513 name "$(touch '"$scratch"'/injected)" hash "attacker" with-interface 02:06:00'
export BLOCKED_DEVICE_RULE="$malicious_rule"
notification_count=$(grep -c '^notification' "$calls" || true)
for presence in Insert Present; do
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
USBGUARD_DEVICE_EVENT="$presence" \
USBGUARD_DEVICE_TARGET=block \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
done
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_TARGET_NEW=allow \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_RULE="${malicious_rule/#block/allow}" \
"$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls" || true) == "$notification_count" ]] ||
fail "a trusted device's transient blocked presence must not prompt"
BLOCKED_DEVICE_PRESENT=0 \
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls" || true) == "$notification_count" ]] ||
fail "a stale blocked event must not prompt after authorization or removal"
pass "USB alerts use the final policy and discard stale events"
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_EVENT=Insert \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
[[ -n $request ]] || fail "a blocked USB device creates a review request"
[[ $(stat -c %a "$request") == 600 ]] || fail "device-controlled request data stays private"
[[ $(jq -r .id "$request") == 17 ]] || fail "the request records the USBGuard device id"
[[ $(jq -r .rule "$request") == "$malicious_rule" ]] || fail "the request preserves the identity snapshot"
! grep -Fq "$malicious_rule" "$calls" || fail "device text never enters the notification's launch command"
[[ ! -e $scratch/injected ]] || fail "device text cannot execute during notification"
token=$(basename "${request%.json}")
grep -Eq "notification .*<omarchy-usb-authorization-review> <$token>$" "$calls" ||
fail "the notification action carries only an opaque request token"
pass "blocked device metadata remains data across the desktop notification boundary"
notification_count=$(grep -c '^notification' "$calls")
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event" &
scan_pid=$!
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
wait "$scan_pid"
[[ $(grep -c '^notification' "$calls") == "$notification_count" ]] ||
fail "overlapping scan and policy events must not duplicate pending prompts"
pass "USB reconnect scans and policy events deduplicate pending requests"
# Dismissing/canceling a notification leaves its request, even after the shell
# discards the archived action. Reusing the ID after daemon restart must prompt.
notification_count=$(grep -c '^notification' "$calls")
export TEST_DAEMON_INVOCATION=22222222222222222222222222222222
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event" &
scan_pid=$!
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
wait "$scan_pid"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
fail "reused IDs must get exactly one new notification after daemon restart"
[[ -f $request ]] || fail "the recovered notification must have a reviewable request"
# Starting another watcher/session also expires old notification deduplication.
notification_count=$(grep -c '^notification' "$calls")
export OMARCHY_USB_AUTHORIZATION_WATCH_ID=new-session
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
fail "a new watcher must recover an orphaned approval action"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
fail "same-generation scans must still deduplicate"
# A removal that arrives late must not erase a new connection's request.
USBGUARD_IPC_SIGNAL=Device.PresenceChanged USBGUARD_DEVICE_EVENT=Remove \
USBGUARD_DEVICE_ID=17 USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
[[ -f $request ]] || fail "delayed removal must preserve a currently blocked device's request"
BLOCKED_DEVICE_PRESENT=0 \
USBGUARD_IPC_SIGNAL=Device.PresenceChanged USBGUARD_DEVICE_EVENT=Remove \
USBGUARD_DEVICE_ID=17 USBGUARD_DEVICE_RULE="${malicious_rule/#block/allow}" \
"$ROOT/bin/omarchy-usb-authorization-event"
[[ ! -e $request ]] || fail "device removal must clean up its pending request"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 2)) ]] ||
fail "a reappeared device must receive a fresh notification"
# Upgrade from requests written before generations existed, and remove stale
# requests for devices no longer in the inventory.
jq 'del(.generation)' "$request" >"$scratch/legacy-request"
mv "$scratch/legacy-request" "$request"
legacy_orphan="${request%/*}/request-$(printf '%064d' 0).json"
printf '{"id":"99","rule":"block id 0000:0000"}\n' >"$legacy_orphan"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 3)) ]] ||
fail "legacy request markers must not suppress notification recovery"
[[ ! -e $legacy_orphan ]] || fail "reconnect must clean up old-generation orphan requests"
TEST_WATCH_ID_FILE="$scratch/watch-one" "$ROOT/bin/omarchy-usb-authorization-watch"
TEST_WATCH_ID_FILE="$scratch/watch-two" "$ROOT/bin/omarchy-usb-authorization-watch"
[[ -s $scratch/watch-one && $(<"$scratch/watch-one") != "$(<"$scratch/watch-two")" ]] ||
fail "each watcher must export a fresh lifetime identifier"
pass "USB prompts recover across daemon and session generations and device removal"
GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-once-output"
grep -Fqx "usbguard <allow-device> <$malicious_rule>" "$calls" || fail "review can allow the exact device once"
[[ ! -e $request ]] || fail "a completed review consumes its request"
pass "USB review allows a still-matching device once"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
# A daemon restart discards the temporary approval. The IPC reconnect must
# recover the blocked device even when its startup policy events were missed.
notification_count=$(grep -c '^notification' "$calls")
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
[[ -f $request ]] || fail "reconnection recreates a consumed Allow once request"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
fail "reconnection prompts again for a temporary approval lost on restart"
GUM_CHOICE='Keep blocked' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >/dev/null
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_TARGET_OLD=allow \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
[[ -f $request ]] || fail "a later allow-to-block policy transition creates a request"
GUM_CHOICE='Keep blocked' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >/dev/null
pass "USB approval recovers after IPC reconnects and policy transitions"
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_EVENT=Insert \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
token=$(basename "${request%.json}")
GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-always-output"
grep -Fq 'usbguard <append-rule> <allow ' "$calls" || fail "review can persist a portable device rule"
append_count=$(grep -c '^usbguard <append-rule>' "$calls")
"$USB_APPROVE_FIXTURE" 17 "allow ${malicious_rule#block }"
[[ $(grep -c '^usbguard <append-rule>' "$calls") == "$append_count" ]] || fail "confirmed permanent retry must preserve rule ordering without duplicate rules"
pass "USB review supports explicit persistent trust"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
# Unmarked rules might be intentional manual restrictions. Review is opt-in,
# never accepted through --yes, and only the selected live identity is added.
legacy_rule='allow id 046d:c53a name "Receiver" hash "receiver" parent-hash "hub-a" via-port "1-2"'
printf '%s\n' "$legacy_rule" >"$TEST_ALLOWED_RULE"
printf '12: %s\n\t17: %s\n' "$legacy_rule" "$legacy_rule" >"$scratch/legacy-policy"
cp "$scratch/legacy-policy" "$scratch/legacy-before"
if "$scratch/setup" --review-existing --yes >/dev/null 2>&1; then fail "legacy migration requires individual consent"; fi
TEST_POLICY_LIST="$scratch/legacy-policy" GUM_DECLINE=1 "$scratch/setup" --review-existing >/dev/null
[[ ! -e $TEST_SAVED_RULE ]] || fail "declining migration adds portable trust"
printf 'yes\nyes\nyes\n' | TEST_POLICY_LIST="$scratch/legacy-policy" GUM_REQUIRE_STDIN=1 "$scratch/setup" --review-existing >/dev/null
[[ $(<"$TEST_SAVED_RULE") == 'allow id 046d:c53a name "Receiver" hash "receiver" label "omarchy-usb-authorization-v1"' ]] || fail "legacy review saves only identity constraints"
cmp -s "$scratch/legacy-policy" "$scratch/legacy-before" || fail "legacy review rewrites existing manual rules"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
printf '12: block id 07a6:8513\n\t17: %s\n' "$malicious_rule" >"$scratch/deny-policy"
if TEST_POLICY_LIST="$scratch/deny-policy" "$USB_APPROVE_FIXTURE" 17 "$malicious_rule" >/dev/null 2>&1; then fail "permanent approval must respect earlier manual denial"; fi
[[ ! -e $TEST_SAVED_RULE && ! -e $TEST_ALLOWED_RULE ]] || fail "manual denial changed policy or authorization"
printf '%s\n' "allow ${malicious_rule#block }" >"$TEST_ALLOWED_RULE"
if TEST_POLICY_LIST="$scratch/deny-policy" "$USB_APPROVE_FIXTURE" 17 "allow ${malicious_rule#block }" >/dev/null 2>&1; then fail "temporary allow cannot hide an earlier manual denial"; fi
rm -f "$TEST_ALLOWED_RULE"
printf '12: block\n' >"$scratch/deny-all-policy"
if TEST_POLICY_LIST="$scratch/deny-all-policy" "$USB_APPROVE_FIXTURE" 17 "$malicious_rule" >/dev/null 2>&1; then fail "unconditional manual denial must be preserved"; fi
pass "USB legacy review requires consent and preserves manual policy"
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_EVENT=Insert \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
token=$(basename "${request%.json}")
device_rule_file="$scratch/blocked-device-rule"
printf '%s\n' "$malicious_rule" >"$device_rule_file"
replacement_rule='block id 1d50:60c7 name "Replacement gadget" hash "replacement"'
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
if BLOCKED_DEVICE_RULE_FILE="$device_rule_file" \
GUM_REASSIGN_RULE="$replacement_rule" \
GUM_CHOICE='Always allow this device' \
"$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-reassigned-output" 2>&1; then
fail "review rejects a USBGuard ID reassigned while the approval dialog is open"
fi
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] ||
fail "an ID reassigned during review cannot authorize a different device"
[[ ! -e $request ]] || fail "a reassigned-ID review request is discarded"
pass "USB review revalidates identity after the approval dialog"
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_EVENT=Insert \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="$malicious_rule" \
"$ROOT/bin/omarchy-usb-authorization-event"
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
token=$(basename "${request%.json}")
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
if BLOCKED_DEVICE_PRESENT=0 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-stale-output" 2>&1; then
fail "review rejects a request after its device disappears"
fi
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] ||
fail "a reused USBGuard id cannot authorize a different device"
[[ ! -e $request ]] || fail "a stale review request is discarded"
pass "USB review binds approval to the device snapshot"
for choice in 'Allow once' 'Always allow this device'; do
for failure in APPROVAL_SILENT_FAIL APPROVAL_EXIT_FAIL APPROVAL_QUERY_FAIL APPROVAL_REPLACEMENT_RULE; do
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE" "$request"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
value=1
[[ $failure != "APPROVAL_REPLACEMENT_RULE" ]] || value='allow id 9999:9999 hash "replacement"'
if env "$failure=$value" GUM_CHOICE="$choice" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-failed" 2>&1; then
fail "$choice must reject $failure"
fi
[[ -f $request ]] || fail "unverified approval must retain the request"
! grep -Eq 'USB accessory (allowed until|added to)' "$scratch/review-failed" || fail "failed approval must not report success"
done
done
for failure in APPROVAL_NO_POLICY APPROVAL_POLICY_QUERY_FAIL; do
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE" "$request"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
if env "$failure=1" GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-no-policy" 2>&1; then
fail "permanent approval must reject $failure"
fi
[[ -f $request ]] || fail "unverified permanent approval must retain the request"
done
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-retry"
[[ ! -e $request ]] || fail "a verified retry consumes the retained request"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
pass "USB approvals verify device and permanent policy before consuming requests"
# Exercise retries without resetting the device state left by the first attempt.
for choice in 'Allow once' 'Always allow this device'; do
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
failure=APPROVAL_QUERY_FAIL
[[ $choice != 'Always allow this device' ]] || failure=APPROVAL_POLICY_QUERY_FAIL
if env "$failure=1" GUM_CHOICE="$choice" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/retry-first" 2>&1; then
fail "transient verification failure must retain the approval"
fi
[[ -s $TEST_ALLOWED_RULE && -f $request ]] || fail "fixture must retain an already-allowed device"
[[ $(jq -r .approval "$request") == "$choice" ]] || fail "retry must remember the selected approval"
[[ $(stat -c %a "$request") == 600 ]] || fail "saved approval intent stays private"
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
prompt_count=$(grep -c '^gum choose' "$calls")
GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/retry-second"
[[ ! -e $request ]] || fail "verified approval retry consumes the request"
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "already-allowed retry must not reapply authorization"
[[ $(grep -c '^gum choose' "$calls") == "$prompt_count" ]] || fail "retry must resume the original approval without another choice"
if [[ $choice == 'Always allow this device' ]]; then
grep -Fq 'added to the trusted policy' "$scratch/retry-second" || fail "permanent retry must finish permanent verification"
else
grep -Fq 'allowed until it is disconnected' "$scratch/retry-second" || fail "once retry must finish temporary verification"
fi
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
done
pass "USB approval retries resume successful temporary and permanent authorization"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
if APPROVAL_NO_POLICY=1 GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy" 2>&1; then
fail "permanent approval requires saved policy"
fi
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
if APPROVAL_NO_POLICY=1 GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy-retry" 2>&1; then
fail "retry cannot downgrade permanent approval when policy is missing"
fi
[[ -f $request && $(jq -r .approval "$request") == 'Always allow this device' ]] || fail "missing policy retains original permanent intent"
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "retry must not repeat authorization on an allowed device"
printf '%s\n' "allow ${malicious_rule#block }" >"$TEST_ALLOWED_RULE"
for failure in BLOCKED_QUERY_FAIL APPROVAL_QUERY_FAIL; do
if env "$failure=1" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/query-retry" 2>&1; then
fail "retry must fail when inventory cannot be read"
fi
[[ -f $request ]] || fail "inventory query failures must preserve pending approvals"
done
printf '%s\n' "allow ${replacement_rule#block }" >"$TEST_ALLOWED_RULE"
if "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/replaced-retry" 2>&1; then
fail "retry must reject a replacement allowed device"
fi
[[ ! -e $request ]] || fail "a proven replacement consumes the stale request"
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "replacement retry must not authorize anything"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
pass "USB approval retries preserve permanent intent and validate allowed identity"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
if BLOCKED_QUERY_FAIL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/fresh-query-failure" 2>&1; then
fail "fresh review must fail on inventory query failure"
fi
[[ -f $request ]] || fail "fresh inventory failure must not delete the request"
jq '.approval = "invalid"' "$request" >"$scratch/invalid-approval"
mv "$scratch/invalid-approval" "$request"
if "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/invalid-intent" 2>&1; then
fail "invalid saved intent must not authorize"
fi
jq 'del(.approval)' "$request" >"$scratch/fresh-request"
mv "$scratch/fresh-request" "$request"
if GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/cancel" 2>&1; then
fail "canceled review must not authorize"
fi
jq -e '.approval == null' "$request" >/dev/null || fail "canceling must not store an approval"
if GUM_REPLACE_REQUEST="$request" GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/replaced-request" 2>&1; then
fail "open dialog must not approve a replaced request"
fi
[[ $(jq -r .generation "$request") == 'replacement-generation' ]] || fail "stale dialog must leave replacement request intact"
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "canceled or superseded requests cannot authorize"
rm -f "$request"
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
if APPROVAL_EXIT_FAIL=1 GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/blocked-retry-first" 2>&1; then
fail "failed apply must retain a blocked request"
fi
GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/blocked-retry-second"
[[ -s $TEST_SAVED_RULE && ! -e $request ]] || fail "blocked retry must apply its saved permanent choice"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
pass "USB review preserves transient failures and rejects canceled or replaced dialogs"
notification_count=$(grep -c '^notification' "$calls")
printf '17: %s\n18: %s\n' "$malicious_rule" 'block id 1234:5678 name "Second blocked device" hash "second"' >"$scratch/two-blocked"
NOTIFICATION_READY_FILE="$scratch/notification-ready" \
NOTIFICATION_FAILURE_MARKER="$scratch/first-send-failed" \
BLOCKED_DEVICES_FILE="$scratch/two-blocked" \
USBGUARD_IPC_SIGNAL=IPC.Connected \
timeout 10 "$ROOT/bin/omarchy-usb-authorization-event" &
scan_pid=$!
for ((attempt=0; attempt<50; attempt++)); do
[[ -e $scratch/notification-ready.waited ]] && break
sleep 0.02
done
[[ -e $scratch/notification-ready.waited ]] || fail "startup waits for notification readiness"
[[ $(grep -c '^notification' "$calls") == "$notification_count" ]] ||
fail "startup does not send before the notification server is ready"
touch "$scratch/notification-ready"
wait "$scan_pid" || fail "startup scan must retry and finish without another USB event"
[[ $(grep -c '^notification' "$calls") == $((notification_count + 3)) ]] ||
fail "two blocked devices must be notified despite a failed first send"
[[ -f $request ]] || fail "successful retry leaves a reviewable request"
pass "USB startup waits for notifications and automatically retries delivery for all devices"
sysfs="$scratch/sys/bus/usb/devices"
mkdir -p "$sysfs/usb1" "$sysfs/1-2"
echo 0 >"$sysfs/usb1/authorized_default"
echo 0 >"$sysfs/1-2/authorized"
export TEST_SYSFS="$sysfs"
for failure in TEST_STOP_FAIL TEST_STOP_LIES; do
touch "$TEST_GUARD_ACTIVE"
: >"$calls"
if env "$failure=1" "$ROOT/bin/omarchy-remove-security-usb-authorization" --yes >"$scratch/$failure.log" 2>&1; then
fail "removal must reject $failure"
fi
[[ -f $unit ]] || fail "failed daemon stop removes watcher"
! grep -q '^usbguard <remove-user>' "$calls" || fail "failed daemon stop revokes watcher access"
! grep -q '^systemctl <--user disable' "$calls" || fail "failed daemon stop disables watcher"
done
pass "USB removal preserves approval prompts if stopping enforcement fails"
"$ROOT/bin/omarchy-remove-security-usb-authorization" --yes >"$scratch/remove-output"
[[ $(<"$sysfs/usb1/authorized_default") == 1 ]] || fail "removal restores root-hub default authorization"
[[ $(<"$sysfs/1-2/authorized") == 1 ]] || fail "removal reauthorizes devices left connected"
grep -Fqx 'systemctl <disable --now usbguard.service>' "$calls" || fail "removal stops a service Omarchy enabled"
grep -Fqx 'usbguard <remove-user> <tester>' "$calls" || fail "removal drops the user's USBGuard IPC access"
grep -Fqx 'sudo <omarchy-usb-authorization-restore-default>' "$calls" ||
fail "removal restores USB authorization only through the fixed-path root helper"
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <disable>' "$calls" ||
fail "removing USB authorization also removes early-boot denial"
! grep -Fqx 'pkg-drop <usbguard>' "$calls" || fail "disabling keeps the default USBGuard package installed"
[[ ! -e $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
fail "removal deletes the graphical-session watcher"
pass "USB authorization removal restores the original default-allow behavior"
cat >"$stub_bin/omarchy-setup-security-usb-authorization" <<'STUB'
#!/bin/bash
printf 'migrate-setup <%s>\n' "$*" >>"$CALLS"
STUB
chmod +x "$stub_bin/omarchy-setup-security-usb-authorization"
: >"$calls"
bash -euo pipefail "$ROOT/migrations/1790608617.sh" >/dev/null
! grep -q '^migrate-setup' "$calls" || fail "migration overrides USB opt-out"
touch "$TEST_GUARD_ACTIVE"
bash -euo pipefail "$ROOT/migrations/1790608617.sh" >/dev/null
grep -Fqx 'migrate-setup <--yes>' "$calls" || fail "migration repairs an active daemon without a watcher"
pass "USB migration repairs enabled protection and preserves opt-out"
+257
View File
@@ -0,0 +1,257 @@
#!/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
+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
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
# Exercise reset staging in a temporary root with the real offline systemctl.
# Never invoke the reset command's main function or any actual disk operations.
python - "$ROOT" <<'PY'
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
repo = Path(sys.argv[1])
source = (repo / 'bin/omarchy-system-factory-reset').read_text()
functions = []
for name in ['install_provisioning_units', 'scrub_factory_accounts', 'sanitize_factory_baseline', 'stage_full_reset']:
match = re.search(r'^' + name + r'\(\) \{\n.*?^\}', source, re.M | re.S)
assert match, name
functions.append(match.group())
with tempfile.TemporaryDirectory(prefix='omarchy-usb-reset-') as tmp:
scratch = Path(tmp)
for scenario in ['enabled', 'disabled', 'absent', 'disable-failure']:
root = scratch / scenario
top = root / 'top'
factory = top / '@factory'
for rel in ['etc/systemd/system/basic.target.wants', 'usr/lib/systemd/system',
'etc/usbguard', 'usr/bin', 'usr/share/omarchy/install/provisioning']:
(factory / rel).mkdir(parents=True, exist_ok=True)
(top / '@').mkdir()
(top / '@' / 'original-root').touch()
(factory / 'etc/passwd').write_text('root:x:0:0:root:/root:/bin/bash\n')
(factory / 'etc/machine-id').write_text('old-machine-id\n')
policy = 'allow id 1234:0001 name "Original owner keyboard" hash "old-owner"\n'
(factory / 'etc/usbguard/rules.conf').write_text(policy)
unit = factory / 'usr/lib/systemd/system/usbguard.service'
enabled_path = Path('etc/systemd/system/basic.target.wants/usbguard.service')
if scenario != 'absent':
unit.write_text('[Unit]\nDescription=Fixture USBGuard\n[Service]\nExecStart=/bin/true\n[Install]\nWantedBy=basic.target\n')
if scenario in ['enabled', 'disable-failure']:
(factory / enabled_path).symlink_to('/usr/lib/systemd/system/usbguard.service')
owner = factory / 'usr/bin/omarchy-provision-owner'
owner.touch()
owner.chmod(0o755)
for name in ['omarchy-provision-owner.service', 'omarchy-system-factory-reset-finish.service']:
shutil.copyfile(repo / 'install/provisioning' / name,
factory / 'usr/share/omarchy/install/provisioning' / name)
harness = '''#!/bin/bash
set -euo pipefail
TOP_MNT="$1/top"
NEXT_NAME=@next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE="$1/log"
log() { :; }
fail() { echo "$*" >&2; exit 1; }
btrfs() {
if [[ $1 == subvolume && $2 == snapshot ]]; then
cp -a "$3" "$4"
elif [[ $1 == property ]]; then
:
else
return 90
fi
}
# Account cleanup has its own real-tool suite; keep this fixture focused on USB.
usermod() {
[[ $1 == --root && ( $2 == "$TOP_MNT/$NEXT_NAME" || $2 == "$TOP_MNT/@factory" ) &&
$3 == --password && $4 == '!' && $5 == root ]]
}
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
sync() { :; }
systemctl() {
# All service changes must address the staged root, never the live machine.
[[ $1 == "--root=$TOP_MNT/$NEXT_NAME" && $2 == disable && $3 == usbguard.service ]] || return 91
[[ $2 != "${FAIL_OPERATION:-}" ]] || return 92
command systemctl "$@"
}
''' + '\n\n'.join(functions) + '\nstage_full_reset\n'
runner = root / 'run'
runner.write_text(harness)
env = os.environ.copy()
env['FAIL_OPERATION'] = 'disable' if scenario == 'disable-failure' else ''
result = subprocess.run(['bash', str(runner), str(root)], env=env, capture_output=True, text=True)
if scenario == 'disable-failure':
assert result.returncode != 0, 'failed disable must abort reset staging'
assert (top / '@' / 'original-root').exists(), 'failure must not activate the reset root'
assert 'could not disable USBGuard' in result.stderr
else:
assert result.returncode == 0, result.stderr
staged = top / '@'
assert not (staged / enabled_path).is_symlink(), 'reset must not start USBGuard before enrollment'
assert (staged / 'var/lib/omarchy/provisioning/pending').exists()
assert (staged / 'etc/usbguard/rules.conf').read_text() == policy
if scenario == 'enabled':
assert (factory / enabled_path).is_symlink(), 'only the staged clone is disabled'
# The owner-enrollment helper enables the daemon after policy and
# ACL setup. Confirm an ordinary enable can reverse this disable.
subprocess.run(['systemctl', '--root=' + str(staged), 'enable', 'usbguard.service'],
check=True, capture_output=True, text=True)
assert (staged / enabled_path).is_symlink()
print('ok - factory USB enrollment staging: ' + scenario)
PY