diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index a078c329..fd8fe2f5 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -1174,15 +1174,6 @@ run_provisioning() { return 1 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" diff --git a/bin/omarchy-remove-security-thunderbolt-authorization b/bin/omarchy-remove-security-thunderbolt-authorization deleted file mode 100755 index 0910041d..00000000 --- a/bin/omarchy-remove-security-thunderbolt-authorization +++ /dev/null @@ -1,10 +0,0 @@ -#!/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." diff --git a/bin/omarchy-remove-security-thunderbolt-boot b/bin/omarchy-remove-security-thunderbolt-boot deleted file mode 100755 index 8657e527..00000000 --- a/bin/omarchy-remove-security-thunderbolt-boot +++ /dev/null @@ -1,9 +0,0 @@ -#!/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." diff --git a/bin/omarchy-remove-security-usb-authorization b/bin/omarchy-remove-security-usb-authorization deleted file mode 100755 index b3961391..00000000 --- a/bin/omarchy-remove-security-usb-authorization +++ /dev/null @@ -1,89 +0,0 @@ -#!/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." diff --git a/bin/omarchy-setup-security-thunderbolt-authorization b/bin/omarchy-setup-security-thunderbolt-authorization deleted file mode 100755 index 45eb2652..00000000 --- a/bin/omarchy-setup-security-thunderbolt-authorization +++ /dev/null @@ -1,42 +0,0 @@ -#!/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 diff --git a/bin/omarchy-setup-security-thunderbolt-boot b/bin/omarchy-setup-security-thunderbolt-boot deleted file mode 100755 index 23909db3..00000000 --- a/bin/omarchy-setup-security-thunderbolt-boot +++ /dev/null @@ -1,23 +0,0 @@ -#!/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 diff --git a/bin/omarchy-setup-security-usb-authorization b/bin/omarchy-setup-security-usb-authorization deleted file mode 100755 index ab3e2584..00000000 --- a/bin/omarchy-setup-security-usb-authorization +++ /dev/null @@ -1,191 +0,0 @@ -#!/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 - -# Boot-time blocking goes into Limine's own configuration, which a Mac's boot -# package owns, so it is not offered there. -if (( from_boot )) && omarchy-hw-aarch64-apple; then - echo "USB authorization from boot is not available on Apple Silicon yet; USB device authorization after login still works." >&2 - exit 2 -fi - -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 diff --git a/bin/omarchy-system-factory-reset b/bin/omarchy-system-factory-reset index c46c5cb1..ed308ba7 100755 --- a/bin/omarchy-system-factory-reset +++ b/bin/omarchy-system-factory-reset @@ -462,12 +462,6 @@ 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 @@ -493,14 +487,6 @@ 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_* @@ -516,13 +502,6 @@ 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 diff --git a/bin/omarchy-thunderbolt-authorization-admin b/bin/omarchy-thunderbolt-authorization-admin deleted file mode 100755 index 28eec766..00000000 --- a/bin/omarchy-thunderbolt-authorization-admin +++ /dev/null @@ -1,17 +0,0 @@ -#!/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 "$@" diff --git a/bin/omarchy-thunderbolt-authorization-approve b/bin/omarchy-thunderbolt-authorization-approve deleted file mode 100755 index 30b86db0..00000000 --- a/bin/omarchy-thunderbolt-authorization-approve +++ /dev/null @@ -1,25 +0,0 @@ -#!/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" diff --git a/bin/omarchy-thunderbolt-authorization-boot-enabled b/bin/omarchy-thunderbolt-authorization-boot-enabled deleted file mode 100755 index 3fb023d8..00000000 --- a/bin/omarchy-thunderbolt-authorization-boot-enabled +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 diff --git a/bin/omarchy-thunderbolt-authorization-daemon b/bin/omarchy-thunderbolt-authorization-daemon deleted file mode 100755 index 39f3dd0e..00000000 --- a/bin/omarchy-thunderbolt-authorization-daemon +++ /dev/null @@ -1,16 +0,0 @@ -#!/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 diff --git a/bin/omarchy-thunderbolt-authorization-review b/bin/omarchy-thunderbolt-authorization-review deleted file mode 100755 index 6b555464..00000000 --- a/bin/omarchy-thunderbolt-authorization-review +++ /dev/null @@ -1,10 +0,0 @@ -#!/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" diff --git a/bin/omarchy-thunderbolt-authorization-watch b/bin/omarchy-thunderbolt-authorization-watch deleted file mode 100755 index 20746c75..00000000 --- a/bin/omarchy-thunderbolt-authorization-watch +++ /dev/null @@ -1,9 +0,0 @@ -#!/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 diff --git a/bin/omarchy-usb-authorization-approve b/bin/omarchy-usb-authorization-approve deleted file mode 100755 index 99ecc640..00000000 --- a/bin/omarchy-usb-authorization-approve +++ /dev/null @@ -1,25 +0,0 @@ -#!/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" diff --git a/bin/omarchy-usb-authorization-boot b/bin/omarchy-usb-authorization-boot deleted file mode 100755 index deaa9952..00000000 --- a/bin/omarchy-usb-authorization-boot +++ /dev/null @@ -1,827 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Manage USB authorization from the start of kernel boot -# omarchy:args= -# 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:-$(&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:-$(&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:-$("$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("^(?\\s*(?:cmdline|kernel_cmdline):\\s*)(?.*)$"; "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="$(/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:-$(/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:-$(/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" -) - -# A Mac's boot chain belongs to its boot package, so boot-time denial is not -# managed here. Disabling what is not configured succeeds, so turning USB -# authorization off still works; anything configured by another route is left -# to the owner, since this cannot rebuild a Mac's boot files. -usb_authorization_apple_boot() { - if [[ ${1:-} == "enable" ]]; then - echo "USB authorization from boot is not available on Apple Silicon yet." >&2 - return 2 - fi - if [[ -e $drop_in ]] || grep -Fqx "$snapshot_begin" "$limine_defaults" 2>/dev/null; then - echo "Boot-time USB authorization is configured in $drop_in or $limine_defaults, which this command cannot remove on Apple Silicon." >&2 - echo "Remove it by hand: delete $drop_in and the lines from '$snapshot_begin' to '$snapshot_end' in $limine_defaults, then rebuild the boot files." >&2 - return 1 - fi -} - -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 " >&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 /usr/bin/omarchy-hw-aarch64-apple; then - usb_authorization_apple_boot "$1" - elif usb_authorization_secure_boot_enabled; then - usb_authorization_boot_transaction "$1" - else - usb_authorization_main "$1" - fi - ;; - *) - echo "Usage: omarchy-usb-authorization-boot " >&2 - return 2 - ;; - esac -} - -if [[ ${BASH_SOURCE[0]} == "$0" ]]; then - main "$@" -fi diff --git a/bin/omarchy-usb-authorization-event b/bin/omarchy-usb-authorization-event deleted file mode 100755 index 0d05001f..00000000 --- a/bin/omarchy-usb-authorization-event +++ /dev/null @@ -1,137 +0,0 @@ -#!/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 diff --git a/bin/omarchy-usb-authorization-restore-default b/bin/omarchy-usb-authorization-restore-default deleted file mode 100755 index e5844db8..00000000 --- a/bin/omarchy-usb-authorization-restore-default +++ /dev/null @@ -1,23 +0,0 @@ -#!/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 diff --git a/bin/omarchy-usb-authorization-review b/bin/omarchy-usb-authorization-review deleted file mode 100755 index aab25309..00000000 --- a/bin/omarchy-usb-authorization-review +++ /dev/null @@ -1,142 +0,0 @@ -#!/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" diff --git a/bin/omarchy-usb-authorization-watch b/bin/omarchy-usb-authorization-watch deleted file mode 100755 index 46ce10d3..00000000 --- a/bin/omarchy-usb-authorization-watch +++ /dev/null @@ -1,17 +0,0 @@ -#!/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" diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index c1ced898..6f111f22 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -220,12 +220,8 @@ "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","when":"! omarchy-hw-aarch64-apple","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"}, @@ -344,12 +340,8 @@ "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":"! omarchy-hw-aarch64-apple && 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"}, diff --git a/default/polkit/org.omarchy.thunderbolt.policy b/default/polkit/org.omarchy.thunderbolt.policy deleted file mode 100644 index e849f583..00000000 --- a/default/polkit/org.omarchy.thunderbolt.policy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Approve a connected Thunderbolt accessory - Authentication is required to approve this Thunderbolt accessory - /usr/bin/omarchy-thunderbolt-authorization-approve - - auth_admin - auth_admin - auth_admin - - - diff --git a/default/polkit/org.omarchy.usb.policy b/default/polkit/org.omarchy.usb.policy deleted file mode 100644 index 1f76638e..00000000 --- a/default/polkit/org.omarchy.usb.policy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Trust a connected USB accessory on any port - Authentication is required to trust this USB accessory - /usr/bin/omarchy-usb-authorization-approve - - auth_admin - auth_admin - auth_admin - - - diff --git a/default/systemd/user/omarchy-thunderbolt-authorization.service b/default/systemd/user/omarchy-thunderbolt-authorization.service deleted file mode 100644 index b7c808c5..00000000 --- a/default/systemd/user/omarchy-thunderbolt-authorization.service +++ /dev/null @@ -1,16 +0,0 @@ -[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 diff --git a/default/systemd/user/omarchy-usb-authorization.service b/default/systemd/user/omarchy-usb-authorization.service deleted file mode 100644 index 6a775c8f..00000000 --- a/default/systemd/user/omarchy-usb-authorization.service +++ /dev/null @@ -1,16 +0,0 @@ -[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 diff --git a/docs/thunderbolt-authorization.md b/docs/thunderbolt-authorization.md deleted file mode 100644 index 2bb0852d..00000000 --- a/docs/thunderbolt-authorization.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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. Both loops tick every 2 s without forking. While Linux sees no Thunderbolt controller and nothing is connected or boot-protected, the daemon reconciles Bolt every 10 s instead of every tick, still well inside a snapshot's 20 s lifetime; a controller appearing, a policy change or a boot recovery checkpoint restores the 2 s cadence at once. The user service skips an unchanged, fresh snapshot with no devices, warnings or requests in flight. 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. diff --git a/etc/polkit-1/rules.d/40-omarchy-thunderbolt.rules b/etc/polkit-1/rules.d/40-omarchy-thunderbolt.rules deleted file mode 100644 index 3f37d36a..00000000 --- a/etc/polkit-1/rules.d/40-omarchy-thunderbolt.rules +++ /dev/null @@ -1,17 +0,0 @@ -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. - } - } -}); diff --git a/etc/polkit-1/rules.d/40-omarchy-usb.rules b/etc/polkit-1/rules.d/40-omarchy-usb.rules deleted file mode 100644 index 1469681d..00000000 --- a/etc/polkit-1/rules.d/40-omarchy-usb.rules +++ /dev/null @@ -1,6 +0,0 @@ -polkit.addRule(function(action, subject) { - if (action.id === "org.omarchy.usb.approve" && - subject.active && subject.local && subject.isInGroup("wheel")) { - return polkit.Result.YES; - } -}); diff --git a/etc/systemd/system/bolt.service.d/omarchy-authorization.conf b/etc/systemd/system/bolt.service.d/omarchy-authorization.conf deleted file mode 100644 index 96b2645b..00000000 --- a/etc/systemd/system/bolt.service.d/omarchy-authorization.conf +++ /dev/null @@ -1,3 +0,0 @@ -[Service] -# Runs before Bolt enumerates hardware, including D-Bus/udev activation. -ExecStartPre=/usr/bin/omarchy-thunderbolt-authorization-admin guard diff --git a/etc/systemd/system/omarchy-thunderbolt-authorization.service b/etc/systemd/system/omarchy-thunderbolt-authorization.service deleted file mode 100644 index c2b5e7a6..00000000 --- a/etc/systemd/system/omarchy-thunderbolt-authorization.service +++ /dev/null @@ -1,26 +0,0 @@ -[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 diff --git a/install/config/all.sh b/install/config/all.sh index bc327e6e..da57760e 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -8,7 +8,5 @@ run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh" run_logged "$OMARCHY_INSTALL/config/docker.sh" run_logged "$OMARCHY_INSTALL/config/locale.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" diff --git a/install/config/thunderbolt-authorization.sh b/install/config/thunderbolt-authorization.sh deleted file mode 100644 index e2e4f2c5..00000000 --- a/install/config/thunderbolt-authorization.sh +++ /dev/null @@ -1,10 +0,0 @@ -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 diff --git a/install/config/usb-authorization.sh b/install/config/usb-authorization.sh deleted file mode 100644 index 32605617..00000000 --- a/install/config/usb-authorization.sh +++ /dev/null @@ -1,26 +0,0 @@ -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 diff --git a/install/helpers/thunderbolt-policy.sh b/install/helpers/thunderbolt-policy.sh deleted file mode 100644 index 1141701e..00000000 --- a/install/helpers/thunderbolt-policy.sh +++ /dev/null @@ -1,356 +0,0 @@ -# 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 -# How often the daemon re-checks Bolt while there is no controller and no boot -# protection: well inside the 20 s a published snapshot stays valid. -TB_IDLE_RECHECK=10 -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; } - -# Sleeps without forking on each tick: a pipe open for reading and writing -# never reaches EOF, so a timed read on it returns only on timeout or a signal. -tb_wait() { - [[ -n ${TB_TICK:-} ]] || exec {TB_TICK}<> <(:) - read -r -t "$1" -u "$TB_TICK" _ || true -} - -tb_controller_present() { - local domain - for domain in "$TB_SYSFS"/domain*; do - [[ -e $domain ]] && return 0 - done - 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 policy idle_policy='' idle_since=0 - 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 - policy='' - [[ -r $TB_STATE ]] && IFS= read -r -d '' policy < "$TB_STATE" || true - # With no controller, no device and no boot protection there is nothing to - # restore or reapply, so Bolt is reconciled every TB_IDLE_RECHECK seconds - # instead of every tick. A controller appearing, a policy change or a boot - # recovery checkpoint brings back the full check at once. - if [[ -n $idle_policy && $policy == "$idle_policy" && ! -e ${TB_STATE%/*}/boot-recovery.json ]] && - (( EPOCHSECONDS - idle_since >= 0 && EPOCHSECONDS - idle_since < TB_IDLE_RECHECK )) && - ! tb_controller_present; then - tb_wait 2 - continue - fi - idle_policy='' - ( - 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" - snapshot='' - [[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' snapshot < "$TB_RUNTIME/snapshot.json" || true - if [[ -n $policy ]] && jq -e '.available == true and .domains == [] and .devices == [] and - .warnings == [] and .error == "" and .boot_protection == false' <<< "$snapshot" >/dev/null 2>&1; then - idle_policy=$policy idle_since=$EPOCHSECONDS - fi - tb_wait 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" -} diff --git a/install/helpers/thunderbolt-review.sh b/install/helpers/thunderbolt-review.sh deleted file mode 100644 index c7c093e3..00000000 --- a/install/helpers/thunderbolt-review.sh +++ /dev/null @@ -1,271 +0,0 @@ -# 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 text generation clean='' clean_generation='' clean_time=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 - clean='' - tb_wait 2 - continue - fi - # The last scan's snapshot, unchanged, still fresh and from the same daemon, - # with no device, warning or request in flight, has nothing to deliver. - text='' generation='' - [[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' text < "$TB_RUNTIME/snapshot.json" || true - [[ -r $TB_RUNTIME/generation ]] && IFS= read -r generation < "$TB_RUNTIME/generation" || true - if [[ -n $clean && ${text%$'\n'} == "$clean" && $generation == "$clean_generation" ]] && - (( EPOCHSECONDS - clean_time >= 0 && EPOCHSECONDS - clean_time < 20 )) && - ! compgen -G "$TB_REQUESTS/request-*.json" >/dev/null; then - tb_wait 2 - continue - fi - clean='' - 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='' - if jq -e '.devices == []' <<< "$snapshot" >/dev/null; then - IFS=' ' read -r clean_generation clean_time <<< "$(jq -r '"\(.generation) \(.time)"' <<< "$snapshot")" - [[ $clean_time =~ ^[0-9]+$ ]] && clean=$snapshot - fi - fi - else - failures=$((failures + 1)) - if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then - warned=unavailable - fi - fi - tb_wait 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' -} diff --git a/install/helpers/thunderbolt-setup.sh b/install/helpers/thunderbolt-setup.sh deleted file mode 100644 index 70ffc48e..00000000 --- a/install/helpers/thunderbolt-setup.sh +++ /dev/null @@ -1,398 +0,0 @@ -# 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 -} diff --git a/install/helpers/usb-authorization-policy.sh b/install/helpers/usb-authorization-policy.sh deleted file mode 100644 index a0d1da1e..00000000 --- a/install/helpers/usb-authorization-policy.sh +++ /dev/null @@ -1,95 +0,0 @@ -# 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" -} diff --git a/install/helpers/usb-authorization.sh b/install/helpers/usb-authorization.sh deleted file mode 100644 index a4b0606d..00000000 --- a/install/helpers/usb-authorization.sh +++ /dev/null @@ -1,146 +0,0 @@ -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:-} 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 -} diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 31cbff7d..38029cfd 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -146,7 +146,6 @@ udiskie ufw ufw-docker unzip -usbguard usage uwsm vi diff --git a/install/user/first-run/enable-user-units.sh b/install/user/first-run/enable-user-units.sh index 07534d4f..0f732622 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -25,9 +25,7 @@ for unit in \ omarchy-sleep-lock.service \ omarchy-migrate-notify.service \ omarchy-fcitx5.service \ - omarchy-crash-watch.service \ - omarchy-usb-authorization.service \ - omarchy-thunderbolt-authorization.service; do + omarchy-crash-watch.service; do systemctl --user enable --now "$unit" || failed=1 done diff --git a/manual/48-security.md b/manual/48-security.md index 36e76830..31e598dd 100644 --- a/manual/48-security.md +++ b/manual/48-security.md @@ -6,30 +6,7 @@ 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. *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. +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. ## Changing your passwords @@ -56,23 +33,3 @@ 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. diff --git a/migrations/1789433473.sh b/migrations/1789433473.sh deleted file mode 100644 index 47a59c55..00000000 --- a/migrations/1789433473.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "Enable approval for newly connected USB devices" - -omarchy-setup-security-usb-authorization --yes diff --git a/migrations/1790344599.sh b/migrations/1790344599.sh deleted file mode 100644 index ee48034d..00000000 --- a/migrations/1790344599.sh +++ /dev/null @@ -1,2 +0,0 @@ -echo "Require approval for new Thunderbolt accessories" -omarchy-setup-security-thunderbolt-authorization --quiet diff --git a/migrations/1790608617.sh b/migrations/1790608617.sh deleted file mode 100644 index 7c1aa722..00000000 --- a/migrations/1790608617.sh +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/migrations/1791769486.sh b/migrations/1791769486.sh new file mode 100644 index 00000000..79c45a47 --- /dev/null +++ b/migrations/1791769486.sh @@ -0,0 +1,212 @@ +echo "Remove approval for new USB and Thunderbolt devices" + +# Approval for new accessories (#11874) shipped briefly to edge and dev. The +# package update has already removed its commands, units and Polkit rules; this +# undoes what they left on the machine so devices connect as they did before. + +usb_state=$HOME/.local/state/omarchy/usb-authorization +tb_dir=/var/lib/omarchy/thunderbolt-authorization +usb_boot_drop_in=/etc/limine-entry-tool.d/usb-authorization.conf +usb_boot_pending=/var/lib/omarchy/usb-authorization-boot.removing +usb_boot_parameter=usbcore.authorized_default=0 +usbguard_shipped=1791573373 # when usbguard joined the default packages +# The access every Omarchy path granted with `usbguard add-user`. +usbguard_grant=$'Devices=list,modify,listen\nPolicy=list\nExceptions=listen' + +# The approval watchers. Their unit files are gone, so systemctl disable can't +# find them; drop the enablement links directly. +for unit in omarchy-usb-authorization.service omarchy-thunderbolt-authorization.service; do + systemctl --user stop "$unit" 2>/dev/null || true + rm -f "$HOME"/.config/systemd/user/*.wants/"$unit" +done +rm -f "$HOME/.config/systemd/user/omarchy-usb-authorization.service" +systemctl --user daemon-reload + +bolt_list() { + sudo busctl --system --json=short call org.freedesktop.bolt /org/freedesktop/bolt org.freedesktop.bolt1.Manager "$1" | jq -r '.data[0][]' +} + +# A device or controller missing from Bolt's inventory has nothing to restore. +# Any other failed read stops the migration, so it is retried. +bolt_has() { + grep -qxF "/org/freedesktop/bolt/$1/${2//-/_}" <<<"$bolt_known" +} + +bolt_get() { + sudo busctl --system --json=short get-property org.freedesktop.bolt "/org/freedesktop/bolt/$1/${2//-/_}" "org.freedesktop.bolt1.$3" "$4" | jq -c .data +} + +if [[ -d $tb_dir || -e /etc/omarchy/thunderbolt-authorization.enabled || -e /etc/omarchy/thunderbolt-authorization.pending ]]; then + sudo systemctl stop omarchy-thunderbolt-authorization.service 2>/dev/null || true + sudo rm -f /etc/systemd/system/multi-user.target.wants/omarchy-thunderbolt-authorization.service \ + /etc/omarchy/thunderbolt-authorization.enabled /etc/omarchy/thunderbolt-authorization.pending + sudo systemctl daemon-reload + + state=$(sudo cat "$tb_dir/policy.json" 2>/dev/null || echo '{}') + recovery=$(sudo cat "$tb_dir/boot-recovery.json" 2>/dev/null || echo '{}') + + # Thunderbolt Boot replaced device policies and emptied the firmware boot + # allowlists. The originals were saved when it was turned on, or only in the + # recovery journal if turning it on was interrupted. + firmware=$(jq -c --argjson recovery "$recovery" \ + '.boot_original // (if $recovery.policies then $recovery | {policies, acls} else {} end)' <<<"$state") + + bolt_devices=$(bolt_list ListDevices) + bolt_domains=$(bolt_list ListDomains) + bolt_known=$bolt_devices$'\n'$bolt_domains + + while read -r uid policy; do + if bolt_has devices "$uid"; then + current=$(bolt_get devices "$uid" Device Policy) + if [[ $current != "\"$policy\"" ]]; then + sudo boltctl config device.policy "$uid" "$policy" + fi + fi + done < <(jq -r '.policies // {} | to_entries[] | "\(.key) \(.value)"' <<<"$firmware") + + # Bolt rejects a BootACL write that changes nothing, so compare first. + unrestored=0 + while read -r uid; do + acl=$(jq -c --arg uid "$uid" '.acls[$uid]' <<<"$firmware") + if ! bolt_has domains "$uid"; then + echo "Thunderbolt controller $uid is not present; its firmware boot allowlist was not restored." + unrestored=1 + continue + fi + current=$(bolt_get domains "$uid" Domain BootACL) + if [[ $current != "$acl" ]]; then + mapfile -t entries < <(jq -r '.[]' <<<"$acl") + sudo busctl --system set-property org.freedesktop.bolt "/org/freedesktop/bolt/domains/${uid//-/_}" \ + org.freedesktop.bolt1.Domain BootACL as "${#entries[@]}" "${entries[@]}" + fi + done < <(jq -r '.acls // {} | keys[]' <<<"$firmware") + + # Accessories approved here were stored with Bolt's manual policy, which + # Bolt won't authorize on its own. Hand back the ones Omarchy stored, as the + # removal command did, never a manual policy somebody else set. + while read -r uid store_time; do + if bolt_has devices "$uid"; then + current=$(bolt_get devices "$uid" Device Policy) + stored=$(bolt_get devices "$uid" Device StoreTime) + if [[ $current == '"manual"' && $stored == "$store_time" ]]; then + sudo boltctl config device.policy "$uid" auto + fi + fi + done < <(jq -r '(.trusted // {}) as $trusted | .enrolled // {} | to_entries[] | select($trusted[.key]) | "\(.key) \(.value.StoreTime)"' <<<"$state") + + # A first setup that failed and then failed to undo itself can lose + # policy.json after switching Bolt off; Bolt's original config is then only + # in the setup journal. A deferred install that never finished setup left + # Bolt's mode untouched and has neither. + setup=$(sudo cat "$tb_dir/setup-recovery.json" 2>/dev/null || echo '{}') + config=$(jq -c '[.files // {} | to_entries[] | select(.key | endswith("/boltd.conf")) | .value]' <<<"$setup") + mode=$(jq -r '.original_authmode // empty' <<<"$state") + if [[ -z $mode && $config != "[]" ]]; then + mode=$(jq -r '.[0].data // ""' <<<"$config" | base64 -d | awk ' + /^[[:space:]]*\[/ { section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/) } + section && /^[[:space:]]*AuthMode[[:space:]]*=/ { sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); mode = $0 } + END { print mode == "" ? "enabled" : mode } + ') + fi + if [[ -n $mode ]]; then + sudo boltctl config global.auth-mode "$mode" + fi + + if (( unrestored )) && [[ -d $tb_dir ]]; then + sudo mv -T "$tb_dir" "$tb_dir.unrestored" + echo "The saved Thunderbolt settings are kept in $tb_dir.unrestored." + else + sudo rm -rf "$tb_dir" + fi + sudo rm -rf /run/omarchy-thunderbolt-authorization +fi + +# limine-mkinitcpio reports success even when a kernel image fails to build, so +# check every current kernel entry. Limine's command line wins, except under +# Secure Boot, where the signed image's embedded one does. +usb_boot_cleared() { + local entries key value esp secure_boot=0 found=0 + local efivar=/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c + + if [[ -r $efivar && $(od -An -t u1 -j 4 -N 1 "$efivar") == *1 ]]; then + secure_boot=1 + fi + esp=$(sed -n 's/^ESP_PATH=["'\'']\?\([^"'\'']*\).*/\1/p' /etc/default/limine | tail -1) + esp=${esp:-/boot} + entries=$(sudo awk ' + /^ \/\/[^\/]/ { entry = 1; next } + /^ *\// { entry = 0 } + entry && /^ (path|cmdline):/ { key = $1; sub(/^ [a-z]+:[[:space:]]*/, ""); print key "\t" $0 } + ' "$esp/limine.conf") || return 1 + + while IFS=$'\t' read -r key value; do + if [[ $key == "cmdline:" ]]; then + [[ " $value " != *" $usb_boot_parameter "* ]] || return 1 + elif [[ $key == "path:" ]]; then + found=1 + if (( secure_boot )); then + value=${value#'boot():'} + value=$esp${value%%#*} + sudo test -r "$value" || return 1 + if sudo grep -qaF "$usb_boot_parameter" "$value"; then + return 1 + fi + fi + fi + done <<<"$entries" + (( found )) +} + +# The opt-in USB at Boot setting: the parameter on the kernel command line, for +# new boot entries and new snapshots. Keep a marker until the rebuilt entries +# are verified, so an interrupted rebuild is retried rather than skipped. +if [[ -e $usb_boot_drop_in || -e $usb_boot_pending ]] || grep -Fqx '# Omarchy USB authorization begin' /etc/default/limine 2>/dev/null; then + sudo touch "$usb_boot_pending" + sudo rm -f "$usb_boot_drop_in" + sudo sed -i '/^# Omarchy USB authorization begin$/,/^# Omarchy USB authorization end$/d' /etc/default/limine + sudo limine-mkinitcpio + if ! usb_boot_cleared; then + echo "A boot entry still has $usb_boot_parameter, so USBGuard stays until it is rebuilt. Run omarchy-migrate to retry." + exit 1 + fi + sudo rm -f "$usb_boot_pending" +fi + +# Leave a USBGuard the user set up before Omarchy shipped it. pacman resets the +# install date on every upgrade, so ask its log when usbguard first arrived. +usbguard_from_omarchy() { + local first + + if [[ -f $usb_state/policy-generated-by-omarchy ]]; then + return 0 + fi + first=$(grep -m1 -oP '^\[\K[^]]+(?=\] \[ALPM\] installed usbguard )' /var/log/pacman.log) || return 1 + (( $(date -d "$first" +%s) >= usbguard_shipped )) || return 1 + [[ $(sudo cat "/etc/usbguard/IPCAccessControl.d/$USER" 2>/dev/null) == "$usbguard_grant" ]] +} + +if omarchy-pkg-present usbguard && usbguard_from_omarchy; then + sudo systemctl disable --now usbguard.service + # USBGuard leaves the root hubs denying new devices when it exits, until + # the next boot. Restore the kernel default and authorize what it blocked. + shopt -s nullglob + echo 1 | sudo tee /sys/bus/usb/devices/usb*/authorized_default /sys/bus/usb/devices/*/authorized >/dev/null + shopt -u nullglob + omarchy-pkg-drop usbguard + + # The approval access granted to each user outlives the package. + while IFS= read -r -d '' grant; do + if [[ $(sudo cat "$grant") == "$usbguard_grant" ]]; then + sudo rm -f "$grant" + fi + done < <(sudo find /etc/usbguard/IPCAccessControl.d -maxdepth 1 -type f -print0 2>/dev/null) + sudo rmdir /etc/usbguard/IPCAccessControl.d /etc/usbguard 2>/dev/null || true +fi + +for action in /usr/share/polkit-1/actions/org.omarchy.usb.policy /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy; do + if [[ -e $action ]] && ! pacman -Qo "$action" &>/dev/null; then + sudo rm -f "$action" + fi +done + +rm -rf "$usb_state" diff --git a/test/shell.d/device-authorization-removal-migration-test.sh b/test/shell.d/device-authorization-removal-migration-test.sh new file mode 100755 index 00000000..4c211ed7 --- /dev/null +++ b/test/shell.d/device-authorization-removal-migration-test.sh @@ -0,0 +1,341 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +shipped_migration="$ROOT/migrations/1791769486.sh" +[[ -f $shipped_migration ]] || fail "the device authorization removal migration exists" + +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +root="$test_dir/root" +home="$test_dir/home" +migration="$test_dir/migration.sh" +export CALLS="$test_dir/calls" FAKE_ROOT="$root" + +# Run a scratch copy whose machine paths all live under the fake root. +sed -e "s#/etc/#$root/etc/#g" -e "s#/var/#$root/var/#g" -e "s#/run/#$root/run/#g" \ + -e "s#/sys/#$root/sys/#g" -e "s#:-/boot}#:-$root/boot}#" -e "s#/usr/share/polkit-1/#$root/usr/share/polkit-1/#g" \ + "$shipped_migration" >"$migration" + +mkdir -p "$test_dir/bin" + +cat >"$test_dir/bin/sudo" <<'STUB' +#!/bin/bash +printf 'sudo %s\n' "$*" >>"$CALLS" +exec "$@" +STUB + +cat >"$test_dir/bin/systemctl" <<'STUB' +#!/bin/bash +printf 'systemctl %s\n' "$*" >>"$CALLS" +STUB + +# Bolt: devices// and domains//BootACL hold JSON values. +cat >"$test_dir/bin/busctl" <<'STUB' +#!/bin/bash +args=() +for arg in "$@"; do [[ $arg == --* ]] || args+=("$arg"); done +object=${args[2]#/org/freedesktop/bolt/} +uid=${object#*/} +file="$FAKE_ROOT/bolt/${object%%/*}/${uid//_/-}/${args[4]}" +case ${args[0]} in + call) + kind=devices + [[ ${args[4]} == "ListDevices" ]] || kind=domains + for dir in "$FAKE_ROOT/bolt/$kind"/*/; do + dir=${dir%/} + uid=${dir##*/} + printf '/org/freedesktop/bolt/%s/%s\n' "$kind" "${uid//-/_}" + done | jq -Rnc '{type:"ao",data:[[inputs]]}' + ;; + get-property) + [[ -f $file && -z ${BOLT_READ_FAILS:-} ]] || exit 1 + printf '{"type":"x","data":%s}\n' "$(<"$file")" + ;; + set-property) + [[ -f $file ]] || exit 1 + value=$(printf '%s\n' "${args[@]:7}" | jq -Rnc '[inputs]') + (( args[6] > 0 )) || value='[]' + # Like Bolt, refuse a BootACL write that changes nothing. + [[ $value != "$(jq -c . "$file")" ]] || exit 1 + printf '%s\n' "$value" >"$file" + ;; +esac +STUB + +cat >"$test_dir/bin/boltctl" <<'STUB' +#!/bin/bash +printf 'boltctl %s\n' "$*" >>"$CALLS" +case $2 in + device.policy) printf '"%s"\n' "$4" >"$FAKE_ROOT/bolt/devices/$3/Policy" ;; + global.auth-mode) printf '%s\n' "$3" >"$FAKE_ROOT/bolt/authmode" ;; +esac +STUB + +# Rebuilds the current entries from the drop-in. Old snapshot entries keep the +# parameter. MKINITCPIO=fail fails; MKINITCPIO=stale "succeeds" without +# rebuilding, like a kernel image that failed inside the Limine hook. +cat >"$test_dir/bin/limine-mkinitcpio" <<'STUB' +#!/bin/bash +printf 'limine-mkinitcpio\n' >>"$CALLS" +[[ ${MKINITCPIO:-} != "fail" ]] || exit 1 +[[ ${MKINITCPIO:-} != "stale" ]] || exit 0 +parameter="" +[[ ! -e $FAKE_ROOT/etc/limine-entry-tool.d/usb-authorization.conf ]] || parameter=" usbcore.authorized_default=0" +esp=$(sed -n 's/^ESP_PATH="\(.*\)"$/\1/p' "$FAKE_ROOT/etc/default/limine") +esp=${esp:-$FAKE_ROOT/boot} +mkdir -p "$esp/EFI/Linux" +printf 'MZ...cmdline root=x%s...initrd' "$parameter" >"$esp/EFI/Linux/omarchy_linux.efi" +cat >"$esp/limine.conf" <"$test_dir/bin/omarchy-pkg-present" <<'STUB' +#!/bin/bash +[[ -e $FAKE_ROOT/packages/$1 ]] +STUB + +cat >"$test_dir/bin/omarchy-pkg-drop" <<'STUB' +#!/bin/bash +printf 'omarchy-pkg-drop %s\n' "$*" >>"$CALLS" +rm -f "$FAKE_ROOT/packages/$1" "$FAKE_ROOT/etc/usbguard/rules.conf" +STUB + +cat >"$test_dir/bin/pacman" <<'STUB' +#!/bin/bash +exit 1 +STUB + +chmod +x "$test_dir/bin/"* + +grant=$'Devices=list,modify,listen\nPolicy=list\nExceptions=listen' +dock=b8f58780-002a-50c4-ffff-ffffffffffff +other=aaaaaaaa-0000-0000-0000-000000000001 +domain=895a5509-000a-3075-ffff-ffffffffffff +gone_domain=895a5509-010a-3075-ffff-ffffffffffff + +# A machine that never had the feature, with Bolt in its default mode. +reset_machine() { + rm -rf "$root" "$home" + mkdir -p "$root"/{etc/default,var/log,var/lib/omarchy,run,packages,usr/share/polkit-1/actions} \ + "$root/sys/bus/usb/devices"/{usb1,usb2,1-1} "$root/bolt/devices"/{$dock,$other} "$root/bolt/domains/$domain" \ + "$home/.config/systemd/user/graphical-session.target.wants" "$home/.local/state/omarchy" + echo 'KERNEL_CMDLINE[default]+="quiet"' >"$root/etc/default/limine" + echo enabled >"$root/bolt/authmode" + echo '"auto"' >"$root/bolt/devices/$dock/Policy" + echo 1791668776 >"$root/bolt/devices/$dock/StoreTime" + echo '"manual"' >"$root/bolt/devices/$other/Policy" + echo 1700000000 >"$root/bolt/devices/$other/StoreTime" + echo '["",""]' >"$root/bolt/domains/$domain/BootACL" + echo 1 | tee "$root"/sys/bus/usb/devices/{usb1,usb2}/authorized_default "$root"/sys/bus/usb/devices/{usb1,usb2,1-1}/authorized >/dev/null + echo '[2026-01-02T03:04:05-0500] [ALPM] installed jq (1.8)' >"$root/var/log/pacman.log" + MKINITCPIO=ok "$test_dir/bin/limine-mkinitcpio" +} + +# What #11874's default setup left behind on the author's machine. +enable_feature() { + touch "$root/packages/usbguard" + echo '[2026-10-10T17:44:55-0400] [ALPM] installed usbguard (1.1.4-11)' >>"$root/var/log/pacman.log" + mkdir -p "$root/etc/usbguard/IPCAccessControl.d" "$root/etc/omarchy" "$root/var/lib/omarchy/thunderbolt-authorization" \ + "$root/etc/systemd/system/multi-user.target.wants" "$home/.local/state/omarchy/usb-authorization/requests" + echo 'allow id 1d6b:0002' >"$root/etc/usbguard/rules.conf" + printf '%s\n' "$grant" >"$root/etc/usbguard/IPCAccessControl.d/tester" + echo 0 | tee "$root"/sys/bus/usb/devices/{usb1,usb2}/authorized_default >/dev/null + echo 0 >"$root/sys/bus/usb/devices/1-1/authorized" + touch "$home/.local/state/omarchy/usb-authorization/policy-generated-by-omarchy" + touch "$root/usr/share/polkit-1/actions"/org.omarchy.{usb,thunderbolt}.policy + touch "$home/.config/systemd/user/omarchy-usb-authorization.service" + ln -s ../omarchy-usb-authorization.service "$home/.config/systemd/user/graphical-session.target.wants/omarchy-usb-authorization.service" + ln -s /usr/lib/systemd/user/omarchy-thunderbolt-authorization.service "$home/.config/systemd/user/graphical-session.target.wants/omarchy-thunderbolt-authorization.service" + ln -s /etc/systemd/system/omarchy-thunderbolt-authorization.service "$root/etc/systemd/system/multi-user.target.wants/omarchy-thunderbolt-authorization.service" + touch "$root/etc/omarchy/thunderbolt-authorization.enabled" + echo disabled >"$root/bolt/authmode" + echo '"manual"' >"$root/bolt/devices/$dock/Policy" + cat >"$root/var/lib/omarchy/thunderbolt-authorization/policy.json" <"$root/etc/limine-entry-tool.d/usb-authorization.conf" + printf '%s\n' '# Omarchy USB authorization begin' 'SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0' \ + '# Omarchy USB authorization end' >>"$root/etc/default/limine" + MKINITCPIO=ok "$test_dir/bin/limine-mkinitcpio" +} + +run_migration() { + : >"$CALLS" + HOME="$home" USER=tester PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >"$test_dir/output" 2>&1 +} + +value() { tr -d '\n' <"$root/bolt/$1"; } + +reset_machine +run_migration || fail "migration fails on a machine that never had the feature" "$(cat "$test_dir/output")" +! grep -q '^sudo' "$CALLS" || fail "migration asks for sudo on a machine that never had the feature" "$(cat "$CALLS")" +pass "a machine that never had the feature is left alone without sudo" + +reset_machine +touch "$root/packages/usbguard" +printf '%s\n' '[2025-03-01T10:00:00-0500] [ALPM] installed usbguard (1.1.3-1)' \ + '[2026-10-10T17:44:55-0400] [ALPM] upgraded usbguard (1.1.3-1 -> 1.1.4-11)' >>"$root/var/log/pacman.log" +run_migration || fail "migration fails beside the user's own USBGuard" "$(cat "$test_dir/output")" +[[ -e $root/packages/usbguard ]] || fail "migration removes a USBGuard installed before Omarchy shipped it" +! grep -q '^sudo' "$CALLS" || fail "migration asks for sudo for a USBGuard it does not own" "$(cat "$CALLS")" +pass "a USBGuard installed before Omarchy shipped it is kept, even after an upgrade" + +reset_machine +touch "$root/packages/usbguard" +echo '[2026-10-10T09:00:00-0400] [ALPM] installed usbguard (1.1.4-11)' >>"$root/var/log/pacman.log" +run_migration || fail "migration fails beside a USBGuard without Omarchy's grant" "$(cat "$test_dir/output")" +[[ -e $root/packages/usbguard ]] || fail "migration removes a USBGuard without Omarchy's ownership evidence" +pass "a USBGuard without Omarchy's marker or grant is kept" + +reset_machine +enable_feature +run_migration || fail "migration fails on a machine with the feature" "$(cat "$test_dir/output")" +[[ ! -e $root/packages/usbguard ]] || fail "migration keeps the USBGuard Omarchy installed" +[[ $(cat "$root"/sys/bus/usb/devices/*/authorized_default "$root"/sys/bus/usb/devices/*/authorized | sort -u) == "1" ]] || + fail "migration leaves USB devices denied after USBGuard exits" +[[ ! -e $root/etc/usbguard ]] || fail "migration leaves Omarchy's USBGuard grants behind" "$(find "$root/etc/usbguard")" +[[ $(value authmode) == "enabled" && $(value devices/$dock/Policy) == '"auto"' ]] || + fail "migration does not hand Thunderbolt back to Bolt" "$(value authmode) $(value devices/$dock/Policy)" +[[ $(value devices/$other/Policy) == '"manual"' ]] || fail "migration changes a manual policy Omarchy did not set" +for path in "$root/var/lib/omarchy/thunderbolt-authorization" "$root/etc/omarchy/thunderbolt-authorization.enabled" \ + "$root/etc/systemd/system/multi-user.target.wants/omarchy-thunderbolt-authorization.service" \ + "$root/usr/share/polkit-1/actions/org.omarchy.usb.policy" "$root/usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy" \ + "$home/.local/state/omarchy/usb-authorization" "$home/.config/systemd/user/omarchy-usb-authorization.service" \ + "$home/.config/systemd/user/graphical-session.target.wants/omarchy-usb-authorization.service" \ + "$home/.config/systemd/user/graphical-session.target.wants/omarchy-thunderbolt-authorization.service"; do + [[ ! -e $path && ! -L $path ]] || fail "migration leaves $path behind" +done +pass "the default feature is removed and devices connect as before" + +run_migration || fail "a second run fails" "$(cat "$test_dir/output")" +! grep -q '^sudo' "$CALLS" || fail "a second run asks for sudo" "$(cat "$CALLS")" +pass "a second run is a no-op" + +# A deferred install that never finished setup left only the pending marker, +# without changing Bolt's mode; an administrator's own setting stays. +reset_machine +mkdir -p "$root/etc/omarchy" +touch "$root/etc/omarchy/thunderbolt-authorization.pending" +echo disabled >"$root/bolt/authmode" +run_migration || fail "migration fails with only the pending marker" "$(cat "$test_dir/output")" +[[ $(value authmode) == "disabled" ]] || fail "migration enables Bolt authorization it never changed" +[[ ! -e $root/etc/omarchy/thunderbolt-authorization.pending ]] || fail "migration leaves the pending marker" +pass "Bolt's mode is untouched when Omarchy never saved one" + +# A first setup failed and its undo deleted policy.json before restoring +# boltd.conf: Bolt's original config survives only in the setup journal. +for original in "$(printf '[config]\nAuthMode = enabled\n' | base64 -w0)" ""; do + reset_machine + mkdir -p "$root/etc/omarchy" "$root/var/lib/omarchy/thunderbolt-authorization" + touch "$root/etc/omarchy/thunderbolt-authorization.enabled" + echo disabled >"$root/bolt/authmode" + if [[ -n $original ]]; then + saved="{\"data\":\"$original\",\"mode\":\"600\"}" + else + saved=null + fi + echo "{\"files\":{\"/var/lib/boltd/boltd.conf\":$saved},\"units\":{}}" \ + >"$root/var/lib/omarchy/thunderbolt-authorization/setup-recovery.json" + run_migration || fail "migration fails with only the setup journal" "$(cat "$test_dir/output")" + [[ $(value authmode) == "enabled" ]] || fail "migration leaves Bolt off when its original config is only in the setup journal" +done +pass "Bolt's original mode is recovered from the setup journal" + +# Turning on Thunderbolt Boot was interrupted: the originals are only in the +# recovery journal. One controller's list already matches; another is gone. +reset_machine +enable_feature +echo '"manual"' >"$root/bolt/devices/$other/Policy" +echo '["",""]' >"$root/bolt/domains/$domain/BootACL" +cat >"$root/var/lib/omarchy/thunderbolt-authorization/boot-recovery.json" <"$test_dir/policy.json" +mv "$test_dir/policy.json" "$root/var/lib/omarchy/thunderbolt-authorization/policy.json" +run_migration || fail "migration fails when a list already matches or a controller is gone" "$(cat "$test_dir/output")" +[[ -f $root/var/lib/omarchy/thunderbolt-authorization.unrestored/policy.json ]] || + fail "migration discards settings it could not restore" +grep -q "$gone_domain is not present" "$test_dir/output" || fail "migration does not say which controller was skipped" +[[ $(value devices/$dock/Policy) == '"auto"' ]] || fail "migration stops before handing back the dock" +pass "unrestorable firmware settings are kept aside, and matching lists are not rewritten" + +# A Bolt read that fails, unlike a record Bolt no longer has, must leave the +# saved settings in place and the migration pending. +reset_machine +enable_feature +BOLT_READ_FAILS=1 run_migration && fail "migration succeeds although Bolt could not be read" +[[ -f $root/var/lib/omarchy/thunderbolt-authorization/policy.json ]] || fail "a failed Bolt read discards the saved settings" +run_migration || fail "migration does not finish once Bolt can be read" "$(cat "$test_dir/output")" +[[ $(value devices/$dock/Policy) == '"auto"' ]] || fail "the retry does not hand back the dock" +pass "a failed Bolt read keeps the saved settings and retries" + +# USB at Boot: a failed rebuild, then one that silently leaves the parameter, +# must both keep USBGuard and retry, even though the settings are already gone. +reset_machine +enable_feature +enable_usb_at_boot +MKINITCPIO=fail run_migration && fail "migration succeeds although the boot rebuild failed" +[[ -e $root/packages/usbguard && -e $root/var/lib/omarchy/usb-authorization-boot.removing ]] || + fail "a failed rebuild removes USBGuard or forgets to retry" +MKINITCPIO=stale run_migration && fail "migration trusts a rebuild that left the parameter" "$(cat "$test_dir/output")" +[[ -e $root/packages/usbguard ]] || fail "migration removes USBGuard while a boot entry still denies USB" +grep -q 'limine-mkinitcpio' "$CALLS" || fail "a retry skips the rebuild once the settings are gone" +MKINITCPIO=ok run_migration || fail "migration fails once the rebuild succeeds" "$(cat "$test_dir/output")" +[[ ! -e $root/packages/usbguard && ! -e $root/var/lib/omarchy/usb-authorization-boot.removing ]] || + fail "migration does not finish after a successful rebuild" +grep -q 'usbcore' "$root/etc/default/limine" && fail "migration leaves the snapshot parameter in /etc/default/limine" +pass "USB at Boot is removed only once the rebuilt entries are verified" + +# A boot partition mounted somewhere other than /boot, named by ESP_PATH. +reset_machine +rm -rf "$root/boot" +echo "ESP_PATH=\"$root/efi\"" >>"$root/etc/default/limine" +enable_feature +enable_usb_at_boot +MKINITCPIO=stale run_migration && fail "migration trusts a stale rebuild on a boot partition outside /boot" +MKINITCPIO=ok run_migration || fail "migration does not check the boot partition named by ESP_PATH" "$(cat "$test_dir/output")" +[[ ! -e $root/packages/usbguard ]] || fail "migration keeps USBGuard after a verified rebuild outside /boot" +pass "boot entries are verified on the boot partition named by ESP_PATH" + +# Under Secure Boot the signed image's own command line is what boots. +reset_machine +enable_feature +enable_usb_at_boot +mkdir -p "$root/sys/firmware/efi/efivars" +printf '\x06\x00\x00\x00\x01' >"$root/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c" +sed -i 's/ usbcore.authorized_default=0$//' "$root/boot/limine.conf" +MKINITCPIO=stale run_migration && fail "migration trusts Limine's command line over the signed image's" +MKINITCPIO=ok run_migration || fail "migration fails under Secure Boot after a clean rebuild" "$(cat "$test_dir/output")" +pass "Secure Boot images are checked for the parameter too" diff --git a/test/shell.d/fixtures/thunderbolt/bolt-exercise.sh b/test/shell.d/fixtures/thunderbolt/bolt-exercise.sh deleted file mode 100644 index 9f853698..00000000 --- a/test/shell.d/fixtures/thunderbolt/bolt-exercise.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/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 diff --git a/test/shell.d/fixtures/thunderbolt/bolt-integration.py b/test/shell.d/fixtures/thunderbolt/bolt-integration.py deleted file mode 100644 index 77a111bc..00000000 --- a/test/shell.d/fixtures/thunderbolt/bolt-integration.py +++ /dev/null @@ -1,185 +0,0 @@ -"""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()) diff --git a/test/shell.d/fixtures/thunderbolt/bolt-removal.sh b/test/shell.d/fixtures/thunderbolt/bolt-removal.sh deleted file mode 100644 index e4db4975..00000000 --- a/test/shell.d/fixtures/thunderbolt/bolt-removal.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/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 diff --git a/test/shell.d/fixtures/thunderbolt/policy-test.sh b/test/shell.d/fixtures/thunderbolt/policy-test.sh deleted file mode 100644 index fce351b5..00000000 --- a/test/shell.d/fixtures/thunderbolt/policy-test.sh +++ /dev/null @@ -1,728 +0,0 @@ -#!/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 - tb_wait() { 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; } - tb_wait() { 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_wait_times_out_without_input() { - local start=$EPOCHSECONDS - TB_TICK='' tb_wait 1 - (( EPOCHSECONDS - start >= 1 && EPOCHSECONDS - start < 10 )) -} -test_daemon_rechecks_controller_free_bolt_less_often() { - touch "$TB_MARKER"; TB_IDLE_RECHECK=600 - local tick=0 extra='{}' - expect() { [[ $(wc -l < "$T/reconciles") == "$1" ]] || fail "tick $tick: expected $1 reconciles" "$(wc -l < "$T/reconciles")"; } - tb_reconcile() { - echo >> "$T/reconciles" - jq -cn --argjson time "$EPOCHSECONDS" --argjson extra "$extra" \ - '{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false} + $extra' - } - tb_wait() { - tick=$((tick + 1)) - case $tick in - 1) expect 1 ;; - 2) expect 1; mkdir "$TB_SYSFS/domain0" ;; # idle: no reconcile - 3) expect 2; rmdir "$TB_SYSFS/domain0" ;; # a controller appeared - 4) expect 2; edit_state '.trusted.x={}' ;; - 5) expect 3; touch "${TB_STATE%/*}/boot-recovery.json" ;; # the policy changed - 6) expect 4; rm "${TB_STATE%/*}/boot-recovery.json" # a recovery checkpoint - extra='{"devices":[{"Uid":"device-1"}]}'; edit_state '.trusted.y={}' ;; - 7) expect 5 ;; # devices from here on - 8) expect 6; rm "$TB_MARKER" ;; # so every tick reconciles - esac - } - tb_daemon - [[ $tick == 8 ]] -} -test_daemon_rechecks_after_the_idle_interval() { - touch "$TB_MARKER"; TB_IDLE_RECHECK=2 - local tick=0 - tb_reconcile() { - echo >> "$T/reconciles" - jq -cn --argjson time "$EPOCHSECONDS" '{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false}' - } - tb_wait() { - tick=$((tick + 1)) - case $tick in - 1) sleep 2.1 ;; - 2) [[ $(wc -l < "$T/reconciles") == 2 ]] || fail "the idle check did not expire"; rm "$TB_MARKER" ;; - esac - } - tb_daemon -} -test_daemon_failed_reconcile_keeps_every_tick() { - touch "$TB_MARKER"; TB_IDLE_RECHECK=600 - local tick=0 - tb_reconcile() { echo >> "$T/reconciles"; tb_fail 'Bolt unavailable'; } - tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || rm "$TB_MARKER"; } - tb_daemon 2>/dev/null - [[ $(wc -l < "$T/reconciles") == 3 ]] - check '.available==false' "$TB_RUNTIME/snapshot.json" -} -test_watch_skips_only_unchanged_clean_snapshots() { - local tick=0 start=$EPOCHSECONDS - publish() { jq -cnS --argjson time "$1" --arg generation generation-a --argjson devices "${2:-[]}" \ - '{available:true,time:$time,generation:$generation,devices:$devices,warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json"; } - expect() { [[ $(wc -l < "$T/scans") == "$1" ]] || fail "tick $tick: expected $1 scans" "$(wc -l < "$T/scans")"; } - tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; } - publish "$start" - tb_wait() { - tick=$((tick + 1)) - case $tick in - 1) expect 1 ;; - 2) expect 1; touch "$TB_REQUESTS/request-x.json" ;; # unchanged: skipped - 3) expect 2; rm "$TB_REQUESTS/request-x.json" ;; # a request in flight - 4) expect 2; publish "$((start - 5))" ;; - 5) expect 3; echo generation-b > "$TB_RUNTIME/generation" ;; # a new snapshot - 6) expect 4; echo generation-a > "$TB_RUNTIME/generation" # another daemon - publish "$start" '[{"identity":{}}]' ;; - 7) expect 5 ;; # devices from here on - 8) expect 6; exit 0 ;; # so every tick scans - esac - } - (tb_watch) -} -test_watch_rescans_a_stale_snapshot() { - local tick=0 - jq -cnS --argjson time "$((EPOCHSECONDS - 30))" \ - '{available:true,time:$time,generation:"generation-a",devices:[],warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json" - tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; } - tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || exit 0; } - (tb_watch) - [[ $(wc -l < "$T/scans") == 3 ]] -} -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 diff --git a/test/shell.d/fixtures/thunderbolt/startup-test.sh b/test/shell.d/fixtures/thunderbolt/startup-test.sh deleted file mode 100644 index 4fc71f51..00000000 --- a/test/shell.d/fixtures/thunderbolt/startup-test.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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 diff --git a/test/shell.d/fixtures/usb-authorization/match.cpp b/test/shell.d/fixtures/usb-authorization/match.cpp deleted file mode 100644 index 5c26df69..00000000 --- a/test/shell.d/fixtures/usb-authorization/match.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include -#include -#include -#include - -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); -} diff --git a/test/shell.d/luks-rekey-journal-test.sh b/test/shell.d/luks-rekey-journal-test.sh index 190c0723..b1974084 100755 --- a/test/shell.d/luks-rekey-journal-test.sh +++ b/test/shell.d/luks-rekey-journal-test.sh @@ -93,7 +93,7 @@ sed -n '/^rekey_luks() {/,/^}/p; /^run_provisioning() {/,/^}/p; /^cleanup_oem_st /^platform_ready() {/,/^}/p; /^run_setup() {/,/^}/p /^rekey_accepts_password() {/,/^}/p' \ "$ROOT/bin/omarchy-provision-owner" | - sed -e "s|/etc/|$tmp/etc/|g" -e "s|/usr/bin/omarchy-thunderbolt-authorization-admin|omarchy-thunderbolt-authorization-admin|" >"$tmp/provision.sh" + sed "s|/etc/|$tmp/etc/|g" >"$tmp/provision.sh" grep -q '^run_provisioning() {' "$tmp/provision.sh" && grep -q '^run_setup() {' "$tmp/provision.sh" || fail "omarchy-provision-owner defines its setup and provisioning worker" @@ -241,11 +241,6 @@ setup_functions() { configure_hostname() { :; } configure_timezone() { :; } finalize_user() { :; } - # Accessory enrollment, recorded with whether the staged unlock was gone. - omarchy-pkg-present() { return 1; } - omarchy-thunderbolt-authorization-admin() { - if luks_staged_unlock_remains; then echo "$* early" >>"$TMP/accessories"; else echo "$*" >>"$TMP/accessories"; fi - } limine_entries_stale() { crash_point "re-key returned" [[ -e $TMP/stale ]] @@ -335,7 +330,7 @@ slot_count() { fixture() { local format=${1:-luks2} rm -rf "$tmp/provisioning" "$tmp/etc" "$tmp/boot" "$tmp/log" "$tmp/output" "$tmp/trace" "$tmp/rebuilds" "$tmp/adds" "$tmp/rebuild-fail" "$tmp/kill-noop" \ - "$tmp/prepare-fail" "$tmp/screen" "$tmp/stale" "$tmp/token-slot" "$tmp/slot-record" "$tmp/record-fail" "$tmp/accessories" + "$tmp/prepare-fail" "$tmp/screen" "$tmp/stale" "$tmp/token-slot" "$tmp/slot-record" "$tmp/record-fail" mkdir -p "$tmp/provisioning" chmod 755 "$tmp/provisioning" touch "$tmp/provisioning/pending" @@ -474,8 +469,6 @@ for run_spec in "${matrix[@]}"; do fixture "$format" run attempt "$owner_password" || fail "$backend: uninterrupted setup completes" "$(cat "$tmp/log" "$tmp/output")" assert_provisioned "uninterrupted" "$owner_password" - [[ $(cat "$tmp/accessories" 2>/dev/null) == "owner" ]] || - fail "$backend: accessories are enrolled once, after the staged unlock is gone" "$(cat "$tmp/accessories" 2>/dev/null)" total_steps=$(cat "$tmp/steps") (( total_steps >= 11 )) || fail "$backend: every durable step is a crash point" "$(cat "$tmp/trace")" if [[ $platform == "apple" ]]; then diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index 5ff8d58a..9ce3c10a 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -352,13 +352,6 @@ 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' diff --git a/test/shell.d/thunderbolt-authorization-test.sh b/test/shell.d/thunderbolt-authorization-test.sh deleted file mode 100755 index c10a84cf..00000000 --- a/test/shell.d/thunderbolt-authorization-test.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/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 diff --git a/test/shell.d/usb-authorization-policy-test.sh b/test/shell.d/usb-authorization-policy-test.sh deleted file mode 100755 index 97fc35a8..00000000 --- a/test/shell.d/usb-authorization-policy-test.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/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" diff --git a/test/shell.d/usb-authorization-test.sh b/test/shell.d/usb-authorization-test.sh deleted file mode 100755 index 7b0c7bfa..00000000 --- a/test/shell.d/usb-authorization-test.sh +++ /dev/null @@ -1,1002 +0,0 @@ -#!/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 ' >>"$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 <%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 <--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 ' "$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 ' "$calls" || fail "USB authorization ensures USBGuard is installed" -pass "USB authorization enrolls present devices before enabling default-deny" - -generate_count=$(grep -c '^usbguard $' "$calls") -"$scratch/setup" --yes >"$scratch/setup-existing-output" -generate_count_after=$(grep -c '^usbguard $' "$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 ' "$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 $' "$calls") -"$scratch/setup" --yes >"$scratch/setup-empty-again-output" -[[ $(grep -c '^usbguard $' "$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 ' "$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=$("$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" </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 ' "$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 <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" || - fail "fresh installation grants the owner narrowly scoped approval access" -grep -Fqx 'systemctl ' "$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 ' "$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 ' "$calls" || fail "deferred installation leaves enforcement disabled" -! grep -Fq 'systemctl ' "$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 <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" || - fail "owner provisioning grants approval access" -[[ $(tail -2 "$calls") == $'systemctl \nsystemctl ' ]] || - 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 .* <$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 <$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 ' "$calls") -"$USB_APPROVE_FIXTURE" 17 "allow ${malicious_rule#block }" -[[ $(grep -c '^usbguard ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$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 ' "$calls" || fail "removal stops a service Omarchy enabled" -grep -Fqx 'usbguard ' "$calls" || fail "removal drops the user's USBGuard IPC access" -grep -Fqx 'sudo ' "$calls" || - fail "removal restores USB authorization only through the fixed-path root helper" -grep -Fqx 'sudo ' "$calls" || - fail "removing USB authorization also removes early-boot denial" -! grep -Fqx 'pkg-drop ' "$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" - -# Boot-time denial writes Limine's own configuration, which a Mac's boot -# package owns: it is refused there before anything changes. Root's platform -# check reads this machine, never the fixture, so this runs unprivileged only. -if (( EUID == 0 )); then - skip "running as root, where the platform check ignores its fixture; skipping the Apple Silicon refusal" -else - fake_platform "$scratch/apple" aarch64-apple - mkdir -p "$scratch/apple-stubs" - printf '#!/bin/bash\necho "sudo $*" >>"%s"\n' "$scratch/apple-sudo" >"$scratch/apple-stubs/sudo" - chmod +x "$scratch/apple-stubs/sudo" - status=0 - OMARCHY_PROC_ROOT="$scratch/apple/proc" PATH="$scratch/apple/bin:$scratch/apple-stubs:$ROOT/bin:$PATH" \ - "$scratch/setup" --boot --yes >"$scratch/apple-out" 2>&1 || status=$? - (( status == 2 )) || fail "Apple Silicon refuses USB authorization from boot" "$(cat "$scratch/apple-out")" - grep -Fq "not available on Apple Silicon" "$scratch/apple-out" || fail "the refusal says why" "$(cat "$scratch/apple-out")" - [[ ! -e $scratch/apple-sudo ]] || fail "the refusal changes nothing" "$(cat "$scratch/apple-sudo")" - pass "USB authorization from boot is refused on Apple Silicon before anything changes" -fi - -# On Apple Silicon the boot helper never touches Limine: enabling is refused, -# disabling what is not configured succeeds (so USB authorization can still be -# turned off), and a configuration from elsewhere is left with removal steps. -( - drop_in=$scratch/apple-boot/usb-authorization.conf - limine_defaults=$scratch/apple-boot/limine - mkdir -p "$scratch/apple-boot" - status=0; usb_authorization_apple_boot enable 2>/dev/null || status=$? - (( status == 2 )) || fail "Apple Silicon refuses boot-time USB authorization in the helper" - usb_authorization_apple_boot disable || fail "a clean Mac disables boot-time USB authorization as a no-op" - echo "$setting" >"$drop_in" - if usb_authorization_apple_boot disable 2>"$scratch/apple-boot/err"; then fail "an inherited boot-time setting is not claimed removed"; fi - grep -Fq "Remove it by hand" "$scratch/apple-boot/err" || fail "the inherited setting comes with removal steps" -) -pass "the boot helper leaves a Mac's boot files alone and still lets USB authorization be turned off" diff --git a/test/shell.d/usb-boot-archives-test.sh b/test/shell.d/usb-boot-archives-test.sh deleted file mode 100755 index 1da27dea..00000000 --- a/test/shell.d/usb-boot-archives-test.sh +++ /dev/null @@ -1,257 +0,0 @@ -#!/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 diff --git a/test/shell.d/usb-boot-enrollment-test.sh b/test/shell.d/usb-boot-enrollment-test.sh deleted file mode 100755 index a1d98d57..00000000 --- a/test/shell.d/usb-boot-enrollment-test.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/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/$("$boot_path/$("$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/$("$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 diff --git a/test/shell.d/usb-factory-reset-test.sh b/test/shell.d/usb-factory-reset-test.sh deleted file mode 100755 index 023b8cc0..00000000 --- a/test/shell.d/usb-factory-reset-test.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/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