diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index b5321a4d..f4bff333 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -747,6 +747,12 @@ create_user() { echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel chmod 440 /etc/sudoers.d/00-omarchy-wheel + if omarchy-pkg-present usbguard; then + source "$OMARCHY_PATH/install/helpers/usb-authorization.sh" + usb_authorization_add_user "$username" + systemctl try-restart usbguard.service + fi + source "$OMARCHY_PATH/install/helpers/browser-policy.sh" for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do [[ -d $dir || -L $dir ]] || continue diff --git a/bin/omarchy-remove-security-usb-authorization b/bin/omarchy-remove-security-usb-authorization new file mode 100755 index 00000000..ba77a0f0 --- /dev/null +++ b/bin/omarchy-remove-security-usb-authorization @@ -0,0 +1,86 @@ +#!/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 + +systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true +rm -f "$unit" +systemctl --user daemon-reload + +if omarchy-pkg-present usbguard; then + sudo usbguard remove-user "$USER" 2>/dev/null || true +fi + +if omarchy-pkg-present usbguard; then + sudo systemctl disable --now usbguard.service 2>/dev/null || true + sudo omarchy-usb-authorization-restore-default +fi + +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-usb-authorization b/bin/omarchy-setup-security-usb-authorization new file mode 100755 index 00000000..0c21c558 --- /dev/null +++ b/bin/omarchy-setup-security-usb-authorization @@ -0,0 +1,112 @@ +#!/bin/bash + +# omarchy:summary=Ask before new USB devices can access the system +# omarchy:args=[--boot] [--yes] +# omarchy:requires-sudo=true + +set -euo pipefail + +assume_yes=0 +from_boot=0 + +while (( $# > 0 )); do + case "$1" in + --yes) assume_yes=1 ;; + --boot) from_boot=1 ;; + -h | --help) + echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes]" + 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." + exit 0 + ;; + *) + echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2 + exit 2 + ;; + esac + shift +done + +state_dir="$HOME/.local/state/omarchy/usb-authorization" +unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service" +unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service" +rules_file=/etc/usbguard/rules.conf +daemon_config=/etc/usbguard/usbguard-daemon.conf + +source "$OMARCHY_PATH/install/helpers/usb-authorization.sh" + +if (( ! 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. Continue only if unlock and recovery do not" + echo "depend on USB input." + 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 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 + +ln -sfn "$unit_source" "$unit" +systemctl --user daemon-reload +systemctl --user enable --now omarchy-usb-authorization.service + +if (( from_boot )); then + # Take the trust snapshot only after USBGuard is enforcing the policy. Every + # ID is resolved by the daemon, and no device-controlled descriptor text is + # passed to a privileged command. + 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." +if (( from_boot )); then + echo "Boot-time authorization is enabled and takes effect after reboot." +fi diff --git a/bin/omarchy-usb-authorization-boot b/bin/omarchy-usb-authorization-boot new file mode 100755 index 00000000..e72139a3 --- /dev/null +++ b/bin/omarchy-usb-authorization-boot @@ -0,0 +1,432 @@ +#!/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 ! limine-enroll-config; then + mv -f "$backup" "$config" + limine-enroll-config >/dev/null 2>&1 || true + echo "Restored $config because its bootloader verification data could not be updated." >&2 + return 1 + fi + + rm -f "$backup" +} + +main() { + local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary + + if (( EUID != 0 )); then + echo "Error: omarchy-usb-authorization-boot must run as root" >&2 + exit 1 + fi + + case "${1:-}" in + enable) + if usb_authorization_snapshot_block_status "$limine_defaults"; then + snapshot_status=0 + else + snapshot_status=$? + fi + if (( snapshot_status == 2 )); then + echo "Refusing to modify the USB authorization block in $limine_defaults." >&2 + exit 1 + fi + + # Quote the expected value because the Limine key contains glob brackets. + if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then + echo "Refusing to replace the existing $drop_in." >&2 + exit 1 + fi + + usb_authorization_enable_snapshot_setting "$limine_defaults" + + if [[ ! -e $drop_in ]]; then + install -d -m755 "${drop_in%/*}" + temporary=$(mktemp "${drop_in}.XXXXXXXXXX") + trap 'rm -f "$temporary"' EXIT + printf '%s\n' "$setting" >"$temporary" + chmod 644 "$temporary" + mv -f "$temporary" "$drop_in" + trap - EXIT + fi + + if ! limine-mkinitcpio || + ! usb_authorization_rewrite_boot_cmdlines enabled "$limine_conf" || + ! usb_authorization_verify_boot_entries enabled "$limine_conf" "$boot_path"; then + echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2 + exit 1 + fi + ;; + disable) + if usb_authorization_snapshot_block_status "$limine_defaults"; then + had_snapshot_setting=1 + else + snapshot_status=$? + if (( snapshot_status == 2 )); then + echo "Refusing to modify the USB authorization block in $limine_defaults." >&2 + exit 1 + fi + fi + + if [[ -e $drop_in ]]; then + if [[ $(<"$drop_in") != "$setting" ]]; then + echo "Refusing to remove the modified $drop_in." >&2 + exit 1 + fi + had_drop_in=1 + rm -f "$drop_in" + elif (( ! had_snapshot_setting )) && usb_authorization_verify_boot_entries disabled "$limine_conf" "$boot_path"; then + exit 0 + fi + + usb_authorization_disable_snapshot_setting "$limine_defaults" + if ! limine-mkinitcpio || + ! usb_authorization_rewrite_boot_cmdlines disabled "$limine_conf" || + ! usb_authorization_verify_boot_entries 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 +} + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + main "$@" +fi diff --git a/bin/omarchy-usb-authorization-event b/bin/omarchy-usb-authorization-event new file mode 100755 index 00000000..53182715 --- /dev/null +++ b/bin/omarchy-usb-authorization-event @@ -0,0 +1,40 @@ +#!/bin/bash + +# omarchy:hidden=true +# omarchy:summary=Create an approval request for a blocked USB device + +set -euo pipefail + +[[ ${USBGUARD_IPC_SIGNAL:-} == "Device.PresenceChanged" ]] || exit 0 +[[ ${USBGUARD_DEVICE_EVENT:-} == "Insert" ]] || exit 0 +[[ ${USBGUARD_DEVICE_TARGET:-} == "block" ]] || exit 0 +[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0 +[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0 + +requests="$HOME/.local/state/omarchy/usb-authorization/requests" +mkdir -p "$requests" +chmod 700 "${requests%/*}" "$requests" + +# 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" +temporary=$(mktemp "$requests/.request.XXXXXXXXXX") +trap 'rm -f "$temporary"' EXIT + +jq -n \ + --arg id "$USBGUARD_DEVICE_ID" \ + --arg rule "$USBGUARD_DEVICE_RULE" \ + '{id: $id, rule: $rule}' >"$temporary" +chmod 600 "$temporary" +mv -f "$temporary" "$request" + +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" diff --git a/bin/omarchy-usb-authorization-restore-default b/bin/omarchy-usb-authorization-restore-default new file mode 100755 index 00000000..e5844db8 --- /dev/null +++ b/bin/omarchy-usb-authorization-restore-default @@ -0,0 +1,23 @@ +#!/bin/bash + +# omarchy:summary=Restore the kernel's default USB authorization state +# omarchy:requires-sudo=true +# omarchy:hidden=true + +set -euo pipefail + +if (( EUID != 0 )); then + echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2 + exit 1 +fi + +# USBGuard intentionally leaves root hubs in default-deny mode when it exits. +# Restore both the hub defaults and devices that are still connected. +shopt -s nullglob +for attribute in /sys/bus/usb/devices/usb*/authorized_default; do + echo 1 >"$attribute" +done +for attribute in /sys/bus/usb/devices/*/authorized; do + echo 1 >"$attribute" +done +shopt -u nullglob diff --git a/bin/omarchy-usb-authorization-review b/bin/omarchy-usb-authorization-review new file mode 100755 index 00000000..aa3c16c4 --- /dev/null +++ b/bin/omarchy-usb-authorization-review @@ -0,0 +1,79 @@ +#!/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" +if [[ ! -f $request || -L $request ]]; then + echo "This USB authorization request is no longer available." >&2 + exit 1 +fi + +id=$(jq -er '.id | select(test("^[0-9]+$"))' "$request") +expected_rule=$(jq -er '.rule | select(type == "string" and length > 0)' "$request") +read_blocked_rule() { + local line + + while IFS= read -r line; do + if [[ $line == "$id: "* ]]; then + printf '%s\n' "${line#"$id: "}" + return 0 + fi + done < <(usbguard list-devices --blocked) + + return 1 +} + +current_rule=$(read_blocked_rule || true) + +if [[ -z $current_rule || $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 + +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 + +if [[ $choice != "Keep blocked" ]]; then + current_rule=$(read_blocked_rule || true) + if [[ -z $current_rule || $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") + # Target the complete identity rule instead of USBGuard's numeric ID. IDs can + # be reassigned when the daemon restarts while the review dialog is open. + usbguard allow-device "$expected_rule" + echo "USB accessory allowed until it is disconnected." + ;; +"Always allow this device") + usbguard allow-device --permanent "$expected_rule" + echo "USB accessory added to the trusted policy." + ;; +"Keep blocked") + echo "USB accessory remains blocked." + ;; +esac + +rm -f "$request" diff --git a/bin/omarchy-usb-authorization-watch b/bin/omarchy-usb-authorization-watch new file mode 100755 index 00000000..8b22fdf4 --- /dev/null +++ b/bin/omarchy-usb-authorization-watch @@ -0,0 +1,23 @@ +#!/bin/bash + +# omarchy:hidden=true +# omarchy:summary=Watch USBGuard for devices awaiting user approval + +set -euo pipefail + +event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event" + +# A device inserted while the graphical session was stopped is already safely +# blocked by USBGuard. Surface it now so the user can approve it after login. +while IFS= read -r line; do + if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then + USBGUARD_IPC_SIGNAL=Device.PresenceChanged \ + USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \ + USBGUARD_DEVICE_EVENT=Insert \ + USBGUARD_DEVICE_TARGET=block \ + USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \ + "$event_handler" + fi +done < <(usbguard list-devices --blocked) + +exec usbguard watch --wait --exec "$event_handler" diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 7583d8d8..c0dd2eda 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -181,6 +181,8 @@ "setup.config": {"icon":"","label":"Config"}, "setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"}, "setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"}, + "setup.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","checked":"systemctl is-enabled --quiet usbguard.service && systemctl --user is-enabled --quiet omarchy-usb-authorization.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-usb-authorization"}, + "setup.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","checked":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-security-usb-authorization --boot'"}, "setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"}, "setup.security.passwordless-sudo": {"icon":"󰟵","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"}, "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"}, @@ -298,6 +300,8 @@ "remove.security": {"icon":"","label":"Security","title":"Remove"}, "remove.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"}, "remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"}, + "remove.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","when":"systemctl is-enabled --quiet usbguard.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-usb-authorization"}, + "remove.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","when":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-security-usb-authorization --boot-only'"}, "remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"}, "remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"}, "remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"}, diff --git a/default/systemd/user/omarchy-usb-authorization.service b/default/systemd/user/omarchy-usb-authorization.service new file mode 100644 index 00000000..bfc27a61 --- /dev/null +++ b/default/systemd/user/omarchy-usb-authorization.service @@ -0,0 +1,17 @@ +[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 +ExecCondition=/usr/bin/systemctl is-active --quiet usbguard.service +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/install/config/all.sh b/install/config/all.sh index a221a398..3ff8d66a 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -7,5 +7,6 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh" run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh" run_logged "$OMARCHY_INSTALL/config/docker.sh" run_logged "$OMARCHY_INSTALL/config/snapper.sh" +run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh" run_logged "$OMARCHY_INSTALL/config/enable-services.sh" run_logged "$OMARCHY_INSTALL/config/firewall.sh" diff --git a/install/config/usb-authorization.sh b/install/config/usb-authorization.sh new file mode 100644 index 00000000..958b82ae --- /dev/null +++ b/install/config/usb-authorization.sh @@ -0,0 +1,28 @@ +echo "Configure USB device authorization" + +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 + policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX") + chmod 600 "$policy" + + if usb_authorization_generate_policy "$policy"; then + install -Dm600 -o root -g root "$policy" "$rules_file" + rm -f "$policy" + else + rm -f "$policy" + return 1 + fi +fi + +if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then + usb_authorization_add_user "$OMARCHY_INSTALL_USER" +fi + +# 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/usb-authorization.sh b/install/helpers/usb-authorization.sh new file mode 100644 index 00000000..219a8e5b --- /dev/null +++ b/install/helpers/usb-authorization.sh @@ -0,0 +1,99 @@ +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 + usb_authorization_require_secure_setting "$config" PresentDevicePolicy apply-policy + usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy + usb_authorization_require_secure_setting "$config" AuthorizedDefault none + usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false +} + +usb_authorization_generate_policy() { + local policy="$1" + + usbguard generate-policy >"$policy" + if [[ ! -s $policy ]] || ! grep -q '^allow ' "$policy"; then + echo "USBGuard did not generate a usable policy; nothing was enabled." >&2 + return 1 + fi +} + +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_trust_present_devices() { + local line id + local count=0 + local -a devices + + mapfile -t devices < <(usbguard list-devices) + + for line in "${devices[@]}"; do + if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then + id="${BASH_REMATCH[1]}" + usbguard allow-device --permanent "$id" + (( ++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 76e74110..3579c27c 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -133,6 +133,7 @@ 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 9865fc20..67addca5 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -18,4 +18,5 @@ systemctl --user enable --now \ omarchy-sleep-lock.service \ omarchy-migrate-notify.service \ omarchy-fcitx5.service \ - omarchy-crash-watch.service + omarchy-crash-watch.service \ + omarchy-usb-authorization.service diff --git a/manual/48-security.md b/manual/48-security.md index 45750398..76f1db24 100644 --- a/manual/48-security.md +++ b/manual/48-security.md @@ -6,7 +6,20 @@ Omarchy takes security extremely seriously. This is meant to be an operating sys 2. *Firewall is enabled by default*: All incoming traffic is blocked by default except for port 53317 for [LocalSend](https://localsend.org/). Even ssh is off until you turn it on via _Setup > Security > SSHD_, which opens port 22 (rate limited against brute force) as part of the setup. We even lock down Docker access using the [ufw-docker](https://github.com/chaifeng/ufw-docker) setup to prevent that your containers are accidentally exposed to the world. 3. *Arch always have the latest updates*: Arch, the underlying distro that Omarchy is built on, is a rolling distribution. This means that any security vulnerability that's discovered and patched in any package is quickly available for install using `omarchy-update`. You're always running the latest, most secure versions of everything that way. 4. *Omarchy maintains its own packages and mirror*: Omarchy only relies on packages from Arch's own core/extra/multilib repositories and its own Omarchy Package Repository by default. You can install software directly from AUR, but the base install doesn't — only a few optional installs, like the third-party browsers, pull from the AUR. -5. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability. +5. *New USB devices require approval*: Omarchy trusts the USB devices present during installation, then blocks newly connected accessories until you approve them. This keeps an unknown accessory from reaching a matching kernel driver merely because it claims a supported device identity. +6. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability. + +## USB Device Authorization + +USB device authorization is enabled by default. Omarchy uses USBGuard to trust the devices connected during installation and block USB devices it has not seen before. When a device is blocked, click the persistent notification and choose whether to allow it once, always allow it, or keep it blocked. + +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 USB input, _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. _Remove > Security > USB at Boot_ restores the normal early-boot behavior while keeping USBGuard active after startup. ## Changing your passwords diff --git a/migrations/1789433473.sh b/migrations/1789433473.sh new file mode 100644 index 00000000..47a59c55 --- /dev/null +++ b/migrations/1789433473.sh @@ -0,0 +1,3 @@ +echo "Enable approval for newly connected USB devices" + +omarchy-setup-security-usb-authorization --yes diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index a424379b..cf8b7a35 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -324,6 +324,13 @@ assert( defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'), 'menu places Passwordless Sudo under Setup > Security' ) +assert( + defaultById['setup.security.usb-authorization'].action.includes('omarchy-setup-security-usb-authorization') + && defaultById['remove.security.usb-authorization'].action.includes('omarchy-remove-security-usb-authorization') + && defaultById['setup.security.usb-authorization-boot'].action.includes('omarchy-setup-security-usb-authorization --boot') + && defaultById['remove.security.usb-authorization-boot'].action.includes('omarchy-remove-security-usb-authorization --boot-only'), + 'menu can enable and remove USB device authorization under Security' +) assert( !defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'], 'menu removes the relocated toggles from Trigger > Toggle' diff --git a/test/shell.d/usb-authorization-test.sh b/test/shell.d/usb-authorization-test.sh new file mode 100755 index 00000000..68c8ed56 --- /dev/null +++ b/test/shell.d/usb-authorization-test.sh @@ -0,0 +1,405 @@ +#!/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 +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 [[ $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 [[ $* == "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" + echo 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' + ;; +list-devices) + if [[ ${2:-} == "--blocked" ]]; then + 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 + 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 + ;; +*) + printf 'usbguard' >>"$CALLS" + printf ' <%s>' "$@" >>"$CALLS" + printf '\n' >>"$CALLS" + ;; +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 == 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) + 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) exit 0 ;; +esac +STUB +cat >"$stub_bin/omarchy-notification-send" <<'STUB' +#!/bin/bash +printf 'notification' >>"$CALLS" +printf ' <%s>' "$@" >>"$CALLS" +printf '\n' >>"$CALLS" +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" + +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" + +"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-output" + +grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' "$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" +[[ -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") +"$ROOT/bin/omarchy-setup-security-usb-authorization" --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" + +: >"$calls" +"$ROOT/bin/omarchy-setup-security-usb-authorization" --boot --yes >"$scratch/setup-boot-output" +grep -Fqx 'usbguard <--permanent> <4>' "$calls" || + fail "boot authorization trusts the connected root controller" +grep -Fqx 'usbguard <--permanent> <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" + +: >"$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_add_user "$username"' "$ROOT/bin/omarchy-provision-owner" || + fail "deferred provisioning grants the new owner approval access" +grep -Fq 'systemctl try-restart usbguard.service' "$ROOT/bin/omarchy-provision-owner" || + fail "deferred provisioning reloads the new owner ACL" +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"' "$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" + +malicious_rule='block id 07a6:8513 name "$(touch '"$scratch"'/injected)" hash "attacker" with-interface 02:06:00' +export BLOCKED_DEVICE_RULE="$malicious_rule" +USBGUARD_IPC_SIGNAL=Device.PresenceChanged \ + USBGUARD_DEVICE_ID=17 \ + USBGUARD_DEVICE_EVENT=Insert \ + USBGUARD_DEVICE_TARGET=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" + +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" + +USBGUARD_IPC_SIGNAL=Device.PresenceChanged \ + USBGUARD_DEVICE_ID=17 \ + USBGUARD_DEVICE_EVENT=Insert \ + USBGUARD_DEVICE_TARGET=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 -Fqx "usbguard <--permanent> <$malicious_rule>" "$calls" || fail "review can persist an exact-device rule" +pass "USB review supports explicit persistent trust" + +USBGUARD_IPC_SIGNAL=Device.PresenceChanged \ + USBGUARD_DEVICE_ID=17 \ + USBGUARD_DEVICE_EVENT=Insert \ + USBGUARD_DEVICE_TARGET=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.PresenceChanged \ + USBGUARD_DEVICE_ID=17 \ + USBGUARD_DEVICE_EVENT=Insert \ + USBGUARD_DEVICE_TARGET=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" + +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" + +"$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"