Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,47 @@ Loopback binding prevents devices on Wi-Fi, Ethernet, or the wider LAN from
connecting. It does not isolate the listener from other users or processes on
the same Mac; guest SSH authentication is still required.

### Touch ID for sudo

The native authentication bridge can enroll this Mac and use
Touch ID as a sufficient authentication method for guest `sudo`. Open
**Omarchy Menu → Setup → Security → Touch ID for sudo**, or run:

```sh
try-omarchy-touch-id
```

The integration ships disabled. Enabling first requires the normal guest sudo
password, then Touch ID creates and proves possession of a Secure Enclave
signing key. Only after that succeeds is the narrowly scoped sudo PAM rule
installed. The menu then offers Test, Re-pair, and Disable actions.

The Mac stores only the Secure Enclave's device-bound encrypted key
representation. Every later approval is signed over a root-private guest ID,
fresh challenge, the sudo user and requesting user, the interactive TTY, and a
15-second validity window. Each enrolled guest has a distinct host signing key.
The QEMU window must be frontmost. Cancellation, invalid responses, missing
enrollment, and unavailable Touch ID all fall back to the normal guest password;
no login or screen-unlock PAM policy is changed.

If Touch ID falls back, sudo displays the reason before asking for the guest
password. Signed approvals require synchronized Mac and guest clocks; factory
images enable `systemd-timesyncd` at boot. On an existing guest with clock drift,
run `sudo systemctl enable --now systemd-timesyncd.service`, then check
`timedatectl` for `System clock synchronized: yes` before retrying.

The Touch ID test refuses guest-password fallback and returns failure if sudo
cannot authenticate. A passwordless sudo policy can also satisfy this check;
the result only demonstrates Touch ID when its prompt appeared. Unanswered Mac
prompts are canceled after 55 seconds, before the guest's 65-second timeout.
Late responses are discarded without extending the current request's deadline.

Enrollment persists across guest and Mac restarts for the same persistent VM,
Mac, and macOS account. Factory Reset, moving the VM to another Mac or account,
or changing the enrolled Touch ID fingerprint set requires re-pairing. Disabling
removes the guest enrollment and, while the host bridge is available, its wrapped
Secure Enclave key representation.

## Requirements

- Apple Silicon Mac (`arm64`)
Expand Down
30 changes: 30 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,36 @@ is reading the camera. Camera permission, capture failure, or device removal is
non-fatal to the VM; the launcher can restart the optional bridge without
restarting Omarchy.

A root-only authentication port
(`dev.tryomarchy.authentication`) lets the guest's `sudo` PAM policy request a
fixed-purpose macOS Touch ID prompt. Enrollment creates a non-exportable P-256
signing key in the Mac's Secure Enclave for a root-private random guest ID and
pins its public key inside that guest. The host stores the Secure Enclave's
device-bound encrypted key representation outside the Keychain, so local ad-hoc
test builds do not need a provisioned Keychain access group. Each authentication uses a new 256-bit
challenge. The host signs a
canonical payload that binds the request ID, challenge, PAM user, requesting
user, `sudo` service, interactive TTY, guest ID, signing-key ID, and a 15-second validity
window. The guest verifies that signature with OpenSSL before PAM can return
success. It never accepts an unsigned approval boolean.

The integration's binaries and root-only device rule are present in the factory
image, but the sudo PAM policy remains unchanged until the user opts in through
**Setup → Security → Touch ID for sudo**. The root control enrolls first and
atomically adds the PAM rule only after successful guest-password and Touch ID
authentication. Disable removes that exact rule before deleting guest state and
requesting deletion of the corresponding host key representation. Re-pair runs
the disable and enable transitions while preserving password fallback.

The QEMU window must be frontmost, the QEMU process identity must still match,
and the host owns both enrollment and sudo prompt text. When enabled, the PAM
module is `sufficient`: a denial, missing enrollment, unavailable bridge,
invalid signature, non-interactive request, or timeout falls through to
Omarchy's normal password authentication. This integration does not authenticate
login or screen-unlock flows, cannot bind approval to the exact sudo command
because PAM does not expose it, and is not a general guest-to-host approval
service.

