Files
omarchy/test/shell.d/luks-rekey-journal-test.sh
T
Ryan Hughes defbf90e89 Fully revert USB and Thunderbolt device approval (#15091)
* Revert "Merge pull request #11874 from acrogenesis/feature/usb-device-authorization"

This reverts commit 5cb31317b4, reversing
changes made to 9d48fc402e.

* Undo USB and Thunderbolt device approval on machines that ran it

* Verify boot rebuilds, recover Thunderbolt journals and prove USBGuard ownership in the removal migration

* Read the boot partition from ESP_PATH and stop on failed Bolt reads in the removal migration

* Recover Bolt's original mode from the setup journal in the removal migration
2026-10-12 01:20:01 -04:00

898 lines
41 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Owner provisioning's LUKS re-key, killed after every durable step and rerun.
# Each attempt is its own process, like a reboot: omarchy-provision-owner's
# setup attempt from the accepted owner form on, the shared re-key, and a
# cryptsetup that is either a slot-table fake or the real binary on a
# file-backed LUKS2 or LUKS1 volume. The callbacks reach the platform through
# the real omarchy-lifecycle-dispatch: on an x86 fixture it is a no-op and the
# Limine UKI path runs; on an Apple fixture a fake boot package with
# provisioning and luks-slots entrypoints owns an unlock kept on a boot
# partition and the kernel command line and records the kept slot.
tmp=$(mktemp -d)
token_key=""
trap 'rm -rf "$tmp"; [[ -z $token_key ]] || keyctl unlink "$token_key" @s >/dev/null 2>&1 || true' EXIT
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
platform=x86
# Root's dispatcher ignores the fixtures and sees this machine, which stands in
# for x86 only where no boot package is registered.
platforms=(x86 apple)
if (( EUID == 0 )); then
if [[ $("$ROOT/bin/omarchy-hw-platform") == "aarch64-apple" ]]; then
skip "running as root on Apple Silicon, where dispatch ignores fixtures; skipping"
exit 0
fi
platforms=(x86)
skip "running as root, where dispatch ignores fixtures; skipping the Apple runs"
fi
runtime=$ROOT
mac_boot=$tmp/lifecycle/usr/lib/omarchy/mac-boot
mkdir -p "$mac_boot"
cat >"$tmp/mac-boot-lib.sh" <<'SH'
ran() { echo "$1" >>"$TMP/mac-boot-ran"; }
crash() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == $(cat "$TMP/crash-at") )); then
kill -9 "$PPID"
kill -9 $$
fi
}
SH
mac_boot_entrypoint() {
{ printf '#!/bin/bash\nset -euo pipefail\nTMP=%q\nsource "$TMP/mac-boot-lib.sh"\nran %s\n' "$tmp" "$1"; cat; } >"$mac_boot/$1"
}
mac_boot_entrypoint provision-prepare <<'SH'
if [[ -e $TMP/prepare-fail ]]; then
echo "The boot partition is not mounted." >&2
exit 1
fi
SH
mac_boot_entrypoint provision-commit <<'SH'
[[ ! -e $TMP/rebuild-fail ]] || exit 1
rm -f "$TMP/boot/omarchy/luks-key"
crash "boot-partition key removed"
sed -i 's/ rd\.luks\.key=[^" ]*//' "$TMP/etc/default/grub"
echo rebuild >>"$TMP/rebuilds"
crash "boot rebuilt"
SH
mac_boot_entrypoint provision-verify <<'SH'
[[ ! -e $TMP/boot/omarchy/luks-key ]] && ! grep -q 'rd\.luks\.key=' "$TMP/etc/default/grub"
SH
mac_boot_entrypoint luks-slots <<'SH'
[[ ! -e $TMP/record-fail ]] || exit 1
printf '%s\n' "$@" >"$TMP/slot-record"
crash "slots recorded"
SH
chmod 755 "$mac_boot"/*
chmod -R go-w "$tmp/lifecycle"
staged_key=staged-install-key
seller_key=previous-owner-key
owner_password=owner-password
sed -n '/^PROVISIONING_UNLOCK_FILES=(/,/^)/p; /^UNLOCK_OWNER=/p; /^limine_auto_unlock_present() {/,/^}/p; /^limine_auto_unlock_drop() {/,/^}/p
/^unlock_owner() {/,/^}/p; /^luks_auto_unlock_present() {/,/^}/p; /^luks_auto_unlock_drop() {/,/^}/p
/^luks_record_slots() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$tmp/etc/|g" >"$tmp/unlock.sh"
grep -q '^luks_auto_unlock_drop() {' "$tmp/unlock.sh" && grep -q '^limine_auto_unlock_drop() {' "$tmp/unlock.sh" &&
grep -q '^luks_record_slots() {' "$tmp/unlock.sh" ||
fail "omarchy-provision-owner defines the dispatched and Limine auto-unlock callbacks and the slot record"
sed -n '/^rekey_luks() {/,/^}/p; /^run_provisioning() {/,/^}/p; /^cleanup_oem_state() {/,/^}/p
/^platform_ready() {/,/^}/p; /^run_setup() {/,/^}/p
/^rekey_accepts_password() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" |
sed "s|/etc/|$tmp/etc/|g" >"$tmp/provision.sh"
grep -q '^run_provisioning() {' "$tmp/provision.sh" && grep -q '^run_setup() {' "$tmp/provision.sh" ||
fail "omarchy-provision-owner defines its setup and provisioning worker"
cat >"$tmp/attempt.sh" <<'SH'
set -euo pipefail
source "$ROOT/install/provisioning/luks-rekey.sh"
source "$TMP/unlock.sh"
source <(sed -n '/^luks_boot_layout() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$TMP/etc/|g")
declare -F luks_boot_layout >/dev/null
PROVISIONING_DIR=$TMP/provisioning
REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state
LOG_FILE=$TMP/log
password=$PASSWORD
log_step() { printf '%s\n' "$*" >>"$LOG_FILE"; }
say() { :; }
crash_point() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == ${CRASH_AT:-0} )); then
kill -9 $$
kill -9 "$BASHPID"
fi
}
eval "$(declare -f rekey_state_put | sed '1s/^rekey_state_put /journal_write /')"
rekey_state_put() {
journal_write "$@" || return 1
crash_point "journal $*"
}
reset_limine_config() {
echo reset >>"$TMP/limine-ran"
crash_point "auto-unlock files removed"
}
limine-update() {
echo update >>"$TMP/limine-ran"
echo rebuild >>"$TMP/rebuilds"
[[ ! -e $TMP/rebuild-fail ]] || return 1
crash_point "boot rebuilt"
}
shred() {
command rm -f -- "${@: -1}"
crash_point "staged key destroyed"
}
fake_cryptsetup() {
local op=$1 key_file="" token_type="" target="" extra="" requested="" material slot next
shift
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--key-slot) requested=$2; shift 2 ;;
--token-type) token_type=$2; shift 2 ;;
-*) shift ;;
*)
if [[ -z $target ]]; then target=$1; else extra=$1; fi
shift
;;
esac
done
[[ $target == "$DEVICE" ]] || return 4
if [[ $op == "luksDump" ]]; then
echo "Keyslots:"
awk '{ printf " %s: luks2\n", $1 }' "$TMP/slots"
if [[ -s $TMP/token-slot ]]; then
printf 'Tokens:\n 0: luks2-keyring\n\tKeyslot: %s\n' "$(cat "$TMP/token-slot")"
fi
printf 'Digests:\n 0: pbkdf2\n'
return 0
fi
if [[ $op == "luksKillSlot" ]]; then
[[ ! -e $TMP/kill-noop ]] || return 0
awk -v s="$extra" '$1 != s' "$TMP/slots" >"$TMP/slots.next"
command mv -f "$TMP/slots.next" "$TMP/slots"
return 0
fi
# An enrolled token unlocks its slot whatever key is given, unless the
# allowed token types exclude it, as with cryptsetup.
if [[ $op == "open" && -z $token_type && -s $TMP/token-slot ]]; then
echo "Key slot $(cat "$TMP/token-slot") unlocked."
return 0
fi
material=$(cat "$key_file")
slot=$(awk -v m="$material" '$2 == m { print $1; exit }' "$TMP/slots")
if [[ -z $slot ]]; then
echo "No key available with this passphrase."
return 2
fi
case $op in
open) echo "Key slot $slot unlocked." ;;
luksAddKey)
next=$requested
if [[ -z $next ]]; then
for (( next = 0; next < 32; next++ )); do
awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || break
done
fi
! awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || return 1
printf '%s %s\n' "$next" "$(cat "$extra")" >>"$TMP/slots"
;;
*) return 1 ;;
esac
}
cryptsetup() {
local status=0
if [[ $BACKEND == "fake" ]]; then
fake_cryptsetup "$@" || status=$?
elif [[ $1 == "luksAddKey" ]]; then
command cryptsetup luksAddKey --pbkdf pbkdf2 --pbkdf-force-iterations 1000 "${@:2}" || status=$?
else
command cryptsetup "$@" || status=$?
fi
(( status == 0 )) || return "$status"
case $1 in
luksAddKey)
echo owner >>"$TMP/adds"
crash_point "owner key added"
;;
luksKillSlot) crash_point "slot ${*: -1} killed" ;;
esac
}
# omarchy-provision-owner's setup functions, with account and boot setup and
# the screen stubbed.
setup_functions() {
source "$TMP/provision.sh"
STATE_FILE=$TMP/state
FINALIZE_WARNING_FLAG=$TMP/finalize-warning
username=owner hostname="" timezone=""
create_user() { :; }
install_authorized_keys() { :; }
configure_login() { :; }
configure_hostname() { :; }
configure_timezone() { :; }
finalize_user() { :; }
limine_entries_stale() {
crash_point "re-key returned"
[[ -e $TMP/stale ]]
}
luks_device() { echo "$DEVICE"; }
systemctl() { :; }
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
}
# The first-boot worker around the re-key.
provision() {
setup_functions
run_provisioning
}
# A whole setup attempt once the owner form is filled in: the password check,
# then the worker.
attempt() {
setup_functions
keyboard_form() { :; }
user_form() { :; }
confirm_form() { :; }
notice() {
printf 'refused: %s\n' "$1" >>"$TMP/screen"
exit 3
}
set_now() { NOW=0; }
render_setup_static() { :; }
render_setup_dynamic() { :; }
SHOW_CURSOR=""
run_setup
}
# First-boot setup up to the owner form, with the screen captured.
setup() {
source "$TMP/provision.sh"
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
keyboard_form() {
echo "owner form" >>"$TMP/screen"
exit 0
}
run_setup
}
case $MODE in
provision) provision ;;
attempt) attempt ;;
setup) setup ;;
rekey) luks_rekey "$DEVICE" ;;
accepts) luks_rekey_accepts_password "$DEVICE" ;;
remains) luks_staged_unlock_remains ;;
esac
SH
run() {
local mode=$1 password=$2 crash_at=${3:-0}
echo "$crash_at" >"$tmp/crash-at"
{
ROOT=$ROOT TMP=$tmp BACKEND=$backend DEVICE=$device MODE=$mode PASSWORD=$password CRASH_AT=$crash_at \
OMARCHY_PATH=$runtime OMARCHY_PROC_ROOT=$tmp/$platform/proc OMARCHY_LIFECYCLE_ROOT=$tmp/lifecycle \
PATH="$tmp/$platform/bin:$PATH" bash "$tmp/attempt.sh"
} >>"$tmp/output" 2>&1
}
# The slot the key opens on the volume, or nothing. Tokens never answer.
opens() {
if [[ $backend == "fake" ]]; then
awk -v m="$1" '$2 == m { print $1; exit }' "$tmp/slots"
else
LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file <(printf '%s' "$1") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' || true
fi
}
slot_count() {
if [[ $backend == "fake" ]]; then
wc -l <"$tmp/slots"
else
cryptsetup luksDump "$device" 2>/dev/null | grep -cE '^ +[0-9]+: luks2$|^Key Slot [0-9]+: ENABLED'
fi
}
fixture() {
local format=${1:-luks2}
rm -rf "$tmp/provisioning" "$tmp/etc" "$tmp/boot" "$tmp/log" "$tmp/output" "$tmp/trace" "$tmp/rebuilds" "$tmp/adds" "$tmp/rebuild-fail" "$tmp/kill-noop" \
"$tmp/prepare-fail" "$tmp/screen" "$tmp/stale" "$tmp/token-slot" "$tmp/slot-record" "$tmp/record-fail"
mkdir -p "$tmp/provisioning"
chmod 755 "$tmp/provisioning"
touch "$tmp/provisioning/pending"
printf '%s' "$staged_key" >"$tmp/provisioning/luks-key"
if [[ $platform == "apple" ]]; then
mkdir -p "$tmp/boot/omarchy" "$tmp/etc/default"
printf '%s' "$staged_key" >"$tmp/boot/omarchy/luks-key"
echo 'GRUB_CMDLINE_LINUX="rd.luks.name=root-uuid=root rd.luks.key=root-uuid=/omarchy/luks-key:UUID=boot-uuid"' >"$tmp/etc/default/grub"
else
mkdir -p "$tmp/etc/omarchy" "$tmp/etc/limine-entry-tool.d" "$tmp/etc/mkinitcpio.conf.d"
printf '%s' "$staged_key" >"$tmp/etc/omarchy/provisioning.key"
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>"$tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf"
echo 'FILES+=(/etc/omarchy/provisioning.key)' >"$tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf"
fi
echo 0 >"$tmp/steps"
: >"$tmp/log"
# A reset leaves the previous owner's slot next to the staged key.
if [[ $backend == "fake" ]]; then
device=$tmp/volume
: >"$device"
printf '0 %s\n1 %s\n' "$staged_key" "$seller_key" >"$tmp/slots"
else
device=$tmp/volume.img
rm -f "$device"
truncate -s 32M "$device"
local pbkdf=(--pbkdf pbkdf2 --pbkdf-force-iterations 1000)
[[ $format == "luks1" ]] && pbkdf=(--pbkdf-force-iterations 1000)
cryptsetup luksFormat -q --type "$format" "${pbkdf[@]}" "$device" <(printf '%s' "$staged_key")
cryptsetup luksAddKey "${pbkdf[@]}" --key-file <(printf '%s' "$staged_key") "$device" <(printf '%s' "$seller_key")
fi
}
# What a previous owner can leave on the real volume: a systemd-tpm2 token and
# a luks2-keyring token on slot $1, the second answered from the session keyring
# with $2. Fails when the kernel keyring is out of reach (no keyctl, or a
# container's seccomp profile), so that a bare cryptsetup open does not take the
# token in place of any key.
enroll_tokens() {
local slot=$1 key=$2 description=omarchy-test-token-$$
command -v keyctl >/dev/null || return 1
token_key=$(printf '%s' "$key" | keyctl padd user "$description" @s 2>/dev/null) || return 1
keyctl timeout "$token_key" 600 >/dev/null 2>&1 || true
printf '{"type":"systemd-tpm2","keyslots":["%s"],"tpm2-blob":"AA==","tpm2-pcrs":[7],"tpm2-pcr-bank":"sha256","tpm2-primary-alg":"ecc","tpm2-policy-hash":"00","tpm2-pin":false}' "$slot" |
cryptsetup token import --disable-external-tokens "$device" || return 1
cryptsetup token add --key-description "$description" --key-slot "$slot" "$device" >/dev/null || return 1
LC_ALL=C cryptsetup open --test-passphrase --verbose --key-file <(printf 'not-a-key') "$device" 2>&1 | grep -qx "Key slot $slot unlocked."
}
unlock_files_present() {
[[ -e $tmp/etc/omarchy/provisioning.key || -e $tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf ||
-e $tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf || -e $tmp/boot/omarchy/luks-key ]] ||
grep -qs 'rd\.luks\.key=' "$tmp/etc/default/grub"
}
no_secrets_in() {
local file
local -a keys=(-e "$staged_key" -e "$seller_key" -e "$owner_password" -e "other-password")
for file in "$@"; do
[[ -e $file ]] || continue
! grep -Fq "${keys[@]}" "$file" ||
fail "$backend: $(basename "$file") holds no key material" "$(cat "$file")"
done
}
# Between attempts the disk must still open at the next boot: unattended while
# the auto-unlock remains, otherwise with a password the owner typed.
assert_recoverable() {
local context=$1 password=$2
if unlock_files_present; then
[[ -n $(opens "$staged_key") ]] || fail "$backend: $context: the auto-unlock key still opens the disk"
else
[[ -n $(opens "$password") ]] || fail "$backend: $context: the owner's password opens the disk once auto-unlock is gone"
fi
if [[ -n $(opens "$staged_key") && ! -f $tmp/provisioning/luks-key ]]; then
fail "$backend: $context: the staged key file is kept while it still unlocks the volume"
fi
if [[ -f $tmp/provisioning/luks-rekey.state ]]; then
[[ $(stat -c %a "$tmp/provisioning/luks-rekey.state") == "600" ]] || fail "$backend: $context: the journal is private"
fi
[[ $(stat -c %a "$tmp/provisioning") == "755" ]] || fail "$backend: $context: the provisioning directory stays readable for the new user"
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log"
}
# The re-key finished: the owner's slot is all the volume holds.
assert_finished() {
local context=$1 password=$2 max_adds=${3:-1} adds=0 owner
owner=$(opens "$password")
[[ $(slot_count) == "1" && -n $owner ]] || fail "$backend: $context: only the owner's slot remains" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") ]] || fail "$backend: $context: the staged install key no longer unlocks the volume"
[[ -z $(opens "$seller_key") ]] || fail "$backend: $context: the previous owner's key no longer unlocks the volume"
[[ ! -e $tmp/provisioning/luks-key ]] || fail "$backend: $context: the staged key file is destroyed"
! unlock_files_present || fail "$backend: $context: no boot-time auto-unlock remains"
[[ -f $tmp/adds ]] && adds=$(grep -c '^owner' "$tmp/adds" || true)
(( adds <= max_adds )) || fail "$backend: $context: the owner's key is added at most $max_adds time(s)"
! run remains "$password" || fail "$backend: $context: nothing of the staged unlock remains"
if [[ $platform == "apple" ]]; then
[[ $(cat "$tmp/slot-record" 2>/dev/null) == "owner=$owner"$'\n'"recovery=" ]] ||
fail "$backend: $context: the boot package records the owner's slot and no recovery slot" "$(cat "$tmp/slot-record" 2>/dev/null)"
else
[[ ! -e $tmp/slot-record ]] || fail "$backend: $context: x86 records no slots"
fi
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log" "$tmp/output" "$tmp/slot-record"
}
# Setup completed: the disk opens with the password the account got, and the
# provisioning state is gone.
assert_provisioned() {
assert_finished "$@"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state ]] ||
fail "$backend: $1: setup drops pending and the journal together"
}
backends=(fake)
if command -v cryptsetup >/dev/null; then
backends+=(luks2 luks1)
else
skip "cryptsetup is not installed; skipping the file-backed volume runs"
fi
# The crash matrix runs whole setup attempts on every backend, for x86 and for
# Apple.
matrix=()
for backend in "${backends[@]}"; do
matrix+=("x86 $backend")
[[ " ${platforms[*]} " != *" apple "* ]] || matrix+=("apple $backend")
done
for run_spec in "${matrix[@]}"; do
read -r platform backend <<<"$run_spec"
rm -f "$tmp/mac-boot-ran" "$tmp/limine-ran"
format=$backend
[[ $backend == "fake" ]] && format=luks2
fixture "$format"
run attempt "$owner_password" || fail "$backend: uninterrupted setup completes" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "uninterrupted" "$owner_password"
total_steps=$(cat "$tmp/steps")
(( total_steps >= 11 )) || fail "$backend: every durable step is a crash point" "$(cat "$tmp/trace")"
if [[ $platform == "apple" ]]; then
pass "apple $backend: uninterrupted setup leaves only the owner's slot, records it and destroys the staged key"
else
pass "x86 $backend: uninterrupted setup leaves only the owner's slot and destroys the staged key"
fi
# After each kill the owner reboots and answers the form again, with the same
# password or a new one. The account takes whatever the form accepts, so the
# disk must open with that password when setup finishes.
for (( step = 1; step <= total_steps; step++ )); do
for retry_password in "$owner_password" other-password; do
fixture "$format"
if run attempt "$owner_password" "$step"; then
fail "$backend: setup is killed at step $step"
fi
point=$(sed -n "${step}p" "$tmp/trace")
assert_recoverable "killed after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: killed after '$point', setup runs again"
staged_alive=$([[ -n $(opens "$staged_key") ]] && echo 1 || echo 0)
if run accepts "$retry_password"; then
[[ $retry_password == "$owner_password" ]] || (( staged_alive )) ||
fail "$backend: after '$point' a new password is refused once the staged key is retired"
run attempt "$retry_password" || fail "$backend: after '$point' setup completes" "$(cat "$tmp/log")"
if [[ $retry_password == "$owner_password" ]]; then
assert_provisioned "rerun after '$point'" "$retry_password"
if head -n "$step" "$tmp/trace" | grep -q 'journal phase boot'; then
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "$backend: rerun after '$point' does not rebuild boot again"
fi
else
assert_provisioned "new password after '$point'" "$retry_password" 2
[[ -z $(opens "$owner_password") ]] || fail "$backend: after '$point' the abandoned password is retired"
fi
else
[[ $retry_password != "$owner_password" ]] || fail "$backend: after '$point' the original password is always accepted"
(( ! staged_alive )) || fail "$backend: after '$point' a new password is accepted while the staged key works"
if run attempt "$retry_password"; then fail "$backend: after '$point' setup refuses a new password"; fi
assert_recoverable "refused new password after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: after '$point' a refused password keeps setup pending"
run attempt "$owner_password" || fail "$backend: after '$point' the original password completes" "$(cat "$tmp/log")"
assert_provisioned "original password after '$point'" "$owner_password"
fi
done
done
if [[ $platform == "apple" ]]; then
pass "apple $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password, with its slot recorded"
else
pass "x86 $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password"
fi
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86 $backend: no Mac boot entrypoint runs" "$(cat "$tmp/mac-boot-ran")"
pass "x86 $backend: dispatch is a no-op and the Limine UKI path runs unchanged"
else
[[ ! -e $tmp/limine-ran ]] || fail "apple $backend: the Limine UKI path never runs on Apple"
grep -qx provision-commit "$tmp/mac-boot-ran" && grep -qx provision-verify "$tmp/mac-boot-ran" ||
fail "apple $backend: the boot package commits and verifies the unlock"
pass "apple $backend: the boot package owns the boot-time unlock through dispatch"
fi
done
platform=x86
backend=fake
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "a failed boot rebuild fails the attempt"; fi
unlock_files_present || fail "a failed boot rebuild restores the auto-unlock"
[[ $(cat "$tmp/etc/omarchy/provisioning.key") == "$staged_key" ]] || fail "the restored keyfile is the staged key"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "a failed boot rebuild retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "the retry after a failed rebuild completes" "$(cat "$tmp/log")"
assert_finished "retry after a failed rebuild" "$owner_password"
pass "a failed boot rebuild keeps the unattended unlock and every slot for the retry"
fixture
printf '2 %s\n' tpm-sealed-key >>"$tmp/slots"
echo 2 >"$tmp/token-slot"
run provision "$owner_password" || fail "setup completes beside an enrolled token" "$(cat "$tmp/log")"
assert_provisioned "beside the previous owner's token" "$owner_password"
pass "a token the previous owner enrolled never answers for a key and is retired with its slot"
if [[ " ${backends[*]} " == *" luks2 "* ]]; then
backend=luks2
fixture luks2
if enroll_tokens 1 "$seller_key"; then
run provision "$owner_password" || fail "luks2: setup completes beside live tokens" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "beside the previous owner's live tokens" "$owner_password"
pass "luks2: with a live keyring token and a TPM2 token on the previous owner's slot, only real keys count"
else
skip "the kernel keyring is out of reach; skipping the live token run"
fi
backend=fake
fi
fixture
touch "$tmp/kill-noop"
if run rekey "$owner_password"; then fail "a slot kill that changes nothing fails the re-key"; fi
[[ -f $tmp/provisioning/luks-key && -f $tmp/provisioning/luks-rekey.state ]] || fail "an unverified retirement keeps the staged key and journal"
rm "$tmp/kill-noop"
run rekey "$owner_password" || fail "the retry after an ineffective slot kill completes" "$(cat "$tmp/log")"
assert_finished "retry after an ineffective slot kill" "$owner_password"
pass "the staged key is destroyed only after the volume proves it no longer opens"
fixture
printf '0 %s\n1 %s\n2 %s\n' "$staged_key" "$seller_key" "$owner_password" >"$tmp/slots"
printf 'staged_slot=0\nowner_slot=2\nphase=boot\n' >"$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "a leftover auto-unlock after the boot step is removed" "$(cat "$tmp/log")"
assert_finished "leftover auto-unlock after the boot step" "$owner_password"
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "the leftover auto-unlock triggers a boot rebuild"
pass "an auto-unlock that outlives the boot step is rebuilt away rather than blocking forever"
# The boot files ask for the disk password in the layout vconsole.conf held
# when they were built. A retry after the owner picked another layout rebuilds
# them once, at the boot step and after done; the same layout rebuilds nothing,
# and a journal from before the fingerprint rebuilds once.
choose_layout() {
mkdir -p "$tmp/etc"
printf 'KEYMAP=%s\nXKBLAYOUT=%s\nXKBMODEL=pc105\n' "$1" "$2" >"$tmp/etc/vconsole.conf"
}
rebuild_count() {
wc -l <"$tmp/rebuilds" 2>/dev/null || echo 0
}
assert_fingerprint() {
local context=$1 journal=$tmp/provisioning/luks-rekey.state
grep -Eqx 'boot_layout=[0-9a-f]{64}' "$journal" || fail "$context: the journal records the layout as a hash" "$(cat "$journal")"
! grep -Eqw 'dk|us' "$journal" "$tmp/log" "$tmp/trace" || fail "$context: the journal and log never name the layout"
}
boot_layout_runs() {
local name=$1 stop step refresh before
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes with a layout" "$(cat "$tmp/log")"
assert_fingerprint "$name: uninterrupted"
cp "$tmp/trace" "$tmp/layout-trace"
for stop in boot done; do
step=$(grep -m1 "^[0-9]* journal phase $stop" "$tmp/layout-trace" | cut -d' ' -f1)
[[ -n $step ]] || fail "$name: the trace names the $stop journal write" "$(cat "$tmp/layout-trace")"
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
[[ $(rebuild_count) == 1 ]] || fail "$name: killed at $stop after one rebuild"
run rekey "$owner_password" || fail "$name: the same layout completes after $stop" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an unchanged layout after $stop rebuilds nothing"
assert_finished "$name: unchanged layout after $stop" "$owner_password"
# The owner picks another layout on the retry right after the interruption.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
run rekey "$owner_password" || fail "$name: a changed layout after $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a changed layout after $stop rebuilds the boot files"
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") != "$before" ]] ||
fail "$name: the journal takes the new layout's fingerprint after $stop"
[[ $(grep -c '^[0-9]* journal boot_layout ' "$tmp/trace") == 1 && $(grep -c "^[0-9]* journal phase $stop" "$tmp/trace") == 1 ]] ||
fail "$name: the rebuild after $stop records only the fingerprint, keeping the phase" "$(cat "$tmp/trace")"
refresh=$(awk -v s="$step" '$1 > s && / boot rebuilt$/ { print $1; exit }' "$tmp/trace")
run rekey "$owner_password" || fail "$name: a rerun after the rebuild completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: the changed layout after $stop rebuilds only once"
assert_finished "$name: changed layout after $stop" "$owner_password"
assert_fingerprint "$name: changed layout after $stop"
# Killed between that rebuild and its fingerprint: the retry rebuilds again.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
choose_layout dk dk
if run rekey "$owner_password" "$refresh"; then fail "$name: the re-key is killed after the layout rebuild at $stop"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a layout rebuild killed before its journal write keeps the old fingerprint"
run rekey "$owner_password" || fail "$name: the retry after a killed layout rebuild at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 3 ]] || fail "$name: a layout rebuild killed at $stop runs again"
assert_finished "$name: killed layout rebuild at $stop" "$owner_password"
assert_fingerprint "$name: killed layout rebuild at $stop"
done
# Interrupted at the boot step, the owner picks another layout and a new
# password while the staged key still opens the disk.
step=$(grep -m1 "^[0-9]* journal phase boot" "$tmp/layout-trace" | cut -d' ' -f1)
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the boot journal write"; fi
choose_layout dk dk
run rekey other-password || fail "$name: a new layout and password after the boot step complete" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a new layout and password after the boot step rebuild once"
assert_finished "$name: new layout and password after the boot step" other-password 2
assert_fingerprint "$name: new layout and password after the boot step"
# A journal from before the fingerprint, at the boot step with the auto-unlock
# already gone and after done: rebuilt once, then recorded.
for stop in boot done; do
fixture
choose_layout dk dk
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
step=$(grep -m1 -n "^[0-9]* journal phase $stop" "$tmp/trace" | cut -d: -f1)
fixture
choose_layout dk dk
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
sed -i '/^boot_layout=/d' "$tmp/provisioning/luks-rekey.state"
rm -f "$tmp/rebuilds"
run rekey "$owner_password" || fail "$name: an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds the boot files once"
run rekey "$owner_password" || fail "$name: a rerun of an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds only once"
assert_finished "$name: older journal at $stop" "$owner_password"
assert_fingerprint "$name: older journal at $stop"
done
# After done a failed rebuild keeps the old fingerprint, so the retry rebuilds.
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "$name: a failed layout rebuild after done fails the attempt"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a failed layout rebuild keeps the old fingerprint"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "$name: the retry after a failed layout rebuild completes" "$(cat "$tmp/log")"
assert_finished "$name: retry after a failed layout rebuild" "$owner_password"
assert_fingerprint "$name: retry after a failed layout rebuild"
}
boot_layout_runs x86
pass "x86: a changed keyboard layout rebuilds the boot files once, an unchanged one never, and an older journal once"
fixture
printf 'wrong-key' >"$tmp/provisioning/luks-key"
if run rekey "$owner_password"; then fail "a staged key that opens nothing fails the re-key"; fi
[[ ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] || fail "a dead staged key changes nothing"
pass "a fresh re-key refuses a staged key that no longer unlocks the volume"
fixture
rm "$tmp/provisioning/luks-key"
run remains "$owner_password" || fail "a leftover auto-unlock counts as the staged unlock remaining"
if run rekey "$owner_password"; then fail "a leftover auto-unlock without its staged key fails closed"; fi
unlock_files_present || fail "the leftover auto-unlock is not silently accepted"
pass "a boot-time auto-unlock without its staged key keeps provisioning from finishing"
fixture
if run rekey "$staged_key"; then fail "the staged key is refused as the owner's password"; fi
[[ $(slot_count) == "2" ]] && unlock_files_present || fail "refusing the staged key as a password changes nothing"
run rekey "$owner_password" || fail "a retry with a real password completes" "$(cat "$tmp/log")"
assert_finished "retry after refusing the staged key" "$owner_password"
pass "the owner's password must differ from the staged install key"
fixture
printf 'phase=owner\nstaged_slot=0\nowner_slot=2\nrecovery_slot=5\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
(
source "$ROOT/install/provisioning/luks-rekey.sh"
REKEY_STATE=$tmp/provisioning/luks-rekey.state
rekey_state_put phase boot
)
[[ $(grep -c . "$tmp/provisioning/luks-rekey.state") == "4" ]] && grep -qx 'phase=boot' "$tmp/provisioning/luks-rekey.state" &&
grep -qx 'recovery_slot=5' "$tmp/provisioning/luks-rekey.state" || fail "a journal write keeps every other recorded key" "$(cat "$tmp/provisioning/luks-rekey.state")"
pass "journal writes replace only their own keys"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "provisioning with a leftover auto-unlock and no staged key fails"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "provisioning keeps its state while the auto-unlock remains"
pass "first-boot provisioning never finishes with the boot-time auto-unlock still configured"
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc"
run provision "$owner_password" || fail "unencrypted provisioning finishes" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] ||
fail "unencrypted provisioning skips the re-key"
pass "provisioning without a staged key or auto-unlock skips the re-key"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
# Apple: a failed boot-package commit keeps the unattended unlock and every slot.
platform=apple
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "apple: a failed boot-package commit fails the attempt"; fi
unlock_files_present || fail "apple: a failed commit keeps the boot-time unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple: a failed commit retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "apple: the retry after a failed commit completes" "$(cat "$tmp/log")"
assert_finished "apple: retry after a failed commit" "$owner_password"
pass "apple: a failed boot-package commit keeps the unattended unlock and every slot for the retry"
# Apple: an inherited decision never replaces resolving who owns the unlock.
fixture
rm -f "$tmp/limine-ran"
export UNLOCK_OWNER=limine
run provision "$owner_password" || fail "apple: setup completes with UNLOCK_OWNER in its environment" "$(cat "$tmp/log")"
unset UNLOCK_OWNER
assert_provisioned "apple: UNLOCK_OWNER inherited" "$owner_password"
[[ ! -e $tmp/limine-ran ]] || fail "apple: an inherited UNLOCK_OWNER does not select the Limine path"
pass "apple: an inherited UNLOCK_OWNER is ignored"
# Apple: the boot package's commit rebuilds for a changed layout, after done too.
boot_layout_runs apple
pass "apple: a changed keyboard layout reruns the boot package's commit once, an unchanged one never, and an older journal once"
fi
# After a factory reset left entries for another machine identity, the menu
# starts over and limine-update rebuilds, on Apple too, and setup still
# finishes only once the boot package verifies nothing of the staged unlock
# remains.
for platform in "${platforms[@]}"; do
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc" "$tmp/boot" "$tmp/limine-ran" "$tmp/mac-boot-ran"
touch "$tmp/stale"
run provision "$owner_password" || fail "$platform: stale boot entries are refreshed" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && $(cat "$tmp/limine-ran") == $'reset\nupdate' ]] ||
fail "$platform: the Limine menu is reset and limine-update rebuilds" "$(cat "$tmp/limine-ran")"
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86: no Mac entrypoint runs"
else
grep -qx provision-verify "$tmp/mac-boot-ran" || fail "apple: the boot package still has the last word"
fi
done
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/limine-ran"
touch "$tmp/stale"
if run provision "$owner_password"; then fail "apple: a leftover boot-partition unlock still stops setup after the refresh"; fi
[[ -e $tmp/provisioning/pending ]] || fail "apple: setup stays pending"
fi
pass "stale boot entries are rebuilt by limine-update, behind the boot package's verify on Apple"
# Before the owner form: a no-op on x86 even with Mac entrypoints on disk, the
# boot package's own answer on Apple.
platform=x86
fixture
rm -f "$tmp/mac-boot-ran"
touch "$tmp/prepare-fail"
run setup "$owner_password" || fail "x86: setup reaches the owner form without a boot package" "$(cat "$tmp/output")"
[[ $(cat "$tmp/screen") == "owner form" && ! -e $tmp/mac-boot-ran ]] ||
fail "x86: no Mac boot entrypoint runs before the owner form" "$(cat "$tmp/screen")"
pass "x86: the platform check before the owner form is a no-op"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
run setup "$owner_password" && grep -qx 'owner form' "$tmp/screen" ||
fail "apple: setup reaches the owner form with the boot package ready" "$(cat "$tmp/output")"
fixture
touch "$tmp/prepare-fail"
if run setup "$owner_password"; then fail "apple: setup stops when the boot package is not ready"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple: the owner is asked nothing when the boot package is not ready"
grep -q 'The boot partition is not mounted.' "$tmp/screen" && grep -q 'The boot partition is not mounted.' "$tmp/log" ||
fail "apple: the boot package's reason reaches the screen and the log" "$(cat "$tmp/screen" "$tmp/log" 2>/dev/null)"
pass "apple: setup stops before the owner form when the boot package is not ready"
# Apple without omarchy-mac-boot, or with one older than its provisioning
# entrypoints: setup stops before the owner form naming the package, and a
# worker that got past it anyway never finishes while any part of the staged
# unlock remains.
mv "$mac_boot" "$tmp/mac-boot.off"
for package in missing old; do
# An older package can already ship other files in that directory.
if [[ $package == old ]]; then
install -d -m 755 "$mac_boot"
install -m 755 /dev/null "$mac_boot/boot-hook"
fi
fixture
rm -f "$tmp/limine-ran" "$tmp/mac-boot-ran"
if run setup "$owner_password"; then fail "apple ($package package): setup refuses without provisioning entrypoints"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple ($package package): the owner is asked nothing"
grep -q 'provision-prepare on aarch64-apple needs omarchy-mac-boot' "$tmp/screen" ||
fail "apple ($package package): the boot package is named on the screen" "$(cat "$tmp/screen" 2>/dev/null)"
grep -q '/usr/lib/omarchy/mac-boot/provision-prepare' "$tmp/log" || fail "apple ($package package): the log names the entrypoint" "$(cat "$tmp/log")"
if run provision "$owner_password"; then fail "apple ($package package): provisioning fails"; fi
[[ -e $tmp/provisioning/pending && -f $tmp/provisioning/luks-key ]] && unlock_files_present ||
fail "apple ($package package): provisioning keeps its state and the staged unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple ($package package): no slot is retired"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "apple ($package package): a leftover boot-partition unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "apple ($package package): a leftover boot-partition unlock keeps provisioning pending"
[[ ! -e $tmp/limine-ran && ! -e $tmp/mac-boot-ran ]] || fail "apple ($package package): the Limine UKI path is no fallback"
[[ $package == missing ]] || rm -r "$mac_boot"
done
mv "$tmp/mac-boot.off" "$mac_boot"
pass "apple: without omarchy-mac-boot's provisioning entrypoints setup stops naming the package, and the worker fails closed"
fi
# A boot package that implements only one of the commit/verify pair owns
# nothing: provisioning fails closed rather than mixing it with the Limine path.
mkdir -p "$tmp/half/bin"
cat >"$tmp/half/bin/omarchy-lifecycle-dispatch" <<SH
#!/bin/bash
echo "\$*" >>"$tmp/half-ran"
[[ \$* != "--resolve provision-commit" ]] || echo /usr/lib/omarchy/mac-boot/provision-commit
SH
chmod +x "$tmp/half/bin/omarchy-lifecycle-dispatch"
runtime=$tmp/half
fixture
rm -f "$tmp/limine-ran" "$tmp/half-ran"
if run provision "$owner_password"; then fail "a half-implemented unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "a half-implemented unlock keeps provisioning pending"
[[ -s $tmp/half-ran && ! -e $tmp/limine-ran ]] && ! grep -qv -- '--resolve' "$tmp/half-ran" ||
fail "a half-implemented unlock runs neither the platform nor the Limine path" "$(cat "$tmp/half-ran")"
runtime=$ROOT
pass "a boot package implementing only half of the unlock pair fails closed"
# Any slot but the owner's is retired, a recovery key an earlier setup added
# and recorded in the journal included.
platform=x86
backend=fake
fixture
printf '2 recovery-key\n' >>"$tmp/slots"
printf 'recovery_slot=2\nrecovery_shown=1\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "the re-key beside an earlier recovery slot completes" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") && -z $(opens "$seller_key") && -z $(opens recovery-key) && -n $(opens "$owner_password") && $(slot_count) == 1 ]] ||
fail "every slot but the owner's is retired" "$(cat "$tmp/slots")"
pass "the re-key retires every slot but the owner's, an earlier recovery slot included"