Require approval for new Thunderbolt accessories

This commit is contained in:
acrogenesis committed 2026-09-28 09:16:57 -06:00
1 parent 27bbbb2fe9
commit 60d6b255da
35 files changed
+2503 -2

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ Three documentation trees, split by genre and audience:
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security-sensitive entrypoint may use the exact `#!/bin/bash -p` form only when it must suppress `BASH_ENV` and exported-function startup injection before its first command; that exception must be explained at the boundary and covered by a regression that rejects an ordinary Bash launch with a decoy `-p` argument.
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
# Command Naming
+3
View File
@@ -1040,6 +1040,9 @@ run_provisioning() {
usb_authorization_provision_owner "$username"
fi
log_step "enrolling the owner's Thunderbolt accessories"
/usr/bin/omarchy-thunderbolt-authorization-admin owner
log_step "cleaning up provisioning state"
cleanup_oem_state
log_step "first-boot setup complete"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# omarchy:summary=Restore Bolt's default authorization behavior
set -euo pipefail
gum confirm "Restore Bolt's previous policy? New accessories may be authorized automatically when IOMMU protection is active." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin disable
systemctl --user disable --now omarchy-thunderbolt-authorization.service
echo "Previous Bolt policy restored. Saved Omarchy trust is retained for re-enabling."
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:summary=Restore saved Thunderbolt firmware boot permissions
set -euo pipefail
gum confirm "Restore the previous Thunderbolt firmware boot allowlists? Device approval will remain enabled." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-disable
echo "Previous firmware boot permissions restored and verified."
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Provide internal helpers for command-scoped sudo authentication
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
echo "omarchy-security-functions is an internal function library." >&2
exit 64
fi
omarchy_security_require_privileged_bash_startup() {
[[ $- == *p* ]] || return 1
/usr/bin/env -i /usr/bin/bash -p -c '
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
[[ $executable == "/usr/bin/bash" &&
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
${argv[1]:-} == "-p" ]]
' omarchy-bash-startup "$$"
}
omarchy_security_sanitize_bash_environment() {
local script=$1
shift
local entry name environment_fd environment_pid
local -a unsets=()
# Read the raw environment: privileged Bash ignores exported functions, but
# leaves their records for ordinary child interpreters to import later.
exec {environment_fd}< <(/usr/bin/env -0)
environment_pid=$!
while IFS= read -r -d '' entry <&"$environment_fd"; do
name=${entry%%=*}
case "$name" in
BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|CDPATH|GLOBIGNORE|BASH_FUNC_*%%)
unsets+=(-u "$name")
;;
esac
done
exec {environment_fd}<&-
wait "$environment_pid" || return 1
if (( ${#unsets[@]} > 0 )); then
exec /usr/bin/env "${unsets[@]}" /usr/bin/bash -p -- "$script" "$@"
fi
}
omarchy_security_require_source_root() {
local command_source command_name=${1##*/}
command_source=$(/usr/bin/readlink -e -- "$1") || return 1
# A runtime root selects the code used by this invocation. Accept the
# canonical checkout containing the entrypoint or the package's bin links.
if [[ ${OMARCHY_PATH:-} != /* || $(/usr/bin/realpath -e -- "$OMARCHY_PATH") != "$OMARCHY_PATH" ]] ||
! { [[ $command_source == "$OMARCHY_PATH/bin/$command_name" ]] ||
[[ $OMARCHY_PATH == "/usr/share/omarchy" && $command_source == "/usr/bin/$command_name" ]]; }; then
echo "OMARCHY_PATH does not match this Omarchy command." >&2
return 1
fi
}
omarchy_security_sudo_supports_no_update() {
local help
help=$(LC_ALL=C /usr/bin/sudo -h 2>&1) || return 1
/usr/bin/grep -Eq '^usage: sudo .*\[[^]]*N[^]]*\]' <<< "$help"
}
omarchy_security_revoke_sudo_timestamp() {
/usr/bin/sudo -k
}
omarchy_security_exit_with_revoked_sudo() {
local status=$1
local message=${2:-Could not invalidate cached sudo authorization.}
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
echo "$message" >&2
(( status != 0 )) || status=1
fi
exit "$status"
}
omarchy_security_install_signal_exit_traps() {
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
}
omarchy_security_install_sudo_cleanup_traps() {
OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE=${1:-Could not invalidate cached sudo authorization.}
trap omarchy_security_run_sudo_cleanup_trap EXIT
omarchy_security_install_signal_exit_traps
}
omarchy_security_enable_no_update_sudo() {
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
if ! omarchy_security_sudo_supports_no_update; then
echo "This sudo does not support --no-update; refusing mixed-trust work." >&2
return 1
fi
if [[ ! -f $wrapper_dir/sudo || ! -x $wrapper_dir/sudo ]]; then
echo "The command-scoped sudo wrapper is missing." >&2
return 1
fi
PATH="$wrapper_dir:$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
OMARCHY_SUDO_NO_UPDATE=1
export PATH OMARCHY_SUDO_NO_UPDATE
}
omarchy_security_run_sudo_cleanup_trap() {
local status=$?
omarchy_security_exit_with_revoked_sudo "$status" \
"${OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE:-Could not invalidate cached sudo authorization.}"
}
omarchy_security_assert_root_directory() {
local path=$1 expected_mode=$2 canonical owner actual_mode
[[ $path == /* && -d $path && ! -L $path ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$path") || return 1
[[ $canonical == "$path" ]] || return 1
read -r owner actual_mode < <(/usr/bin/stat -Lc '%u %a' -- "$path") || return 1
[[ $owner == "0" && $actual_mode == "$expected_mode" ]]
}
omarchy_security_prepare_private_root_directory() {
local path=$1 parent=$2
omarchy_security_assert_root_directory "$parent" 755 || return 1
if [[ -e $path || -L $path ]]; then
omarchy_security_assert_root_directory "$path" 700
else
/usr/bin/install -d -o root -g root -m 0700 -- "$path" || return 1
omarchy_security_assert_root_directory "$path" 700
fi
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# omarchy:summary=Require approval for new Thunderbolt PCIe accessories
# omarchy:args=[--quiet|status|recover]
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
if [[ ${1:-} == "status" ]]; then
tb_status
elif [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin recover
elif [[ $# == 0 || ( $# == 1 && $1 == "--quiet" ) ]]; then
if [[ ${1:-} != "--quiet" ]]; then
if [[ -f $TB_PENDING ]]; then tb_status; fi
echo "First setup, including a deferred setup, trusts connected accessories. Re-enabling a completed setup preserves saved trust."
gum confirm "Require approval for new Thunderbolt accessories?" || exit 0
fi
# System units and rules are shipped by omarchy-settings.
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
sudo systemctl daemon-reload
action=enable
if [[ ${1:-} == "--quiet" ]]; then
action=migrate
fi
sudo /usr/bin/omarchy-thunderbolt-authorization-admin "$action"
systemctl --user daemon-reload
systemctl --user enable --now omarchy-thunderbolt-authorization.service
if [[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]; then
for ((attempt=0; attempt<10; attempt++)); do
if tb_public_snapshot >/dev/null 2>&1; then break; fi
sleep 1
done
tb_status
elif [[ -f $TB_PENDING ]]; then
tb_status
fi
else
echo "Usage: omarchy-setup-security-thunderbolt-authorization [--quiet|status|recover]" >&2
exit 1
fi
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# omarchy:summary=Clear supported Thunderbolt firmware boot allowlists
# omarchy:args=[recover]
set -euo pipefail
if [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-recover
elif [[ $# == 0 ]]; then
gum style --bold "Thunderbolt at Boot"
echo "This trusts connected accessories for use after Linux starts, then clears the firmware boot allowlist."
echo "Trusted Thunderbolt keyboards, storage, and network adapters may be unavailable at disk unlock."
echo "You must be able to unlock and recover without Thunderbolt accessories."
echo "All controllers must be connected and support a boot allowlist in user or secure mode."
echo "Older snapshots or another operating system may restore firmware boot permissions."
gum confirm "Clear firmware boot access with these recovery limits?" || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-enable
echo "Firmware boot allowlists cleared and verified. Current connections were not disconnected."
else
echo "Usage: omarchy-setup-security-thunderbolt-boot [recover]" >&2
exit 1
fi
+13
View File
@@ -320,6 +320,12 @@ scrub_factory_accounts() {
sanitize_factory_baseline() {
local factory="$1"
btrfs property set -ts "$factory" ro false
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
btrfs property set -ts "$factory" ro true
fail "could not reset Thunderbolt authorization in the factory baseline"
fi
fi
if ! scrub_factory_accounts "$factory"; then
btrfs property set -ts "$factory" ro true
@@ -368,6 +374,13 @@ stage_full_reset() {
scrub_factory_accounts "$next" ||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
# Leave the new owner's input available before interactive enrollment. Clear
# inherited accessory identities in both roots; never change the live daemon.
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
fail "could not reset Thunderbolt authorization in the staged root"
fi
# @factory itself survives the wipe as the baseline for future resets. If it
# came from a normal install it still holds the seller's account and
# /etc/shadow, which the new wheel user could mount and read — and a second
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash -p
# omarchy:summary=Manage system Thunderbolt authorization settings
# omarchy:hidden=true
# This root entrypoint uses installed code and rejects unsafe Bash startup.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
source /usr/share/omarchy/install/helpers/thunderbolt-setup.sh
tb_admin "$@"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash -p
# omarchy:summary=Approve one verified Thunderbolt connection
# omarchy:hidden=true
# pkexec permits this exact program for an active local wheel user. Suppress
# startup injection before reading input; it cannot dispatch general root work.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
[[ $# == 2 && ( $1 == "true" || $1 == "false" ) ]] || exit 64
(( ${#2} <= 16384 )) || exit 64
jq -e 'type == "object"' <<< "$2" >/dev/null
exec 9> "$TB_LOCK"
flock -x 9
result=$(tb_approve "$2" "$1" true)
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$result"
printf '%s\n' "$result"
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Check whether Thunderbolt firmware boot access is restricted
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
tb_public_snapshot | jq -e .boot_protection >/dev/null
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash -p
# omarchy:summary=Restore trusted Thunderbolt devices and publish their status
# omarchy:hidden=true
# Keep root startup independent of a caller's environment or checkout.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
tb_daemon
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval review
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
[[ $# == 1 ]] || exit 64
tb_review "$1"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval watch
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
tb_watch
+4
View File
@@ -179,6 +179,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"},
@@ -298,6 +300,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"},
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.thunderbolt.approve">
<description>Approve a connected Thunderbolt accessory</description>
<message>Authentication is required to approve this Thunderbolt accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-thunderbolt-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
@@ -0,0 +1,16 @@
[Unit]
Description=Offer approval for blocked Thunderbolt accessories
After=graphical-session.target
PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.enabled
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.pending
[Service]
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-watch
Restart=always
RestartSec=2
UMask=0077
[Install]
WantedBy=graphical-session.target
+43
View File
@@ -0,0 +1,43 @@
# Thunderbolt authorization
Omarchy uses Bolt for Thunderbolt/USB4 PCIe authorization. USB functions remain separate and need USB authorization. The end-user controls and firmware limitations are documented in [the Security manual](../manual/48-security.md#thunderbolt-device-authorization).
## Policy and services
Bolt's default IOMMU policy can enroll new devices without asking. Omarchy keeps `AuthMode=disabled` in `/var/lib/boltd/boltd.conf` and authorizes individual devices through Bolt's D-Bus API. This does not disable Bolt or prevent explicit authorization. Setting only `DefaultPolicy=manual` would still leave IOMMU auto-enrollment enabled.
The root `omarchy-thunderbolt-authorization.service` holds explicit consent in `/var/lib/omarchy/thunderbolt-authorization/policy.json`. Bolt can import devices authorized by firmware, so its database is not treated as consent. Initial setup captures currently connected accessories; subsequent enables preserve that inventory, including an initially empty inventory. Always-allow approvals authorize, read back the real status, enroll with manual policy, verify storage, and save consent. Trusted devices are restored even without an unlocked graphical session. No operation globally enables Bolt to authorize one device.
Omarchy records which Bolt entries it enrolled, including their store timestamps, separately from consent. Removing protection converts only matching, successfully approved entries from manual to automatic policy before restoring Bolt's global mode; this includes disconnected accessories. Existing manual records and externally recreated records retain their policies. Secure keys are kept. The handoff verifies the stored policies after restarting Bolt and checkpoints policies, firmware allowlists, configuration and services for rollback. Automatic policies resume Bolt's normal firmware allowlist behavior, so every controller must be online for a recoverable handoff. A completed handoff clears enrollment ownership while retaining the trust list for re-enabling.
The Bolt startup drop-in sets the persisted mode before hardware enumeration whenever `/etc/omarchy/thunderbolt-authorization.enabled` exists. The Omarchy service checks the live mode again. Firmware security `none` remains a bypass: software cannot prevent driver loading after firmware has already opened a PCIe tunnel. This produces a warning instead of disconnecting active storage.
Secure-capable devices require both a stored Bolt record and a saved key for automatic trust. An explicit permanent approval pins the current sysfs directory and key descriptor, seeds an empty kernel key before authorization, and verifies Bolt imported it before saving consent. Automatic reconciliation cannot initialize a missing key and retains its read-only sysfs sandbox. First activation is deferred when a captured secure-capable device lacks a saved Bolt key: the `.pending` marker starts a setup notification, while existing Bolt configuration and services remain untouched. Installation can finish; only explicit setup after safe disconnection clears the pending state. Initial identity capture uses Bolt's numeric device/vendor fallback when descriptive sysfs attributes are absent.
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
The terminal dialog saves an approval intent; the background watcher submits it through `pkexec` to the fixed installed `/usr/bin/omarchy-thunderbolt-authorization-approve` helper. Polkit grants active local wheel users this individual approval action. The helper validates its arguments and the live connection, then independently verifies authorization and any saved trust before returning success. Root entrypoints use protected Bash startup and fixed installed libraries; they discard caller-supplied D-Bus addresses. A shared lock serializes setup, reconciliation, and approvals. General setup uses a separate administrator command invoked with `sudo` from the terminal. Blanket Bolt management requires administrator authentication while protection is enabled. These controls protect against unsolicited accessories, not malicious software already running as an administrator. Identifiers can be forged; Bolt's secure connection keys provide additional verification on supported hardware.
## Firmware boot access
The optional boot control applies only to online controllers with a firmware BootACL in `user` or `secure` mode. It captures connected accessories, changes stored policies to manual, clears each allowlist, and independently reads back the results. Reconciliation keeps firmware imports and returning controllers from silently restoring automatic boot permissions. Unsupported or offline controllers produce explicit warnings/errors.
Changes retain the previous policy and firmware lists. A failed transition restores matching state, or leaves a recovery checkpoint and reports failure. Device-authorization setup has a separate checkpoint that includes service state and firmware settings when boot protection was active. Restore commands are described in the manual. Firmware modified by another OS or an older snapshot is outside the current system's control; this is not an early-boot kernel authorization switch.
Deferred owner setup leaves ordinary Bolt behavior in place until the owner finishes provisioning. Factory reset disables the copied service and removes previous-owner trust, recovery data, Bolt identities, and keys from both staged and retained roots. It does not change the live daemon.
## Validation
Run policy, notification, rollback, deferred-owner, and real offline-systemctl tests with:
```sh
bash test/shell.d/thunderbolt-authorization-test.sh
```
The optional integration suite runs the installed `boltd` against upstream's UMockdev Thunderbolt fixture and a private D-Bus. Its hardware simulator uses Python/GObject; all Omarchy policy code and test assertions execute Bash. It needs `python-gobject`, `python-dbusmock`, `umockdev`, Bolt, and a Bolt source checkout (validated with 0.9.11):
```sh
umockdev-wrapper env BOLT_TEST_SOURCE=/path/to/bolt bash test/shell.d/thunderbolt-authorization-test.sh
```
It covers user/secure modes, IOMMU default denial, verified permanent and one-time approval, daemon restart, stale requests, firmware ACL restoration, and a separate Bash daemon publishing status after reconnecting to Bolt. Removal cases exercise the real setup and approval functions, automatic reconnection with retained secure keys, preservation of existing manual policies, disconnected accessories, and rollback of policies, firmware ACLs and the real private approval daemon after a setup failure. A service-control bridge manages only the fixture's private processes. These tests simulate kernel devices and firmware. They do not demonstrate physical DMA isolation or real firmware enforcement during boot. Systemd packaging, the installed approval helper, and the actual desktop Polkit rules require installed-guest validation; source-only dev linking does not install system units or rules.
@@ -0,0 +1,17 @@
polkit.addRule(function(action, subject) {
if (action.id === "org.omarchy.thunderbolt.approve" &&
subject.active && subject.local && subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
// Bolt's vendor rule grants wheel passwordless management, including enabling
// automatic approval of every IOMMU device. Keep blanket policy changes behind
// administrator authentication; the narrow Omarchy action approves one device.
if (action.id === "org.freedesktop.bolt.manage" && subject.user !== "root") {
try {
polkit.spawn(["/usr/bin/test", "-f", "/etc/omarchy/thunderbolt-authorization.enabled"]);
return polkit.Result.AUTH_ADMIN;
} catch (error) {
// Protection is disabled: retain the packaged Bolt permission rules.
}
}
});
@@ -0,0 +1,3 @@
[Service]
# Runs before Bolt enumerates hardware, including D-Bus/udev activation.
ExecStartPre=/usr/bin/omarchy-thunderbolt-authorization-admin guard
@@ -0,0 +1,26 @@
[Unit]
Description=Restore approved Thunderbolt devices and handle new approvals
Wants=bolt.service
After=bolt.service polkit.service
ConditionPathExists=/etc/omarchy/thunderbolt-authorization.enabled
[Service]
Type=simple
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-daemon
RuntimeDirectory=omarchy-thunderbolt-authorization
RuntimeDirectoryMode=0755
Restart=on-failure
RestartSec=2
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/omarchy/thunderbolt-authorization /run/lock
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX
[Install]
WantedBy=multi-user.target
+1
View File
@@ -10,3 +10,4 @@ run_logged "$OMARCHY_INSTALL/config/snapper.sh"
run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh"
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
run_logged "$OMARCHY_INSTALL/config/thunderbolt-authorization.sh"
@@ -0,0 +1,10 @@
echo "Configure Thunderbolt device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
# The builder's accessories must not become the new owner's trust policy.
# Keep ordinary Bolt behavior until interactive owner setup has finished.
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin prepare
systemctl enable omarchy-thunderbolt-authorization.service
fi
+319
View File
@@ -0,0 +1,319 @@
# Shared by the root controller and session commands. These paths are fixed at
# privileged entrypoints; tests may source the functions in an isolated fixture.
TB_STATE=/var/lib/omarchy/thunderbolt-authorization/policy.json
TB_CONFIG=/var/lib/boltd/boltd.conf
TB_MARKER=/etc/omarchy/thunderbolt-authorization.enabled
TB_PENDING=/etc/omarchy/thunderbolt-authorization.pending
TB_RUNTIME=/run/omarchy-thunderbolt-authorization
TB_LOCK=/run/lock/omarchy-thunderbolt-authorization.lock
TB_SERVICE=omarchy-thunderbolt-authorization.service
TB_BOLT=org.freedesktop.bolt
TB_MANAGER=org.freedesktop.bolt1.Manager
TB_DEVICE=org.freedesktop.bolt1.Device
TB_DOMAIN=org.freedesktop.bolt1.Domain
TB_PATH=/org/freedesktop/bolt
TB_SYSFS=/sys/bus/thunderbolt/devices
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
tb_json_write() {
local target=$1 mode=${2:-600} temporary
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
if jq -cS . > "$temporary" && chmod "$mode" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$target" && sync "${target%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_state() {
jq -ce 'select(.version == 1 and (.trusted | type == "object") and (.enabled | type == "boolean"))' "$TB_STATE"
}
tb_bus() {
busctl --system --auto-start=no --timeout=10 --json=short "$@"
}
tb_owner() {
tb_bus call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$TB_BOLT" | jq -er '.data[0]'
}
tb_properties() {
tb_bus call "$1" "$2" org.freedesktop.DBus.Properties GetAll s "$3" | jq -ce '.data[0] | map_values(.data)'
}
tb_set() {
local owner=$1 path=$2 interface=$3 key=$4 value=$5 signature=${6:-s} current
local -a values=()
current=$(tb_properties "$owner" "$path" "$interface") || return 1
if ! jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null; then
if [[ $signature == "as" ]]; then
mapfile -t values < <(jq -r '.[]' <<< "$value")
tb_bus set-property "$owner" "$path" "$interface" "$key" as "${#values[@]}" "${values[@]}" >/dev/null || return 1
else
tb_bus set-property "$owner" "$path" "$interface" "$key" s "$(jq -r . <<< "$value")" >/dev/null || return 1
fi
fi
# Bolt rejects no-op BootACL writes; even skipped writes are read back.
current=$(tb_properties "$owner" "$path" "$interface") || return 1
jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null ||
tb_fail "Bolt did not retain the requested $key"
}
tb_inventory() {
local owner manager paths path item inode domains='[]' devices='[]' stored='[]'
owner=$(tb_owner) || return 1
manager=$(tb_properties "$owner" "$TB_PATH" "$TB_MANAGER") || return 1
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDomains | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DOMAIN" | jq -c --arg path "$path" '. + {path:$path}') || return 1
domains=$(jq -c --argjson item "$item" '. + [$item]' <<< "$domains") || return 1
done < <(jq -r '.[]' <<< "$paths")
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDevices | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DEVICE" | jq -c --arg path "$path" '. + {path:$path}') || return 1
[[ $(jq -r .Type <<< "$item") == "peripheral" ]] || continue
if jq -e .Stored <<< "$item" >/dev/null; then
stored=$(jq -c --argjson item "$item" '. + [$item]' <<< "$stored") || return 1
fi
[[ $(jq -r .Status <<< "$item") != "disconnected" ]] || continue
inode=$(stat -Lc '%i' -- "$(jq -r .SysfsPath <<< "$item")") || return 1
item=$(jq -c --arg inode "$inode" '. + {inode:$inode}' <<< "$item") || return 1
devices=$(jq -c --argjson item "$item" '. + [$item]' <<< "$devices") || return 1
done < <(jq -r '.[]' <<< "$paths")
[[ $(tb_owner) == "$owner" ]] || { tb_fail "Bolt restarted while reading its inventory"; return 1; }
jq -cn --arg owner "$owner" --argjson manager "$manager" --argjson domains "$domains" \
--argjson devices "$devices" --argjson stored "$stored" '
{owner:$owner,manager:$manager,domains:$domains,stored:$stored,devices:[$devices[] |
. + {Secure:((.Domain as $domain | any($domains[]; .Uid == $domain and .SecurityLevel == "secure"))
and (.AuthFlags | contains("nokey") | not))}]}'
}
tb_read_inventory() {
local inventory
inventory=$(tb_inventory) || return 1
jq -e '.manager.AuthMode == "disabled"' <<< "$inventory" >/dev/null || {
tb_fail "Bolt automatic authorization is enabled; protection is not active"
return 1
}
printf '%s\n' "$inventory"
}
tb_identity() {
jq -c --arg owner "$1" --arg generation "$2" \
'{owner:$owner,generation:$generation,path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime}'
}
tb_snapshot() {
local state inventory generation recovery=false
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
[[ -f ${TB_STATE%/*}/boot-recovery.json ]] && recovery=true
jq -c --argjson state "$state" --arg generation "$generation" --argjson recovery "$recovery" \
--arg error "${1:-}" --argjson time "$(date +%s)" "$TB_TRUST_JQ"'
.owner as $owner |
[.devices[] | . as $d | {
identity: ({owner:$owner,generation:$generation} + ($d | {path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime})),
trusted: ($d | trusted($state)),
status:.Status,stored:.Stored,policy:.Policy,flags:.AuthFlags
}] as $devices |
{available:true,time:$time,owner:$owner,generation:$generation,devices:$devices,error:$error,
boot_protection:($state.boot_protection // false),
domains:[.domains[] | {Uid,SecurityLevel,IOMMU,BootACL}],
warnings: ([
(if $recovery then "A firmware boot-access change needs recovery. Boot protection is not confirmed." else empty end),
(.domains[] | if .SecurityLevel == "none" then
"This controller allows PCIe connections in firmware. Select user authorization in firmware settings; Omarchy cannot block its devices before their drivers load."
else empty end),
(.domains[] | .SecurityLevel as $level | select(["none","user","secure","dponly","usbonly","nopcie"] | index($level) | not) |
"The controller authorization mode could not be verified."),
(.domains[] | select($state.boot_protection and (.SysfsPath == "" or
(.SecurityLevel != "user" and .SecurityLevel != "secure") or (.BootACL | length == 0) or any(.BootACL[]; . != ""))) |
"The firmware boot allowlist is not confirmed empty on every controller. Check Thunderbolt Boot before relying on boot protection."),
($devices[] | select((.status | startswith("authorized")) and (.trusted | not) and (.flags | contains("boot"))) |
"An untrusted device was authorized by firmware before Linux started. Disconnect it safely to apply approval on its next connection.")
] | unique)}' <<< "$inventory"
}
tb_current() {
local expected=$1 inventory owner generation device identity
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if jq -e --argjson expected "$expected" '. == $expected' <<< "$identity" >/dev/null; then
jq -cn --arg owner "$owner" --argjson device "$device" '{owner:$owner,device:$device}'
return 0
fi
done < <(jq -c '.devices[]' <<< "$inventory")
tb_fail "That Thunderbolt device was removed or changed. Open its latest notification."
}
tb_prepare_key() (
local identity=$1 current key key_fd
current=$(tb_current "$identity") || return 1
# Bolt creates keys for stored devices itself. For a new device, disabled
# AuthMode prevents enrollment before authorization. Seed the kernel key so
# Authorize provisions it and the subsequent enrollment imports it into Bolt.
jq -e '.device.Secure and (.device.Stored | not)' <<< "$current" >/dev/null || return 0
cd -- "$(jq -r .device.SysfsPath <<< "$current")" || return 1
[[ $(stat -Lc '%i' .) == "$(jq -r .device.inode <<< "$current")" &&
$(cat unique_id) == "$(jq -r .device.Uid <<< "$current")" ]] || {
tb_fail 'The Thunderbolt connection changed before key setup'; return 1;
}
# Pin the attribute in the original directory; do not follow a replacement
# connection at the same path, or truncate an existing key on a retry.
exec {key_fd}<>key || return 1
key=$(cat key) || return 1
if [[ -z $key ]]; then
[[ $(cat authorized) == "0" ]] || { tb_fail 'Reconnect the device before saving secure approval'; return 1; }
key=$(openssl rand -hex 32) || return 1
[[ $key =~ ^[0-9a-f]{64}$ ]] || return 1
tb_current "$identity" >/dev/null || return 1
printf '%s' "$key" >&"$key_fd" || return 1
[[ $(cat key) == "$key" ]] || { tb_fail 'Could not verify the Thunderbolt key'; return 1; }
else
[[ $key =~ ^[0-9a-fA-F]{64}$ ]] || { tb_fail 'The Thunderbolt key is invalid'; return 1; }
fi
)
tb_approve() {
local identity=$1 permanent=$2 initialize=${3:-false} current owner path status state
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail "Thunderbolt protection is disabled"; return 1; }
current=$(tb_current "$identity") || return 1
owner=$(jq -r .owner <<< "$current")
path=$(jq -r .device.path <<< "$current")
status=$(jq -r .device.Status <<< "$current")
case "$status" in
connected|auth-error)
if [[ $permanent == "true" ]]; then
if [[ $initialize == "true" ]]; then
tb_prepare_key "$identity" || return 1
else
jq -e '(.device.Secure | not) or (.device.Stored and (.device.Key | IN("have","new")))' <<< "$current" >/dev/null || {
tb_fail 'A secure key requires explicit device approval'; return 1;
}
fi
fi
current=$(tb_current "$identity") || return 1
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null || return 1 ;;
authorized|authorized-newkey|authorized-secure) ;;
*) tb_fail "Device authorization is still in progress; try again"; return 1 ;;
esac
current=$(tb_current "$identity") || return 1
jq -e '.device.Status | IN("authorized","authorized-newkey","authorized-secure")' <<< "$current" >/dev/null || {
tb_fail "Could not confirm device authorization; try again"; return 1;
}
if [[ $permanent == "true" ]]; then
if ! jq -e .device.Stored <<< "$current" >/dev/null; then
# Save enrollment intent before Bolt writes its record, so a lost reply
# can be retried without confusing our record with a pre-existing one.
state=$(jq --argjson device "$(jq -c .device <<< "$current")" \
'.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state") || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$(jq -r .device.Uid <<< "$current")" manual '' >/dev/null || return 1
fi
current=$(tb_current "$identity") || return 1
jq -e .device.Stored <<< "$current" >/dev/null || { tb_fail "Could not confirm saved device information; try again"; return 1; }
jq -e '(.device.Secure | not) or (.device.Key | IN("have","new"))' <<< "$current" >/dev/null || {
tb_fail 'Secure approval has no saved key. Safely disconnect, reconnect, and approve the device again.'; return 1;
}
jq --argjson device "$(jq -c .device <<< "$current")" '
if .enrolled[$device.Uid] == ($device | {Uid,Name,Vendor}) then
.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor,StoreTime})
else . end |
.trusted[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state" |
tb_json_write "$TB_STATE" || return 1
fi
tb_snapshot
}
tb_boot_apply() {
local saved=$1 inventory owner item uid policies acls value path
inventory=$(tb_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
jq -e --argjson saved "$saved" '
(($saved.policies | keys) - [.stored[].Uid] | length == 0) and
(($saved.acls | keys) - [.domains[] | select(.SysfsPath != "") | .Uid] | length == 0)' <<< "$inventory" >/dev/null || {
tb_fail "Reconnect the original controllers and restore missing Bolt records before recovery"; return 1;
}
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.policies[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DEVICE" Policy "$value" || return 1
done < <(jq -c '.stored[]' <<< "$inventory")
# Changing a device policy can update firmware ACLs, so restore ACLs last.
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.acls[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
[[ $(jq length <<< "$value") == "$(jq '.BootACL | length' <<< "$item")" ]] || {
tb_fail "Firmware boot allowlist capacity changed"; return 1;
}
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DOMAIN" BootACL "$value" as || return 1
done < <(jq -c '.domains[]' <<< "$inventory")
}
tb_reconcile() {
local inventory state owner generation device identity error='' saved
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
if jq -e .boot_protection <<< "$state" >/dev/null; then
saved=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map(select(.SysfsPath != "" and (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length>0))) |
map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
tb_boot_apply "$saved" || return 1
fi
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if ! tb_approve "$identity" true >/dev/null; then
error='A trusted Thunderbolt device could not be restored. Check the system service log.'
fi
done < <(jq -c --argjson state "$state" "$TB_TRUST_JQ"'.devices | sort_by(.SysfsPath | length)[] |
select((.Status == "connected" or .Status == "auth-error") and (.AuthFlags | contains("nopcie") | not)) |
# Never silently initialize a missing secure binding on a later connection.
select(trusted($state))' <<< "$inventory")
tb_snapshot "$error"
}
tb_daemon() {
local snapshot error
install -d -m 755 "$TB_RUNTIME"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
chmod 644 "$TB_RUNTIME/generation"
while [[ -f $TB_MARKER ]]; do
(
flock -x 9
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$snapshot"
else
cat "$TB_RUNTIME/error" >&2
error=$(< "$TB_RUNTIME/error")
jq -cn --arg error "$error" --argjson time "$(date +%s)" '{available:false,time:$time,error:$error}' |
tb_json_write "$TB_RUNTIME/snapshot.json" 644
fi
) 9> "$TB_LOCK"
sleep 2
done
}
tb_public_snapshot() {
local snapshot generation
snapshot=$(cat "$TB_RUNTIME/snapshot.json") || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
jq -e --argjson now "$(date +%s)" --arg generation "$generation" \
'.available and .generation == $generation and .time <= $now and $now - .time < 20' <<< "$snapshot" >/dev/null || {
tb_fail 'Thunderbolt policy is unavailable or could not be verified.'; return 1;
}
printf '%s\n' "$snapshot"
}
+254
View File
@@ -0,0 +1,254 @@
# Session-side notification and terminal dialog functions.
tb_requests() {
TB_REQUESTS="$HOME/.local/state/omarchy/thunderbolt-authorization/requests"
mkdir -p -m 700 "$TB_REQUESTS"
chmod 700 "${TB_REQUESTS%/*}" "$TB_REQUESTS"
}
tb_safe_text() {
jq -r 'gsub("[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]"; "?")'
}
tb_notify() {
local token=$1 retry=${2:-false} title='Thunderbolt accessory blocked' description
description='Click to review it. Keep it blocked unless you recognize the device.'
if [[ $retry == "true" ]]; then
title='Thunderbolt approval needs retry'
description='The last approval could not be confirmed. Click to retry it.'
fi
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical --glyph '󱐋' \
"$title" "$description" --exec omarchy-launch-floating-terminal-with-presentation \
omarchy-thunderbolt-authorization-review "$token"
}
tb_attention() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Thunderbolt protection needs attention' 'The approval or firmware policy could not be confirmed. Click for details.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization status
}
tb_setup_notice() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Finish Thunderbolt protection setup' 'A connected accessory needs secure enrollment. Protection is not enabled. Click to finish safely.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization
}
tb_token() {
local digest
digest=$(jq -cS --arg watch "$2" '{watch:$watch,identity:.}' <<< "$1" | sha256sum) || return 1
printf 'request-%s\n' "${digest%% *}"
}
tb_request_read() {
[[ -f $1 && ! -L $1 ]] || return 1
jq -ce 'select(.identity | type == "object")' "$1"
}
tb_deliver() {
local path=$1 request token
request=$(tb_request_read "$path") || return 1
if ! jq -e '.notified // false' <<< "$request" >/dev/null; then
token=${path##*/}
if tb_notify "${token%.json}" "$(jq -r '.approval != null' <<< "$request")"; then
jq '.notified=true' <<< "$request" | tb_json_write "$path"
else
return 1
fi
fi
}
tb_request_apply() {
# The watcher has no terminal. Polkit permits this one fixed helper for an
# active local administrator; it cannot dispatch general setup operations.
pkexec --disable-internal-agent /usr/bin/omarchy-thunderbolt-authorization-approve "$1" "$2"
}
tb_request_process() {
local path=$1 request identity permanent result error_file error
request=$(tb_request_read "$path") || return 1
if jq -e '.pending // false' <<< "$request" >/dev/null; then
identity=$(jq -c .identity <<< "$request")
permanent=$(jq -r '.approval == "Always allow this device"' <<< "$request")
error_file=$(mktemp "$TB_REQUESTS/.error.XXXXXX") || return 1
if result=$(tb_request_apply "$permanent" "$identity" 2> "$error_file") &&
jq -e --argjson identity "$identity" --argjson permanent "$permanent" \
'any(.devices[]; .identity==$identity and (.status | IN("authorized","authorized-newkey","authorized-secure")) and
($permanent == false or .trusted))' <<< "$result" >/dev/null; then
jq --argjson result "$result" '.pending=false | .result=$result | del(.error)' <<< "$request" |
tb_json_write "$path" || return 1
else
error=$(cat "$error_file")
[[ -n $error ]] || error='Could not confirm approval. Click the retry notification.'
jq --arg error "$error" '.pending=false | .notified=false | .error=$error' <<< "$request" |
tb_json_write "$path" || return 1
fi
rm -f -- "$error_file"
fi
tb_deliver "$path"
}
tb_scan() {
local watch=$1 snapshot path request device identity token existing
snapshot=$(tb_public_snapshot) || return 1
(
flock -x 9
for path in "$TB_REQUESTS"/request-*.json; do
[[ -e $path || -L $path ]] || continue
if ! request=$(tb_request_read "$path"); then
rm -f -- "$path"
continue
fi
identity=$(jq -c .identity <<< "$request")
device=$(jq -c --argjson identity "$identity" '.devices[] | select(.identity==$identity)' <<< "$snapshot") || exit 1
if [[ -z $device ]] || { ! jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null &&
! jq -e '.approval != null' <<< "$request" >/dev/null; }; then
rm -- "$path"
continue
fi
if [[ $(jq -r .watch <<< "$request") != "$watch" ]]; then
rm -- "$path"
# Resume an interrupted attempt only for the same exact connection.
jq -e '.approval != null and .result == null' <<< "$request" >/dev/null || continue
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
request=$(jq -c --arg watch "$watch" '.watch=$watch | .notified=false' <<< "$request") || exit 1
tb_json_write "$path" <<< "$request" || exit 1
fi
tb_request_process "$path" || true
done
snapshot=$(tb_public_snapshot) || exit 1
while IFS= read -r device; do
identity=$(jq -c .identity <<< "$device")
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
if [[ ! -f $path ]]; then
jq -cn --argjson identity "$identity" --arg watch "$watch" \
'{identity:$identity,watch:$watch,notified:false}' | tb_json_write "$path" || exit 1
if ! tb_deliver "$path"; then
rm -f -- "$path"
fi
fi
done < <(jq -c '.devices[] | select((.trusted | not) and (.status | IN("connected","auth-error")) and
(.flags | contains("nopcie") | not))' <<< "$snapshot")
) 9> "$TB_REQUESTS/.lock" || return 1
printf '%s\n' "$snapshot"
}
tb_watch() {
local watch snapshot warning warned='' failures=0
tb_requests
watch=$(cat /proc/sys/kernel/random/uuid)
while true; do
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
sleep 2
continue
fi
if snapshot=$(tb_scan "$watch"); then
failures=0
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
if jq -e '(.warnings | length > 0) or .error != ""' <<< "$snapshot" >/dev/null; then
if [[ $warning != "$warned" ]] && tb_attention; then
warned=$warning
fi
else
warned=''
fi
else
failures=$((failures + 1))
if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then
warned=unavailable
fi
fi
sleep 2
done
}
tb_find_device() {
local snapshot
snapshot=$(tb_public_snapshot) || return 1
jq -ce --argjson identity "$1" '.devices[] | select(.identity==$identity)' <<< "$snapshot" ||
tb_fail 'That Thunderbolt device was removed or changed. Open its latest notification.'
}
tb_review() {
local token=$1 path request identity device choice latest index
[[ $token =~ ^request-[0-9a-f]{64}$ ]] || { tb_fail 'Invalid or expired Thunderbolt request'; return 1; }
tb_requests
path="$TB_REQUESTS/$token.json"
request=$(tb_request_read "$path") || return 1
identity=$(jq -c .identity <<< "$request")
device=$(tb_find_device "$identity") || return 1
choice=$(jq -r '.approval // empty' <<< "$request")
if [[ -z $choice ]]; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || { tb_fail 'This device is no longer blocked'; return 1; }
gum style --foreground 212 --bold 'Blocked Thunderbolt accessory'
printf '\nThese details come from the device and can be forged:\n'
for index in Name Vendor Uid; do
printf '%s: ' "$index"
jq --arg key "$index" '.[$key]' <<< "$identity" | tb_safe_text
done
printf '\nApproval gives this accessory access to its PCIe drivers.\n\n'
choice=$(gum choose 'Allow once' 'Always allow this device' 'Keep blocked') || return 0
fi
case "$choice" in
'Allow once'|'Always allow this device'|'Keep blocked') ;;
*) tb_fail 'Invalid approval choice'; return 1 ;;
esac
(
flock -x 9
latest=$(tb_request_read "$path") || exit 1
[[ $latest == "$request" ]] || { tb_fail 'This request changed. Open the latest notification.'; exit 1; }
device=$(tb_find_device "$identity") || exit 1
if [[ $choice == "Keep blocked" ]]; then
printf 'Thunderbolt accessory remains blocked.\n'
else
if ! jq -e '.approval != null' <<< "$request" >/dev/null; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || exit 1
fi
jq --arg choice "$choice" '.approval=$choice | .pending=true | del(.error,.result)' <<< "$request" |
tb_json_write "$path"
fi
) 9> "$TB_REQUESTS/.lock" || return 1
if [[ $choice != "Keep blocked" ]]; then
for ((index=0; index<60; index++)); do
latest=$(tb_request_read "$path") || return 1
if jq -e '.result != null' <<< "$latest" >/dev/null; then
rm -- "$path"
if [[ $choice == "Always allow this device" ]]; then
printf 'Thunderbolt accessory trusted.\n'
else
printf 'Thunderbolt accessory allowed for this connection.\n'
fi
return 0
elif jq -e '.error != null' <<< "$latest" >/dev/null; then
jq .error <<< "$latest" | tb_safe_text >&2
return 1
fi
sleep 1
done
tb_fail 'Approval is still pending. The watcher will retain and retry the request.'
fi
}
tb_status() {
local snapshot
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
printf 'Thunderbolt protection is deferred. Previous Bolt behavior remains active.\nKeep a keyboard/input device available independently of Thunderbolt. Safely disconnect the Thunderbolt accessories, then run Setup > Security > Thunderbolt. Reconnect each accessory and choose Always allow to save its secure key.\n'
return 0
fi
snapshot=$(tb_public_snapshot) || return 1
printf 'Thunderbolt device approval is enabled.\nTrusted accessories reconnect automatically; new accessories require approval.\n'
if jq -e '.domains | length == 0' <<< "$snapshot" >/dev/null; then
printf 'No Thunderbolt controller is currently visible. Its firmware policy will be checked when it appears.\n'
fi
jq '.warnings[], (.error | select(. != ""))' <<< "$snapshot" | while IFS= read -r warning; do
printf 'Attention: '
tb_safe_text <<< "$warning"
done
printf 'Firmware boot access is separate; see the Security manual before relying on pre-boot protection.\n'
}
+398
View File
@@ -0,0 +1,398 @@
# Root setup functions. The caller sources thunderbolt-policy.sh first.
tb_config_authmode() {
local path=$1
if [[ -f $path ]]; then
awk '
/^[[:space:]]*\[/ { section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/) }
section && /^[[:space:]]*AuthMode[[:space:]]*=/ {
sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); mode=$0
}
END { print mode == "" ? "enabled" : mode }
' "$path"
else
printf 'enabled\n'
fi
}
tb_config_write() {
local path=$1 mode=$2 temporary
[[ $mode == "enabled" || $mode == "disabled" ]] || return 1
install -d -m 700 "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.config.XXXXXX") || return 1
if awk -v mode="$mode" '
/^[[:space:]]*\[/ {
section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/)
if (section) { found=1; print; print "AuthMode = " mode; next }
}
section && /^[[:space:]]*AuthMode[[:space:]]*=/ { next }
{ print }
END { if (!found) print "\n[config]\nAuthMode = " mode }
' "$(if [[ -e $path ]]; then printf '%s' "$path"; else printf /dev/null; fi)" > "$temporary" &&
sync "$temporary" && mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_guard() {
if [[ -f $TB_MARKER ]]; then
tb_state | jq -e .enabled >/dev/null || { tb_fail 'Thunderbolt policy and enable marker disagree'; return 1; }
tb_config_write "$TB_CONFIG" disabled
fi
}
tb_capture() {
local path uid name vendor trusted='{}'
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 ]] || continue
uid=$(cat "$path/unique_id") || return 1
# Bolt uses the numeric attribute when a descriptive name is absent.
if [[ -e $path/device_name ]]; then
name=$(cat "$path/device_name") || return 1
else
name=$(cat "$path/device") || return 1
fi
if [[ -e $path/vendor_name ]]; then
vendor=$(cat "$path/vendor_name") || return 1
else
vendor=$(cat "$path/vendor") || return 1
fi
[[ -n $uid ]] || { tb_fail 'A connected Thunderbolt device has no identity'; return 1; }
trusted=$(jq -c --arg uid "$uid" --arg name "$name" --arg vendor "$vendor" \
'.[$uid] = {Uid:$uid,Name:$name,Vendor:$vendor}' <<< "$trusted") || return 1
done
printf '%s\n' "$trusted"
}
tb_capture_ready() {
local fresh=${1:-false} offline=${2:-false} path uid inode inventory=''
# Do not relax an active policy, or capture new devices when re-enabling an
# existing policy. This preflight only guards the initial trust inventory.
[[ ! -f $TB_MARKER ]] || return 0
if [[ -f $TB_STATE && ! -f $TB_PENDING && $fresh != "true" ]]; then
tb_state | jq -e '.enrollment_pending // false' >/dev/null || return 0
fi
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 && -e $path/key ]] || continue
uid=$(cat "$path/unique_id") || return 1
[[ $uid =~ ^[0-9a-fA-F-]{36}$ ]] || { tb_fail 'A connected Thunderbolt device has an invalid identity'; return 1; }
path=$(realpath -e -- "$path") || return 1
inode=$(stat -Lc '%i' -- "$path") || return 1
# A key file alone can be orphaned after a failed Bolt record write. Ask
# Bolt whether this exact connection has a stored record and key. Offline
# prepare must not accept the live installer's daemon as the target store.
if [[ $offline != "true" && -z $inventory ]]; then
inventory=$(tb_inventory) || inventory='{}'
fi
if [[ $offline == "true" ]] || ! jq -e --arg uid "$uid" --arg path "$path" --arg inode "$inode" \
'any(.devices[]?; .Uid==$uid and .SysfsPath==$path and .inode==$inode and .Stored and (.Key | IN("have","new")))' <<< "$inventory" >/dev/null; then
# An active keyless dock cannot receive a key without disconnecting it.
# Preserve input/storage and finish installation with setup deferred.
install -d -m 755 "${TB_PENDING%/*}" || return 1
install -m 644 /dev/null "$TB_PENDING" || return 1
printf 'Thunderbolt protection is deferred: safely disconnect accessories without saved secure keys, keep independent input available, then run Setup > Security > Thunderbolt.\n' >&2
return 2
fi
done
}
tb_prepare() {
local fresh=${1:-false} state='{}' trusted mode
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
fi
if [[ ! -f $TB_STATE || -f $TB_PENDING || $fresh == "true" ]] || jq -e '.enrollment_pending // false' <<< "$state" >/dev/null; then
trusted=$(tb_capture) || return 1
mode=$(jq -r '.original_authmode // empty' <<< "$state") || return 1
[[ -n $mode ]] || mode=$(tb_config_authmode "$TB_CONFIG") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c --arg mode "$mode" --argjson trusted "$trusted" \
'del(.enrollment_pending) | . + {version:1,enabled:true,trusted:$trusted,original_authmode:$mode}' <<< "$state") || return 1
else
mode=$(jq -r .original_authmode <<< "$state") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c '.enabled=true' <<< "$state") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_config_write "$TB_CONFIG" disabled || return 1
install -d -m 755 "${TB_MARKER%/*}" || return 1
install -m 644 /dev/null "$TB_MARKER"
}
tb_boot_saved() {
jq -c '{policies:(.stored | map({key:.Uid,value:.Policy}) | from_entries),
acls:(.domains | map({key:.Uid,value:.BootACL}) | from_entries)}'
}
tb_boot_change() {
local enabled=$1 state inventory saved after desired backup="${TB_STATE%/*}/boot-recovery.json"
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail 'Enable Thunderbolt device authorization first'; return 1; }
if [[ $enabled == "false" ]] && ! jq -e .boot_protection <<< "$state" >/dev/null; then
return 0
fi
[[ ! -e $backup ]] || { tb_fail 'Recover the previous firmware boot-access change before retrying'; return 1; }
inventory=$(tb_read_inventory) || return 1
jq -e '.domains | length > 0 and all(.[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Connect every Thunderbolt controller before changing firmware boot access'; return 1;
}
saved=$(tb_boot_saved <<< "$inventory" | jq -c --argjson state "$state" '. + {state:$state}') || return 1
if [[ $enabled == "true" ]]; then
jq -e 'all(.domains[]; (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length > 0))' <<< "$inventory" >/dev/null || {
tb_fail 'This controller has no supported firmware boot allowlist. Disable Thunderbolt pre-boot/PCIe boot support in firmware settings; Omarchy cannot verify that setting.'; return 1;
}
desired=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
after=$(jq -c --argjson saved "$saved" --argjson devices "$(jq -c .devices <<< "$inventory")" '
if .boot_protection then . else .boot_original=($saved | {policies,acls}) end |
.boot_protection=true | .trusted += ($devices | map({key:.Uid,value:{Uid,Name,Vendor}}) | from_entries)' <<< "$state") || return 1
else
desired=$(jq -ce .boot_original <<< "$state") || return 1
after=$(jq -c 'del(.boot_original) | .boot_protection=false' <<< "$state") || return 1
fi
tb_json_write "$backup" <<< "$saved" || return 1
if tb_boot_apply "$desired" && tb_json_write "$TB_STATE" <<< "$after"; then
rm -- "$backup"
else
if tb_boot_apply "$saved" && tb_json_write "$TB_STATE" <<< "$state"; then
rm -- "$backup" || return 1
tb_fail 'Firmware change failed; previous state restored'
else
tb_fail "Firmware change failed; recovery is incomplete. Recovery data retained at $backup"
fi
return 1
fi
}
tb_boot_recover() {
local backup="${TB_STATE%/*}/boot-recovery.json" saved
saved=$(jq -ce . "$backup") || return 1
tb_boot_apply "$saved" || return 1
jq .state <<< "$saved" | tb_json_write "$TB_STATE" || return 1
rm -- "$backup"
}
tb_unit_state() {
local properties
properties=$(systemctl show -p ActiveState -p UnitFileState "$1") || return 1
jq -Rn '[inputs | split("=") | {key:.[0],value:.[1]}] | from_entries |
{active:(.ActiveState == "active"),enabled:(.UnitFileState == "enabled")}' <<< "$properties"
}
tb_handoff_policies() {
local state=$1
# StoreTime distinguishes a record recreated outside Omarchy. A completed
# enrollment and matching consent are both required, including offline
# accessories. Never promote somebody else's manual policy.
jq -c --argjson state "$state" '{policies:([.stored[] |
select(.Policy == "manual" and .StoreTime > 0 and
$state.enrolled[.Uid] == {Uid,Name,Vendor,StoreTime} and
$state.trusted[.Uid] == {Uid,Name,Vendor}) |
{key:.Uid,value:"auto"}] | from_entries),acls:{}}'
}
tb_config_checkpoint() {
local backup="${TB_STATE%/*}/setup-recovery.json" path original files='{}' units='{}' unit value saved inventory data mode state
[[ ! -e $backup && ! -e ${TB_STATE%/*}/boot-recovery.json ]] || {
tb_fail 'A previous setup change needs recovery before continuing'; return 1;
}
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=null
if [[ -e $path ]]; then
data=$(base64 -w0 -- "$path") || return 1
mode=$(stat -c '%a' -- "$path") || return 1
original=$(jq -cn --arg data "$data" --arg mode "$mode" '{data:$data,mode:$mode}') || return 1
fi
files=$(jq -c --arg path "$path" --argjson original "$original" '.[$path]=$original' <<< "$files") || return 1
done
for unit in bolt.service "$TB_SERVICE"; do
value=$(tb_unit_state "$unit") || return 1
units=$(jq -c --arg unit "$unit" --argjson value "$value" '.[$unit]=$value' <<< "$units") || return 1
done
saved=$(jq -cn --argjson files "$files" --argjson units "$units" '{files:$files,units:$units}') || return 1
state='{}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
# A handoff to auto also changes Bolt's firmware allowlists. Checkpoint the
# policies and ACLs even when the optional boot protection was never used.
if jq -e '.enabled and (.boot_protection or ((.enrolled // {}) | length > 0))' <<< "$state" >/dev/null; then
inventory=$(tb_read_inventory) || return 1
jq -e 'all(.domains[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Reconnect every Thunderbolt controller before changing device policies'; return 1;
}
saved=$(jq -c --argjson firmware "$(tb_boot_saved <<< "$inventory")" '.firmware=$firmware' <<< "$saved") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$backup" <<< "$saved"
}
tb_config_restore() {
local backup="${TB_STATE%/*}/setup-recovery.json" saved path original temporary unit
saved=$(jq -ce . "$backup") || return 1
systemctl stop "$TB_SERVICE" bolt.service || return 1
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=$(jq -c --arg path "$path" '.files[$path]' <<< "$saved") || return 1
if [[ $original == "null" ]]; then
rm -f -- "$path" || return 1
else
mkdir -p -- "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.restore.XXXXXX") || return 1
if jq -r .data <<< "$original" | base64 -d > "$temporary" &&
chmod "$(jq -r .mode <<< "$original")" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
:
else
rm -f -- "$temporary"
return 1
fi
fi
done
if jq -e .firmware <<< "$saved" >/dev/null; then
systemctl start bolt.service || return 1
tb_boot_apply "$(jq -c .firmware <<< "$saved")" || return 1
fi
rm -f -- "${TB_STATE%/*}/boot-recovery.json" || return 1
if jq -e --arg unit "$TB_SERVICE" '.units[$unit].enabled' <<< "$saved" >/dev/null; then
systemctl enable "$TB_SERVICE" || return 1
else
systemctl disable "$TB_SERVICE" || return 1
fi
for unit in bolt.service "$TB_SERVICE"; do
if jq -e --arg unit "$unit" '.units[$unit].active' <<< "$saved" >/dev/null; then
systemctl start "$unit" || return 1
else
systemctl stop "$unit" || return 1
fi
done
rm -- "$backup"
}
tb_transaction() {
tb_config_checkpoint || return 1
if "$@"; then
rm -- "${TB_STATE%/*}/setup-recovery.json"
else
if tb_config_restore; then
tb_fail 'Thunderbolt setup failed; previous configuration restored'
else
tb_fail 'Thunderbolt setup failed; restoration is incomplete. Run the setup recovery command.'
fi
return 1
fi
}
tb_enable() {
systemctl stop "$TB_SERVICE" bolt.service || return 1
tb_prepare "${1:-false}" || return 1
systemctl daemon-reload || return 1
systemctl start bolt.service || return 1
tb_read_inventory >/dev/null || return 1
systemctl enable --now "$TB_SERVICE"
}
tb_disable() {
local state mode policies
systemctl stop "$TB_SERVICE" || return 1
tb_boot_change false || return 1
state=$(tb_state) || return 1
mode=$(jq -er .original_authmode <<< "$state") || return 1
policies=$(tb_read_inventory | tb_handoff_policies "$state") || return 1
tb_boot_apply "$policies" || return 1
systemctl stop bolt.service || return 1
tb_config_write "$TB_CONFIG" "$mode" || return 1
rm -f -- "$TB_MARKER" || return 1
jq '.enabled=false | del(.enrolled)' <<< "$state" | tb_json_write "$TB_STATE" || return 1
systemctl disable "$TB_SERVICE" || return 1
systemctl start bolt.service || return 1
tb_inventory | jq -e --arg mode "$mode" --argjson policies "$policies" '
.manager.AuthMode==$mode and
(([.stored[] | {key:.Uid,value:.Policy}] | from_entries) as $actual |
all($policies.policies | to_entries[]; $actual[.key]==.value))' >/dev/null
}
tb_cancel_pending() {
local state
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
else
state=$(jq -cn --arg mode "$(tb_config_authmode "$TB_CONFIG")" \
'{version:1,enabled:false,trusted:{},enrollment_pending:true,original_authmode:$mode}') || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
jq '.enabled=false | .enrollment_pending=true' <<< "$state" | tb_json_write "$TB_STATE" || return 1
rm -- "$TB_PENDING"
}
tb_reset_root() {
local root directory
root=$(realpath -e -- "$1") || return 1
[[ $root != "/" && -d $root ]] || { tb_fail 'Refusing to reset the running system'; return 1; }
[[ -e $root$TB_MARKER || -e $root$TB_PENDING || -d $root${TB_STATE%/*} ]] || return 0
# Resolve fixed paths inside the cloned root, never through an outside link.
for directory in etc etc/omarchy var var/lib var/lib/omarchy var/lib/boltd; do
[[ ! -L $root/$directory ]] || { tb_fail 'Refusing a linked factory policy directory'; return 1; }
done
systemctl --root="$root" disable "$TB_SERVICE" || return 1
rm -f -- "$root$TB_MARKER" || return 1
rm -f -- "$root$TB_PENDING" || return 1
rm -rf -- "$root${TB_STATE%/*}" || return 1
for directory in devices keys domains; do
rm -rf -- "$root/var/lib/boltd/$directory" || return 1
done
tb_config_write "$root/var/lib/boltd/boltd.conf" enabled
}
tb_admin() {
local action=${1:-} status=0 state
if [[ $action != "guard" ]]; then
exec 9> "$TB_LOCK"
flock -x 9 || return 1
fi
case "$action" in
prepare|enable|owner|migrate)
# A deferred setup needs explicit consent after the accessories have
# been disconnected; a later migration must not silently finish it.
if [[ -f $TB_PENDING && $action != "enable" ]]; then return 0; fi
if [[ $action == "migrate" && -f $TB_STATE ]] && tb_state | jq -e '.enabled == false' >/dev/null; then return 0; fi
if tb_capture_ready "$(if [[ $action == "owner" ]]; then echo true; else echo false; fi)" \
"$(if [[ $action == "prepare" ]]; then echo true; else echo false; fi)"; then
:
else
status=$?
if (( status == 2 )); then return 0; else return "$status"; fi
fi ;;
esac
case "$action" in
guard) tb_guard ;;
prepare) tb_prepare && rm -f -- "$TB_PENDING" ;;
enable) tb_transaction tb_enable && rm -f -- "$TB_PENDING" ;;
owner) tb_transaction tb_enable true && rm -f -- "$TB_PENDING" ;;
migrate)
state='{"enabled":true}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
if jq -e .enabled <<< "$state" >/dev/null; then
tb_transaction tb_enable
fi ;;
disable)
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
tb_cancel_pending
else
tb_transaction tb_disable
fi ;;
recover) tb_config_restore ;;
boot-enable|boot-disable|boot-recover)
systemctl stop "$TB_SERVICE" || return 1
case "$action" in
boot-enable) tb_boot_change true || status=$? ;;
boot-disable) tb_boot_change false || status=$? ;;
boot-recover) tb_boot_recover || status=$? ;;
esac
systemctl start "$TB_SERVICE" || return 1
return "$status" ;;
reset-root) [[ $# == 2 ]] && tb_reset_root "$2" ;;
*) tb_fail 'Unknown Thunderbolt setup action' ;;
esac
}
+2 -1
View File
@@ -20,6 +20,7 @@ systemctl --user enable --now \
omarchy-migrate-notify.service \
omarchy-fcitx5.service \
omarchy-crash-watch.service \
omarchy-usb-authorization.service
omarchy-usb-authorization.service \
omarchy-thunderbolt-authorization.service
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync
+20
View File
@@ -48,3 +48,23 @@ Be clear-eyed about this one: while it's on, anything running as your user can d
The public key for all ISO signatures and Omarchy repo package is `40DFB630FF42BCFFB047046CF0134EE680CAC571` ([verify at openpgp.org](https://keys.openpgp.org/search?q=pkgs%40omarchy.org)). The `omarchy/omarchy-keyring` package contains this as well and will be used to rollout any potential updates seamlessly.
You can find the signature for any ISO release by adding .sig to the URL. Like https://iso.omarchy.org/omarchy-x.x.x.iso.sig.
## Thunderbolt device authorization
Omarchy requires approval before connecting new Thunderbolt and USB4 PCIe accessories on controllers that support authorization. Devices connected during initial setup are trusted. New devices show a persistent notification with **Allow once**, **Always allow this device**, and **Keep blocked**. Trusted devices reconnect automatically, including while the screen is locked. IOMMU memory protection stays enabled; it does not count as consent to load a device's driver.
On controllers in secure mode, permanent approval also saves the accessory's authentication key. If a connected accessory has no saved key during initial setup, Omarchy defers Thunderbolt protection and keeps the previous Bolt behavior so your keyboard and storage remain usable. A **Finish Thunderbolt protection setup** notification explains how to continue: keep input available independently of Thunderbolt, safely disconnect the accessories, enable protection, then reconnect and approve each one. Losing a previously saved secure key requires a new explicit approval; Omarchy does not silently create a replacement key while restoring trusted devices.
_Setup > Security > Thunderbolt_ enables protection. _Remove > Security > Thunderbolt_ restores the previous Bolt behavior, which may automatically approve new devices when IOMMU protection is available. Re-enabling keeps your saved trust list. Omarchy never disconnects active storage when changing these settings.
When you remove protection, devices enrolled by Omarchy for permanent approval reconnect through Bolt's automatic policy, keeping their secure keys. Devices with a manual Bolt policy from before setup retain that policy. Bolt also resumes managing firmware boot allowlists for automatically approved devices. Keep all Thunderbolt controllers connected during removal so Omarchy can restore their settings if the change fails.
Firmware can authorize PCIe accessories before Linux runs. A controller configured with security mode `none` bypasses software approval; select user authorization or secure authorization in its firmware settings. Omarchy reports these limitations instead of claiming that the controller is protected. Existing firmware boot allowlists can also authorize devices early. USB accessories attached through a dock require separate USB authorization.
Approval does not repair a vulnerable driver. Device names and identifiers can be forged; secure connection keys are used where Bolt and the hardware support them.
_Setup > Security > Thunderbolt Boot_ can clear and verify supported firmware boot allowlists. It captures connected accessories for authorization after Linux starts, prevents Bolt from repopulating those allowlists from automatic device policies, and retains the previous lists for _Remove > Security > Thunderbolt Boot_. This option requires every controller to be connected and to expose a boot allowlist in `user` or `secure` mode. Other hardware needs firmware settings instead; Omarchy cannot verify those settings automatically.
Enable this only if you can unlock and recover without any Thunderbolt accessories. Even a trusted keyboard or disk may remain unavailable until the policy service starts. Older snapshots and other operating systems can restore their own firmware permissions. A failed change restores the previous settings when possible; an incomplete restoration retains recovery data. Reconnect the original controllers and run `omarchy setup security thunderbolt-boot recover` to retry that recovery.
If enabling or removing device authorization fails and reports incomplete restoration, run `omarchy setup security thunderbolt-authorization recover` before trying again. This restores the configuration and service state saved before that change.
+2
View File
@@ -0,0 +1,2 @@
echo "Require approval for new Thunderbolt accessories"
omarchy-setup-security-thunderbolt-authorization --quiet
@@ -0,0 +1,87 @@
#!/bin/bash
# Runs against the private real boltd created by bolt-integration.py. No policy,
# busctl, firmware reads, or writes are mocked here.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json"
TB_RUNTIME="$TB_TEST_DIR/runtime"
mkdir -p "$TB_RUNTIME"
check() { jq -e "$1" "$2" >/dev/null; }
reject() { if "$@"; then echo 'Unexpected approval success' >&2; exit 1; fi; }
case "$1" in
approve)
echo '{"version":1,"enabled":true,"trusted":{}}' | tb_json_write "$TB_STATE"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
tb_snapshot > "$TB_TEST_DIR/before.json"
capture=$(tb_capture)
tb_inventory | jq -e --arg uid "$TB_TEST_UID" --argjson capture "$capture" \
'.devices[] | select(.Uid==$uid) | $capture[$uid]=={Uid,Name,Vendor}' >/dev/null
check 'all(.devices[]; .status=="connected" and (.stored|not))' "$TB_TEST_DIR/before.json"
id=$(jq -c --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .identity' "$TB_TEST_DIR/before.json")
printf '%s\n' "$id" > "$TB_TEST_DIR/identity.json"
tb_approve "$id" true true > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized" and .policy=="manual"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
check 'all(.domains[0].BootACL[]; .=="")' "$TB_TEST_DIR/after.json"
inventory=$(tb_inventory)
if [[ $TB_TEST_SECURITY == "secure" ]]; then
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.Uid==$uid) | .Stored and .Key=="have"' <<< "$inventory" >/dev/null
[[ -s $TB_TEST_KEYS/$TB_TEST_UID ]]
sha256sum "$TB_TEST_KEYS/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi
owner=$(jq -r .owner <<< "$inventory")
device=$(jq -c --arg uid "$TB_TEST_UID" '.stored[] | select(.Uid==$uid)' <<< "$inventory")
domain=$(jq -c '.domains[0]' <<< "$inventory")
device_path=$(jq -r .path <<< "$device"); domain_path=$(jq -r .path <<< "$domain")
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"auto"'
acl=$(jq -c --arg uid "$TB_TEST_UID" '.BootACL | map("") | .[0]=$uid' <<< "$domain")
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$acl" as
tb_boot_change true
check .boot_protection "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
check 'all(.domains[0].BootACL[]; .=="") and .stored[0].Policy=="manual"' "$TB_TEST_DIR/inventory.json"
tb_boot_change false
check '.boot_protection==false' "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
jq -e --argjson acl "$acl" '.domains[0].BootACL==$acl and .stored[0].Policy=="auto"' "$TB_TEST_DIR/inventory.json" >/dev/null
jq --arg other "$TB_TEST_OTHER" 'del(.trusted[$other])' "$TB_STATE" | tb_json_write "$TB_STATE"
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"manual"'
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$(jq -c 'map("")' <<< "$acl")" as
;;
restart)
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
reject tb_approve "$(cat "$TB_TEST_DIR/identity.json")" true
tb_reconcile > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
tb_inventory | jq -e '.manager.AuthMode=="disabled"' >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
# Bolt must challenge with the originally saved key, not provision a new
# key merely because a reconnecting device advertises the same identity.
tb_inventory | jq -e --arg uid "$TB_TEST_UID" \
'.devices[] | select(.Uid==$uid) | .Key=="have" and (.AuthFlags | contains("secure"))' >/dev/null
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
;;
daemon)
TB_MARKER="$TB_TEST_DIR/enabled" TB_LOCK="$TB_TEST_DIR/root.lock"
touch "$TB_MARKER"
tb_daemon
;;
status)
snapshot=$(tb_public_snapshot)
jq -e --arg owner "$(tb_owner)" '.owner==$owner' <<< "$snapshot" >/dev/null
printf '%s\n' "$snapshot"
;;
once)
snapshot=$(tb_public_snapshot)
id=$(jq -c --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .identity' <<< "$snapshot")
exec 9> "$TB_TEST_DIR/root.lock"
flock -x 9
tb_approve "$id" false > "$TB_TEST_DIR/once.json"
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="authorized" and (.trusted|not)' "$TB_TEST_DIR/once.json" >/dev/null
;;
*) exit 64 ;;
esac
@@ -0,0 +1,185 @@
"""Only the hardware fixture is Python: upstream Bolt uses UMockdev/GObject.
All Omarchy behavior and assertions run in bolt-exercise.sh against real boltd,
real busctl, a private system bus, and disposable sysfs/firmware/store state.
"""
import importlib.machinery
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import time
import unittest
root = Path(sys.argv.pop(1))
loader = importlib.machinery.SourceFileLoader('bolt_upstream', str(Path(os.environ['BOLT_TEST_SOURCE']) / 'tests/test-integration'))
spec = importlib.util.spec_from_loader(loader.name, loader)
u = importlib.util.module_from_spec(spec)
loader.exec_module(u)
class ApprovalIntegration(u.BoltTest):
def exercise(self, security, numeric=False):
self.user_config(AuthMode='disabled', DefaultPolicy='manual')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
_, other = self.add_device(host, 2, 'Unknown', 'Example', authorized=0, key=None, boot='0')
if numeric:
# Linux omits these files when the peripheral has no descriptive names.
for attribute in ('device_name', 'vendor_name'):
Path(self.testbed.get_root_dir(), path.lstrip('/'), attribute).unlink()
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_KEYS': str(Path(self.dbpath, 'keys')), 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh')]
subprocess.run(command + ['approve'], env=env, check=True)
self.daemon_stop()
self.testbed.set_attribute(path, 'authorized', '0')
if security == 'secure':
self.testbed.set_attribute(path, 'key', '\n')
self.daemon_start()
subprocess.run(command + ['restart'], env=env, check=True)
# The real Bash daemon publishes a fresh readable snapshot and reconnects
# to Bolt after a restart. No custom Omarchy D-Bus server is involved.
child = subprocess.Popen(command + ['daemon'], env=env)
try:
for _ in range(100):
result = subprocess.run(command + ['status'], env=env, capture_output=True)
if result.returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish a valid snapshot')
subprocess.run(command + ['once'], env=env, check=True)
self.daemon_stop()
self.daemon_start()
for _ in range(100):
if subprocess.run(command + ['status'], env=env, capture_output=True).returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish the new Bolt generation')
finally:
child.terminate()
child.wait(timeout=15)
self.daemon_stop()
def test_user_approval(self):
self.exercise('user')
def test_secure_approval(self):
self.exercise('secure')
def test_numeric_identity(self):
self.exercise('user', numeric=True)
def removal(self, security, boot=False, disconnected=False):
self.user_config(AuthMode='disabled')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
other_path, other = self.add_device(host, 2, 'Existing', 'Example', authorized=0, key=None, boot='0')
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_STORE': self.dbpath, 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-removal.sh')]
service = 'omarchy-thunderbolt-authorization.service'
child = None
def operation(action, unit):
nonlocal child
if unit == 'bolt.service':
if action == 'stop':
self.daemon_stop()
elif self.daemon is None:
self.daemon_start()
elif unit == service:
if action == 'stop' and child is not None:
child.terminate()
child.wait(timeout=15)
child = None
elif action == 'start' and child is None:
child = subprocess.Popen(['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh'), 'daemon'], env=env)
else:
raise RuntimeError('Unexpected private service: ' + unit)
marker = Path(self.rundir, unit + '.active')
if action == 'start':
marker.touch()
else:
marker.unlink(missing_ok=True)
def run(action):
process = subprocess.Popen(command + [action], env=env)
control = Path(self.rundir, 'control')
deadline = time.monotonic() + 90
try:
while process.poll() is None:
if time.monotonic() > deadline:
raise RuntimeError('Private setup timed out: ' + action)
if control.exists():
operation(*control.read_text().strip().split())
control.unlink()
time.sleep(0.05)
if process.returncode:
raise subprocess.CalledProcessError(process.returncode, command + [action])
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=15)
def wait_for(action):
for _ in range(50):
result = subprocess.run(command + [action], env=env, capture_output=True, text=True)
if result.returncode == 0:
print(result.stdout, end='', flush=True)
return
time.sleep(0.1)
raise RuntimeError(action + ': ' + result.stdout + result.stderr)
def unplug():
for device_path in (path, other_path):
self.testbed.uevent(device_path, 'remove')
self.testbed.remove_device(device_path)
wait_for('disconnected')
try:
Path(self.rundir, 'bolt.service.active').touch()
run('approve')
operation('start', service)
Path(self.rundir, service + '.enabled').touch()
if boot:
run('boot-enable')
run('rollback')
if disconnected:
unplug()
run('disable')
if not disconnected:
unplug()
for number, device_uid, name in ((1, uid, 'Dock'), (2, other, 'Existing')):
attributes = ['device_name', name, 'device', '0x23', 'vendor_name', 'Example',
'vendor', '0x23', 'authorized', '0', 'unique_id', device_uid, 'boot', '0']
if number == 1 and security == 'secure':
attributes += ['key', '\n']
self.testbed.add_device('thunderbolt', '0-' + str(number), host, attributes, ['DEVTYPE', 'thunderbolt_device'])
wait_for('reconnected')
finally:
operation('stop', service)
self.daemon_stop()
def test_user_removal(self):
self.removal('user')
def test_secure_removal(self):
self.removal('secure', boot=True, disconnected=True)
names = sys.argv[1:] or ('test_user_approval', 'test_secure_approval', 'test_numeric_identity', 'test_user_removal', 'test_secure_removal')
suite = unittest.TestSuite(ApprovalIntegration(name) for name in names)
result = unittest.TextTestRunner(verbosity=2).run(suite)
sys.exit(not result.wasSuccessful())
@@ -0,0 +1,95 @@
#!/bin/bash
# Real Bolt policy/approval/setup calls. Only systemd is bridged to the private
# daemon processes owned by bolt-integration.py; never contact host systemd.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json" TB_CONFIG="$TB_TEST_STORE/boltd.conf"
TB_MARKER="$TB_TEST_DIR/enabled" TB_PENDING="$TB_TEST_DIR/pending"
TB_RUNTIME="$TB_TEST_DIR/runtime" TB_LOCK="$TB_TEST_DIR/root.lock"
mkdir -p "$TB_RUNTIME"
service_request() {
printf '%s\n' "$*" > "$TB_TEST_DIR/control.tmp"
mv "$TB_TEST_DIR/control.tmp" "$TB_TEST_DIR/control"
for (( attempt=0; attempt<300; attempt++ )); do
[[ -f $TB_TEST_DIR/control ]] || return 0
sleep 0.05
done
echo "Private service operation timed out: $*" >&2
return 1
}
systemctl() {
local action=$1 unit
shift
printf '%s\n' "$action $*" >> "$TB_TEST_DIR/services.log"
case "$action" in
show)
unit=${!#}
if [[ -f $TB_TEST_DIR/$unit.active ]]; then echo ActiveState=active; else echo ActiveState=inactive; fi
if [[ -f $TB_TEST_DIR/$unit.enabled ]]; then echo UnitFileState=enabled; else echo UnitFileState=disabled; fi ;;
start|stop)
for unit in "$@"; do service_request "$action" "$unit" || return 1; done ;;
enable)
unit=${!#}
touch "$TB_TEST_DIR/$unit.enabled"
if [[ $1 == "--now" ]]; then service_request start "$unit"; fi ;;
disable)
if [[ -f $TB_TEST_DIR/fail-disable ]]; then rm "$TB_TEST_DIR/fail-disable"; return 1; fi
rm -f -- "$TB_TEST_DIR/$1.enabled" ;;
daemon-reload) ;;
*) echo "Unexpected systemctl action: $action $*" >&2; return 1 ;;
esac
}
check_inventory() { tb_inventory | jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" "$1" >/dev/null; }
case "$1" in
approve)
jq -cn '{version:1,enabled:true,trusted:{},original_authmode:"enabled"}' > "$TB_STATE"
echo removal-test > "$TB_RUNTIME/generation"
touch "$TB_MARKER"
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_UID" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
# An existing manual record, even explicitly trusted through Omarchy,
# must keep the policy its administrator chose before enrollment.
inventory=$(tb_inventory)
owner=$(jq -r .owner <<< "$inventory")
path=$(jq -r --arg uid "$TB_TEST_OTHER" '.devices[]|select(.Uid==$uid)|.path' <<< "$inventory")
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$TB_TEST_OTHER" manual '' >/dev/null
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_OTHER" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" \
'.enrolled[$uid].StoreTime>0 and .enrolled[$other]==null and .trusted[$other]!=null' "$TB_STATE" >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
sha256sum "$TB_TEST_STORE/keys/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi ;;
boot-enable) tb_admin boot-enable ;;
rollback)
cp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
tb_inventory | tb_boot_saved > "$TB_TEST_DIR/old-firmware.json"
touch "$TB_TEST_DIR/fail-disable"
if tb_admin disable; then echo 'Failed removal unexpectedly succeeded' >&2; exit 1; fi
cmp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
cmp <(tb_inventory | tb_boot_saved | jq -cS .) <(jq -cS . "$TB_TEST_DIR/old-firmware.json")
[[ -f $TB_MARKER && -f $TB_TEST_DIR/$TB_SERVICE.active && -f $TB_TEST_DIR/$TB_SERVICE.enabled && ! -f $TB_TEST_DIR/setup-recovery.json ]]
check_inventory '.manager.AuthMode=="disabled"'
echo 'Removal failure restored policy, firmware ACLs and approval service' ;;
disable)
tb_admin disable
[[ ! -f $TB_MARKER && ! -f $TB_TEST_DIR/$TB_SERVICE.active && ! -f $TB_TEST_DIR/$TB_SERVICE.enabled ]]
jq -e '.enabled==false and .enrolled==null' "$TB_STATE" >/dev/null
check_inventory '.manager.AuthMode=="enabled" and any(.stored[]; .Uid==$uid and .Policy=="auto") and any(.stored[]; .Uid==$other and .Policy=="manual")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then sha256sum --status -c "$TB_TEST_DIR/key-checksum"; fi
echo 'Removal handed our record back to Bolt and preserved existing manual policy' ;;
disconnected)
check_inventory 'any(.stored[]; .Uid==$uid and .Status=="disconnected") and any(.stored[]; .Uid==$other and .Status=="disconnected")' ;;
reconnected)
check_inventory 'any(.devices[]; .Uid==$uid and .Policy=="auto" and (.Status|startswith("authorized"))) and any(.devices[]; .Uid==$other and .Policy=="manual" and .Status=="connected")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then
check_inventory 'any(.devices[]; .Uid==$uid and .Key=="have" and (.AuthFlags|contains("secure")))'
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
echo "Automatic reconnect succeeded in $TB_TEST_SECURITY mode; pre-existing manual device stayed blocked" ;;
*) exit 64 ;;
esac
@@ -0,0 +1,640 @@
#!/bin/bash
# These behavior cases were first run as the existing Python assertions against
# the Bash implementation. Fixtures replace only Bolt, systemd and desktop I/O.
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
source "$ROOT/install/helpers/thunderbolt-review.sh"
fixture() {
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
export HOME="$T/home"
TB_STATE="$T/policy.json" TB_CONFIG="$T/boltd.conf" TB_MARKER="$T/enabled"
TB_PENDING="$T/pending" TB_SYSFS="$T/sysfs" TB_KEYS="$T/keys"
TB_RUNTIME="$T/runtime" TB_LOCK="$T/root.lock"
mkdir -p "$TB_RUNTIME" "$HOME" "$TB_SYSFS" "$TB_KEYS"
echo generation-a > "$TB_RUNTIME/generation"
echo '{"version":1,"enabled":true,"trusted":{},"original_authmode":"enabled"}' > "$TB_STATE"
cat > "$T/inventory" <<'JSON'
{"owner":":1.5","manager":{"AuthMode":"disabled"},"domains":[{"Uid":"host","SecurityLevel":"user","IOMMU":true,"SysfsPath":"/sys/domain","path":"/domain","BootACL":["device-1",""]}],"devices":[{"Uid":"device-1","Name":"Dock","Vendor":"Vendor","Parent":"host","SysfsPath":"/sys/devices/test/0-1","ConnectTime":1,"Status":"connected","Stored":false,"Policy":"default","AuthFlags":"","path":"/devices/1","inode":"1"}]}
JSON
: > "$T/operations"; : > "$T/units"; : > "$T/notifications"
echo '{}' > "$T/capture"
tb_requests
}
edit_inventory() { jq "$@" "$T/inventory" > "$T/next"; mv "$T/next" "$T/inventory"; }
edit_state() { jq "$@" "$TB_STATE" | tb_json_write "$TB_STATE"; }
check() { jq -e "$1" "$2" >/dev/null || fail "$1" "$(cat "$2")"; }
reject() { if ("$@") > "$T/output" 2> "$T/error"; then fail "unexpected success: $*"; fi; }
identity() { tb_snapshot | jq -c '.devices[0].identity'; }
tb_inventory() {
[[ ! -f $T/inventory-failure ]] || { tb_fail 'inventory unavailable'; return 1; }
jq '. + {stored:[.devices[] | select(.Stored)]}' "$T/inventory"
}
tb_properties() { jq -c --arg path "$2" '(.devices + .domains)[] | select(.path==$path)' "$T/inventory"; }
tb_bus() {
local command=$1 path=$3 member=$5 value
printf '%s\n' "$member" >> "$T/operations"
case "$member" in
Authorize)
[[ ! -f $T/authorize-failure ]] || return 1
if [[ ! -f $T/authorize-noop ]]; then edit_inventory --arg path "$path" '(.devices[] | select(.path==$path)).Status="authorized"'; fi ;;
EnrollDevice)
if [[ ! -f $T/enroll-noop ]]; then
edit_inventory '(.devices[0] | .Stored=true | .Policy="manual" | .StoreTime=100) as $d | .devices[0]=$d'
if [[ -f $TB_SYSFS/0-1/key && $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ && ! -f $T/key-store-failure ]]; then
edit_inventory '.devices[0].Key="have"'
fi
fi
if [[ -f $T/enroll-lost-reply ]]; then rm "$T/enroll-lost-reply"; return 1; fi ;;
Policy|BootACL)
if [[ $member == "BootACL" && -f $T/boot-failure ]] || [[ $member == "Policy" && ${7:-} == "auto" && -f $T/restore-failure ]]; then
tb_fail 'firmware rejected write'; return 1
fi
[[ $member != "Policy" || ! -f $T/policy-noop ]] || return 0
if [[ $member == "BootACL" ]]; then
shift 7
value=$(jq -cn '$ARGS.positional' --args "$@")
else
value=$(jq -cn --arg value "$7" '$value')
fi
edit_inventory --arg path "$path" --arg key "$member" --argjson value "$value" \
'((.devices[],.domains[]) | select(.path==$path))[$key]=$value'
if [[ $member == "Policy" ]]; then
# Real Bolt changes firmware ACLs when a stored policy changes.
edit_inventory --arg path "$path" --arg value "$7" '
(.devices[] | select(.path==$path) | .Uid) as $uid |
.domains[].BootACL |= (if $value=="auto" and (index($uid)==null) then
(index("") // 0) as $slot | .[$slot]=$uid
elif $value!="auto" then map(if .==$uid then "" else . end) else . end)'
fi ;;
*) tb_fail "Unexpected Bolt call: $*"; return 1 ;;
esac
}
tb_capture() { [[ ! -f $T/capture-failure ]] && cat "$T/capture"; }
tb_unit_state() { jq -cn --arg unit "$1" '{active:($unit=="bolt.service"),enabled:false}'; }
systemctl() {
if [[ $1 == --root=* ]]; then command systemctl "$@"; return; fi
printf '%s\n' "$*" >> "$T/units"
if [[ -f $T/unit-failure && $* == "$(cat "$T/unit-failure")" ]]; then
[[ -f $T/repeat-failure ]] || rm "$T/unit-failure"
tb_fail 'systemd failed'; return 1
fi
if [[ $* == "start bolt.service" && -f $TB_CONFIG ]]; then
edit_inventory --arg mode "$(tb_config_authmode "$TB_CONFIG")" '.manager.AuthMode=$mode'
if [[ ! -f $TB_MARKER && -f $T/restart-policy-failure ]]; then
rm "$T/restart-policy-failure"
edit_inventory '.devices[0].Policy="manual"'
fi
fi
}
tb_public_snapshot() { cat "$T/public"; }
tb_notify() {
echo "$1" >> "$T/notifications"
if [[ -f $T/notify-failure ]]; then rm "$T/notify-failure"; return 1; fi
}
tb_attention() { echo attention >> "$T/notifications"; }
gum() { if [[ $1 == "choose" ]]; then cat "$T/choice"; fi; }
tb_request_apply() {
jq --argjson identity "$2" --argjson permanent "$1" \
'(.devices[] | select(.identity==$identity)) |= (.status="authorized" | .trusted=$permanent | .stored=$permanent)' "$T/public" > "$T/next"
mv "$T/next" "$T/public"
[[ ! -f $T/lost-reply ]] || { tb_fail 'reply lost'; return 1; }
cat "$T/public"
}
request() { local paths=("$TB_REQUESTS"/request-*.json); printf '%s\n' "${paths[0]}"; }
request_count() { local paths; shopt -s nullglob; paths=("$TB_REQUESTS"/request-*.json); echo "${#paths[@]}"; }
public_fixture() { tb_snapshot > "$T/public"; }
boot_fixture() { edit_inventory '.devices[0] |= (.Stored=true | .Policy="auto")'; }
review_tick() {
local path watch
path=$(request); watch=$(jq -r .watch "$path")
tb_scan "$watch" >/dev/null
}
test_iommu_is_not_consent() {
edit_inventory '.devices[0] |= (.Stored=true | .Policy="iommu")'
tb_reconcile > "$T/result"
[[ ! -s $T/operations ]]; check '.devices[0].trusted==false' "$T/result"
}
test_once_is_not_saved() {
tb_approve "$(identity)" false > "$T/result"
check '.warnings==[]' "$T/result"; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "Authorize" ]]
}
test_always_survives_restart() {
tb_approve "$(identity)" true >/dev/null
edit_inventory '.owner=":1.9" | .devices[0] |= (.Status="connected" | .inode="2")'
echo generation-b > "$TB_RUNTIME/generation"
tb_reconcile > "$T/result"
check '.devices[0].status=="authorized" and .devices[0].trusted' "$T/result"
check '.manager.AuthMode=="disabled"' "$T/inventory"
}
test_enrollment_lost_reply_retains_ownership() {
touch "$T/enroll-lost-reply"
reject tb_approve "$(identity)" true
check '.trusted=={} and .enrolled["device-1"].StoreTime==null' "$TB_STATE"
tb_approve "$(identity)" true >/dev/null
check '.enrolled["device-1"].StoreTime==100 and .trusted["device-1"]!=null' "$TB_STATE"
}
secure_fixture() {
mkdir -p "$TB_SYSFS/0-1"
echo 11111111-2222-3333-4444-555555555555 > "$TB_SYSFS/0-1/unique_id"
echo 0 > "$TB_SYSFS/0-1/authorized"
: > "$TB_SYSFS/0-1/key"
edit_inventory --arg path "$TB_SYSFS/0-1" --arg inode "$(stat -Lc %i "$TB_SYSFS/0-1")" \
'.domains[0].SecurityLevel="secure" | .devices[0] |= (.Secure=true | .Key="missing" |
.Uid="11111111-2222-3333-4444-555555555555" | .SysfsPath=$path | .inode=$inode)'
}
test_secure_approval_initializes_before_trust() {
secure_fixture
tb_approve "$(identity)" true true > "$T/result"
[[ $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ ]]
check '.devices[0].trusted and .devices[0].stored' "$T/result"
cp "$TB_SYSFS/0-1/key" "$T/original-key"
tb_approve "$(identity)" true true >/dev/null
cmp "$TB_SYSFS/0-1/key" "$T/original-key"
}
test_secure_automatic_restore_cannot_initialize_keys() {
secure_fixture
jq '.devices[0] | {(.Uid):{Uid,Name,Vendor}}' "$T/inventory" > "$T/trusted"
edit_state --slurpfile trusted "$T/trusted" '.trusted=$trusted[0]'
tb_reconcile > "$T/result"
check '.devices[0].trusted==false' "$T/result"
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
reject tb_approve "$(identity)" true
[[ ! -s $T/operations ]]
}
test_secure_missing_saved_key_cannot_claim_success() {
secure_fixture
touch "$T/key-store-failure"
reject tb_approve "$(identity)" true true
check '.trusted=={}' "$TB_STATE"
tb_snapshot > "$T/result"; check '.devices[0].trusted==false' "$T/result"
}
test_secure_key_failure_does_not_authorize() {
secure_fixture
openssl() { return 1; }
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
unset -f openssl
echo invalid > "$TB_SYSFS/0-1/key"
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations ]]
}
test_authorized_keyless_device_requires_explicit_reconnect() {
secure_fixture
edit_inventory '.devices[0].Status="authorized"'
echo 1 > "$TB_SYSFS/0-1/authorized"
reject tb_approve "$(identity)" true true
rg -q 'disconnect, reconnect' "$T/error"
[[ ! -s $TB_SYSFS/0-1/key ]]; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "EnrollDevice" ]]
}
test_stale_identity_rejected() {
local id field
id=$(identity)
for field in inode Name ConnectTime; do
cp "$T/inventory" "$T/original"
edit_inventory --arg field "$field" '.devices[0][$field]="replacement"'
reject tb_approve "$id" true
mv "$T/original" "$T/inventory"
done
edit_inventory '.owner=":1.9"'; reject tb_approve "$id" true
[[ ! -s $T/operations ]]
}
test_false_success_rejected() {
local id; id=$(identity)
touch "$T/authorize-noop"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/authorize-noop"; touch "$T/enroll-noop"
reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/enroll-noop"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 2 ]]
}
test_write_failure_retains_retry() {
local id; id=$(identity)
eval "$(declare -f tb_json_write | sed '1s/tb_json_write/disk_write/')"
tb_json_write() { [[ ! -f $T/disk-full ]] && disk_write "$@"; }
touch "$T/disk-full"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/disk-full"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 1 ]]
}
test_firmware_bypass_warns() {
edit_inventory '.domains[0].SecurityLevel="none" | .devices[0] |= (.Status="authorized" | .AuthFlags="boot")'
tb_snapshot > "$T/result"; check '.warnings|length==2' "$T/result"; [[ ! -s $T/operations ]]
}
test_global_enable_not_protected() {
edit_inventory '.manager.AuthMode="enabled"'
reject tb_snapshot; rg -q 'not active' "$T/error"
reject tb_reconcile
}
test_display_only_needs_no_approval() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
edit_inventory '.devices[0].AuthFlags="nopcie"'
tb_reconcile > "$T/result"; check '.error==""' "$T/result"; [[ ! -s $T/operations ]]
}
test_empty_capture_preserved() {
rm "$TB_STATE"; tb_prepare
touch "$T/capture-failure"; tb_prepare
check '.trusted=={}' "$TB_STATE"; [[ $(tb_config_authmode "$TB_CONFIG") == "disabled" ]]
}
test_capture_failure_does_not_publish() {
rm "$TB_STATE"; touch "$T/capture-failure"; reject tb_prepare
[[ ! -e $TB_STATE && ! -e $TB_MARKER ]]
}
test_capture_optional_names_match_numeric_identity() {
source "$ROOT/install/helpers/thunderbolt-setup.sh"
mkdir -p "$TB_SYSFS/0-1"
echo device-1 > "$TB_SYSFS/0-1/unique_id"
echo 0x1234 > "$TB_SYSFS/0-1/device"
echo 0xab > "$TB_SYSFS/0-1/vendor"
tb_capture > "$T/result"
check '.["device-1"]=={Uid:"device-1",Name:"0x1234",Vendor:"0xab"}' "$T/result"
echo Dock > "$TB_SYSFS/0-1/device_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="Dock" and .["device-1"].Vendor=="0xab"' "$T/result"
rm "$TB_SYSFS/0-1/device_name"; echo Vendor > "$TB_SYSFS/0-1/vendor_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="0x1234" and .["device-1"].Vendor=="Vendor"' "$T/result"
mkdir "$TB_SYSFS/0-1/device_name"
reject tb_capture
}
test_first_activation_defers_without_touching_bolt() {
secure_fixture; rm "$TB_STATE"
printf '[config]\nAuthMode=enabled\n' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_STATE && ! -f $TB_MARKER && ! -s $T/units ]]
cmp "$TB_CONFIG" "$T/original"
rm -rf "$TB_SYSFS/0-1"
tb_admin migrate
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
tb_admin enable
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_saved_secure_binding_allows_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_key_without_record_defers_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0].Key="have"'
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_offline_prepare_cannot_trust_installers_bolt() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin prepare
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_deferred_opt_out_retains_initial_enrollment() {
secure_fixture; rm "$TB_STATE"
tb_admin owner
tb_admin disable
[[ ! -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
check '.enabled==false and .enrollment_pending==true' "$TB_STATE"
tb_admin migrate
[[ ! -f $TB_PENDING && ! -s $T/units ]]
tb_admin enable
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
rm -rf "$TB_SYSFS/0-1"
echo '{"new-device":{}}' > "$T/capture"
tb_admin enable
check '.enabled and .enrollment_pending==null and .trusted=={"new-device":{}}' "$TB_STATE"
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_notice_retries_without_claiming_protection() {
touch "$TB_PENDING"
local count=0 notices=0
sleep() { count=$((count + 1)); (( count < 3 )) || exit 99; }
tb_setup_notice() { notices=$((notices + 1)); (( notices > 1 )) && echo pending >> "$T/notifications"; }
reject tb_watch
[[ $(cat "$T/notifications") == "pending" ]]
tb_status > "$T/result"; rg -q 'protection is deferred' "$T/result"
}
test_active_policy_is_not_deferred() {
secure_fixture; touch "$TB_MARKER"
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_only_factory_reset_reaches_both_roots() {
local function_name root="$T/top/@factory"
local TOP_MNT="$T/top" NEXT_NAME=@next PROVISIONING_DIR=/var/lib/omarchy/provisioning LOG_FILE="$T/reset.log" swap_done=0
mkdir -p "$root/etc/omarchy" "$root/etc/systemd/system" "$root/usr/bin" \
"$root/usr/share/omarchy/install/provisioning" "$T/top/@"
touch "$root/etc/omarchy/thunderbolt-authorization.pending" "$root/usr/bin/omarchy-provision-owner"
chmod +x "$root/usr/bin/omarchy-provision-owner"
echo 'root:x:0:0:root:/root:/bin/bash' > "$root/etc/passwd"
cp "$ROOT/install/provisioning/omarchy-provision-owner.service" \
"$ROOT/install/provisioning/omarchy-system-factory-reset-finish.service" "$root/usr/share/omarchy/install/provisioning/"
cp "$ROOT/etc/systemd/system/omarchy-thunderbolt-authorization.service" "$root/etc/systemd/system/"
for function_name in install_provisioning_units scrub_factory_accounts sanitize_factory_baseline stage_full_reset; do
eval "$(sed -n "/^$function_name() {$/,/^}/p" "$ROOT/bin/omarchy-system-factory-reset")"
done
log() { :; }
btrfs() {
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then cp -a "$3" "$4"
elif [[ $1 == "property" ]]; then :
else return 1; fi
}
usermod() { [[ $1 == "--root" && $2 == "$TOP_MNT/"* && $3 == "--password" && $4 == "!" && $5 == "root" ]]; }
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
sync() { :; }
systemctl() {
[[ ( $1 == "--root=$TOP_MNT/@next" || $1 == "--root=$TOP_MNT/@factory" ) && $2 == "disable" && $3 == "$TB_SERVICE" ]] || return 1
command systemctl "$@"
}
function /usr/bin/omarchy-thunderbolt-authorization-admin() {
[[ $1 == "reset-root" ]] || return 1
printf '%s\n' "$2" >> "$T/reset-roots"
(
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
tb_reset_root "$2"
)
}
stage_full_reset
[[ $(wc -l < "$T/reset-roots") == 2 && $swap_done == 1 ]]
[[ ! -f $TOP_MNT/@/etc/omarchy/thunderbolt-authorization.pending &&
! -f $TOP_MNT/@factory/etc/omarchy/thunderbolt-authorization.pending ]]
[[ -f $TOP_MNT/@/var/lib/omarchy/provisioning/pending ]]
}
test_config_preserves_settings() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
tb_prepare; rg -q '^DefaultPolicy=manual$' "$TB_CONFIG"
check '.original_authmode=="enabled"' "$TB_STATE"
}
test_fresh_owner_replaces_trust() {
rm "$TB_STATE"; echo '{"old":{}}' > "$T/capture"; tb_prepare
echo '{"new":{}}' > "$T/capture"; tb_prepare true
check '.trusted=={"new":{}} and .original_authmode=="enabled"' "$TB_STATE"
}
test_enable_rollback() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
cp "$TB_CONFIG" "$T/original"; echo 'start bolt.service' > "$T/unit-failure"
reject tb_transaction tb_enable
cmp "$TB_CONFIG" "$T/original"; [[ ! -e $TB_STATE && ! -e $TB_MARKER && ! -e $T/setup-recovery.json ]]
[[ $(tail -2 "$T/units") == $'start bolt.service\nstop omarchy-thunderbolt-authorization.service' ]]
}
test_failed_restore_keeps_checkpoint() {
rm "$TB_STATE"; echo '[config]' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
echo 'stop omarchy-thunderbolt-authorization.service bolt.service' > "$T/unit-failure"; touch "$T/repeat-failure"
reject tb_transaction tb_enable; rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]; reject tb_transaction tb_enable; rg -q 'needs recovery' "$T/error"
echo partial > "$TB_CONFIG"; rm "$T/unit-failure"
tb_config_restore; cmp "$TB_CONFIG" "$T/original"
}
test_disable_restores_matching_boot_state() {
boot_fixture; tb_prepare; tb_boot_change true; cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_transaction tb_disable
cmp "$TB_STATE" "$T/original"; [[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
}
test_disable_hands_off_only_our_records() {
tb_prepare
tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices += [(.devices[0] | .Uid="existing" | .path="/devices/2" | .Policy="manual")]'
tb_approve "$(tb_snapshot | jq -c '.devices[1].identity')" true >/dev/null
tb_admin disable
check '.enabled==false and .enrolled==null and (.trusted|length)==2' "$TB_STATE"
check '.manager.AuthMode=="enabled" and .devices[0].Policy=="auto" and .devices[1].Policy=="manual"' "$T/inventory"
[[ ! -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
# After a completed handoff, an administrator may choose manual themselves.
edit_inventory '.devices[0].Policy="manual"'
tb_admin enable
tb_admin disable
check 'all(.devices[]; .Policy=="manual")' "$T/inventory"
}
test_disable_hands_off_disconnected_device() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].Status="disconnected"'
tb_admin disable
check '.devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_preserves_recreated_manual_record() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].StoreTime=200'
tb_admin disable
check '.devices[0].Policy=="manual"' "$T/inventory"
}
test_disable_preserves_original_disabled_mode() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_state '.original_authmode="disabled"'
tb_admin disable
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_handoff_after_boot_protection() {
tb_prepare; tb_boot_change true
tb_approve "$(identity)" true >/dev/null
tb_admin disable
check '.enabled==false and .boot_protection==false' "$TB_STATE"
check '.devices[0].Policy=="auto" and (.domains[0].BootACL|index("device-1"))!=null' "$T/inventory"
}
test_disable_handoff_rolls_back_policies_and_acl() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].BootACL=["unrelated",""]'
cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_admin disable
cmp "$TB_STATE" "$T/original"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["unrelated",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_failure_keeps_protection() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
tb_unit_state() { echo '{"active":true,"enabled":true}'; }
touch "$T/restore-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
! rg -q '^disable ' "$T/units"
[[ $(tail -1 "$T/units") == "start omarchy-thunderbolt-authorization.service" ]]
}
test_disable_handoff_verifies_policy_writes() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/policy-noop"
reject tb_admin disable
rg -q 'did not retain the requested Policy' "$T/error"
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_verifies_policy_after_restart() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/restart-policy-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_incomplete_rollback_is_recoverable() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
touch "$T/boot-failure"
reject tb_admin disable
rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]
rm "$T/boot-failure"
tb_admin recover
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_offline_controller_is_unchanged() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].SysfsPath=""'
reject tb_admin disable
rg -q 'Reconnect every Thunderbolt controller' "$T/error"
check '.enabled' "$TB_STATE"
[[ ! -s $T/units && ! -f $T/setup-recovery.json ]]
}
test_boot_policy_reconnection_and_restore() {
boot_fixture; tb_boot_change true
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
check '.boot_protection and .trusted["device-1"]!=null' "$TB_STATE"
edit_inventory '.devices[0].Policy="auto" | .domains[0].BootACL=["device-1",""]'
tb_snapshot > "$T/result"; check '.warnings|length>0' "$T/result"
tb_reconcile > "$T/result"; check '.error==""' "$T/result"
check '.domains[0].BootACL==["",""]' "$T/inventory"
tb_boot_change false
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
}
test_boot_change_rollback() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure"
reject tb_boot_change true
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1","other"]' "$T/inventory"
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_boot_recovery_retained() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure" "$T/restore-failure"
reject tb_boot_change true; rg -q 'recovery is incomplete' "$T/error"; [[ -f $T/boot-recovery.json ]]
rm "$T/boot-failure" "$T/restore-failure"; tb_boot_recover
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_unsupported_boot_cannot_succeed() {
boot_fixture; cp "$T/inventory" "$T/original"
local expression
for expression in '.SysfsPath=""' '.SecurityLevel="none"' '.BootACL=[]'; do
cp "$T/original" "$T/inventory"; edit_inventory ".domains[0] |= ($expression)"
reject tb_boot_change true
done
[[ ! -s $T/operations ]]
}
test_install_defers_owner() {
install() { :; }
function /usr/bin/omarchy-thunderbolt-authorization-admin() { echo "helper $*"; }
export OMARCHY_PATH=$ROOT OMARCHY_INSTALL_USER=''
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
! rg -q helper "$T/output"; [[ ! -s $T/units ]]
OMARCHY_INSTALL_USER=lab
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
rg -q 'helper prepare' "$T/output"; rg -q '^enable omarchy-thunderbolt-authorization.service$' "$T/units"
}
test_offline_reset_scrubs_private_state() {
source "$ROOT/install/helpers/thunderbolt-policy.sh"
local root="$T/clone" unit="$TB_SERVICE"
mkdir -p "$root/etc/systemd/system" "$root${TB_STATE%/*}" "$root${TB_MARKER%/*}"
cp "$ROOT/etc/systemd/system/$unit" "$root/etc/systemd/system/"
command systemctl --root="$root" enable "$unit" >/dev/null 2>&1
touch "$root$TB_MARKER" "$root$TB_STATE" "$root${TB_STATE%/*}/setup-recovery.json"
local kind
for kind in devices keys domains; do mkdir -p "$root/var/lib/boltd/$kind"; touch "$root/var/lib/boltd/$kind/old"; done
tb_reset_root "$root"
[[ ! -e $root$TB_MARKER && ! -e $root${TB_STATE%/*} && ! -e $root/etc/systemd/system/multi-user.target.wants/$unit ]]
[[ $(ls "$root/var/lib/boltd") == "boltd.conf" && $(tb_config_authmode "$root/var/lib/boltd/boltd.conf") == "enabled" ]]
}
test_live_reset_refused() { reject tb_reset_root /; }
test_trusted_reconnect_no_alert() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
public_fixture; tb_scan watch1 >/dev/null; [[ ! -s $T/notifications ]]
}
test_unavailable_warns_once() {
tb_scan() { return 1; }
sleep() { count=$((count+1)); ((count<4)) || exit 99; }
local count=0
reject tb_watch
[[ $(cat "$T/notifications") == "attention" ]]
}
test_failed_delivery_retries_other_devices() {
edit_inventory '.devices += [(.devices[0] | .Uid="device-2")]'; public_fixture; touch "$T/notify-failure"
tb_scan watch1 >/dev/null; [[ $(request_count) == 1 ]]
tb_scan watch1 >/dev/null; [[ $(request_count) == 2 && $(wc -l < "$T/notifications") == 3 ]]
}
test_restarts_restore_requests() {
public_fixture; tb_scan watch1 >/dev/null; local old; old=$(request)
tb_scan watch1 >/dev/null; [[ $(wc -l < "$T/notifications") == 1 ]]
edit_inventory '.owner=":1.9"'; public_fixture
tb_scan watch1 >/dev/null; [[ $(request) != "$old" ]]
tb_scan watch2 >/dev/null; [[ $(request_count) == 1 && $(wc -l < "$T/notifications") == 3 ]]
}
test_inventory_failure_preserves_request() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
rm "$T/public"; reject tb_scan watch1; [[ -f $path ]]
}
test_replacement_during_dialog_rejected() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
gum() {
if [[ $1 == "choose" ]]; then
jq '.devices[0].identity.Uid="replacement"' "$T/public" > "$T/next"; mv "$T/next" "$T/public"
echo 'Always allow this device'
fi
}
reject tb_review "${path##*/}" # A filename is not a valid token.
reject tb_review "$(basename "$path" .json)"
check '.devices[0].status=="connected"' "$T/public"
}
test_lost_reply_keeps_approval_intent() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
echo 'Always allow this device' > "$T/choice"; touch "$T/lost-reply"
sleep() { review_tick; }
reject tb_review "$(basename "$path" .json)"
check '.approval=="Always allow this device"' "$path"
tb_scan watch1 >/dev/null; [[ -f $path && $(wc -l < "$T/notifications") == 2 ]]
tb_scan watch2 >/dev/null; [[ ! -f $path && $(wc -l < "$T/notifications") == 3 ]]; path=$(request)
rm "$T/lost-reply"; gum() { fail 'Saved approval must not ask again'; }
tb_review "$(basename "$path" .json)" >/dev/null
[[ ! -f $path ]]
}
test_disconnect_and_untrusted_text() {
edit_inventory '.devices[0].Name="$(touch /tmp/untrusted)\u001b[2J"'
public_fixture; tb_scan watch1 >/dev/null
[[ $(basename "$(request)") =~ ^request-[0-9a-f]{64}.json$ ]]
[[ $(jq '.devices[0].Name' "$T/inventory" | tb_safe_text) != *$'\e'* ]]
edit_inventory '.devices=[]'; public_fixture; tb_scan watch1 >/dev/null; [[ $(request_count) == 0 ]]
}
test_snapshot_freshness_and_disabled_state() {
# Exercise the real public reader, including freshness after daemon failure.
source "$ROOT/install/helpers/thunderbolt-policy.sh"
TB_RUNTIME="$T/runtime"
jq -cn --argjson now "$(date +%s)" '{available:true,time:$now,generation:"generation-a"}' > "$TB_RUNTIME/snapshot.json"
tb_public_snapshot >/dev/null
jq '.time-=30' "$TB_RUNTIME/snapshot.json" > "$T/next"; mv "$T/next" "$TB_RUNTIME/snapshot.json"
reject tb_public_snapshot
echo generation-b > "$TB_RUNTIME/generation"; reject tb_public_snapshot
}
test_invalid_state_not_skipped_by_migration() {
echo '{"enabled":false}' > "$TB_STATE"; reject tb_admin migrate
[[ ! -s $T/units ]]
}
for case in $(declare -F | awk '$3 ~ /^test_/ {print $3}'); do
(fixture; "$case")
pass "Thunderbolt ${case#test_}"
done
@@ -0,0 +1,31 @@
#!/bin/bash
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
fixture=$(mktemp -d)
trap 'rm -rf -- "$fixture"' EXIT
for name in admin approve daemon; do
# Map the security library and stop before service work, including when the
# suite itself runs as root. Startup checks remain the production code.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-thunderbolt-authorization-$name" > "$fixture/entry"
chmod 755 "$fixture/entry"
printf 'set -p\n' > "$fixture/decoy"
if BASH_ENV="$fixture/decoy" /usr/bin/bash "$fixture/entry" -p; then
fail "$name accepts ordinary Bash with a decoy -p"
fi
printf 'touch "%s"\n' "$fixture/injected" > "$fixture/startup"
# The entrypoint must suppress startup files and exported functions before
# checking EUID or reaching the fixture's service-work boundary.
if BASH_ENV="$fixture/startup" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture service-work boundary"
fi
[[ ! -e $fixture/injected ]] || fail "$name executed BASH_ENV"
if /usr/bin/env 'BASH_FUNC_source%%=() { echo injected > "$TB_TEST_INJECTED"; }' \
TB_TEST_INJECTED="$fixture/injected" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture boundary with an exported function"
fi
[[ ! -e $fixture/injected ]] || fail "$name imported an exported function"
pass "Thunderbolt $name rejects unsafe Bash startup"
done
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/policy-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/startup-test.sh"
if [[ -n ${BOLT_TEST_SOURCE:-} ]]; then
/usr/bin/python3 "$ROOT/test/shell.d/fixtures/thunderbolt/bolt-integration.py" "$ROOT"
else
skip "real boltd integration needs BOLT_TEST_SOURCE and an UMockdev environment"
fi