When a folder is chosen on the start menu, QEMU exports it over virtio-9p with
`security_model=none`, so every host file operation runs as the Mac user and
the Mac keeps real modes and ownership. A small QEMU patch adds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SUBSYSTEM=="virtio-ports", ATTR{name}=="dev.tryomarchy.authentication", OWNER="root", GROUP="root", MODE="0600"
107 changes: 107 additions & 0 deletions guest/native-overlay/usr/local/bin/try-omarchy-touch-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/bin/bash

set -euo pipefail

PAM_LINE=$'auth\t\tsufficient\tpam_exec.so quiet seteuid /usr/local/lib/try-omarchy/native-authentication-broker pam'
CURRENT_PAM_LINE=$'auth\t\tsufficient\tpam_exec.so quiet seteuid stdout /usr/local/lib/try-omarchy/native-authentication-broker pam'
CONTROL=/usr/local/sbin/try-omarchy-touch-id-control

enabled() {
grep -Fxq -e "$PAM_LINE" -e "$CURRENT_PAM_LINE" /etc/pam.d/sudo 2>/dev/null
}

confirm() {
gum confirm "$1"
}

refresh_menu() {
omarchy menu refresh >/dev/null 2>&1 || true
}

enable_touch_id() {
if enabled; then
echo "Touch ID is already enabled for sudo."
return
fi
printf '%s\n' \
"Touch ID will approve sudo only while this Try Omarchy window is focused." \
"Your guest password remains available as fallback." \
"" \
"Enabling requires your guest sudo password, followed by Touch ID on this Mac."
confirm "Enable Touch ID for sudo?" || exit 130
sudo -k
sudo "$CONTROL" enable
refresh_menu
try-omarchy-touch-id-test
echo "Touch ID for sudo is enabled; the password-free sudo check passed."
}

disable_touch_id() {
if ! enabled; then
echo "Touch ID is already disabled for sudo."
return
fi
confirm "Disable Touch ID for sudo?" || exit 130
sudo "$CONTROL" disable
refresh_menu
}

repair_touch_id() {
printf '%s\n' \
"Re-pairing removes the current guest enrollment and creates a new" \
"Touch ID-protected key for this guest."
confirm "Re-pair Touch ID for sudo?" || exit 130
sudo -k
sudo "$CONTROL" repair
refresh_menu
try-omarchy-touch-id-test
echo "Touch ID for sudo was re-paired; the password-free sudo check passed."
}

manage_touch_id() {
if ! enabled; then
enable_touch_id
return
fi
local choice
choice=$(printf '%s\n' "Test Touch ID" "Re-pair Touch ID" "Disable Touch ID" | \
gum choose --header "Touch ID for sudo is enabled") || exit 130
case "$choice" in
"Test Touch ID")
exec try-omarchy-touch-id-test
;;
"Re-pair Touch ID")
repair_touch_id
;;
"Disable Touch ID")
disable_touch_id
;;
esac
}

case "${1:-manage}" in
manage)
manage_touch_id
;;
enable)
enable_touch_id
;;
disable)
disable_touch_id
;;
repair)
repair_touch_id
;;
status)
if enabled; then
[[ ${2:-} == --quiet ]] || echo "enabled"
exit 0
fi
[[ ${2:-} == --quiet ]] || echo "disabled"
exit 1
;;
*)
echo "Usage: try-omarchy-touch-id [manage|enable|disable|repair|status [--quiet]]" >&2
exit 64
;;
esac
11 changes: 11 additions & 0 deletions guest/native-overlay/usr/local/bin/try-omarchy-touch-id-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh

set -eu

sudo -k
if SUDO_ASKPASS=/bin/false sudo -A true; then
echo "sudo authenticated without a guest password. Touch ID was used if its prompt appeared."
else
echo "Touch ID sudo check failed; password fallback was not accepted." >&2
exit 1
fi
Loading
Loading