Phase 3 of the fresh-install spec: the parts of a run that depend on what the machine actually is. NVIDIA, Secure Boot, Fedora's preinstalled extras, firmware. Two of these looked like they would force a compromise, and neither did. sunhat opened an editor in the middle of its run so grub could be hand-corrected, and that single step is why walking away from an install did not work. The step existed to delete duplicated kernel arguments -- and grubby replaces an argument that already exists rather than appending a second copy, so the duplicates cannot accumulate and there is nothing to correct. The editor was load-bearing for a problem that a different tool does not have. MOK enrolment needs a password now and the same password at the next boot's blue screen, which reads like a prompt that has to happen mid-run. mokutil has --generate-hash and --hash-file for exactly this: the interview asks, hashes it on the spot, and records only the hash. The plaintext never reaches the answers file, the environment, or a command line, and the stage runs without asking. The stage runs last rather than fourth as the spec's table had it. The constraint was always "late" and fourth of eight is not late: enrolment arms a prompt for the next boot and firmware may want a reboot, so a machine that reboots out of this stage should already be completely configured. Every question names what was found -- the card, the packages actually installed -- and is not asked at all on a machine it would do nothing to. sunhat's debloat list no longer describes Fedora 44: totem became showtime and LibreOffice is not preinstalled, so the list is curated and a package that is not installed is never passed to dnf, which is what lets it outlive a release. This stage cannot be verified by running it. It installs a proprietary driver and queues a Secure Boot enrolment, and this machine is an AMD desktop. So every privileged command is stood in on PATH and the contract asserts which answer led to which call: that no answers means no commands, that a failed driver install is not followed by arguments and services for a driver that is not there, that the hash reaches mokutil through a file and never a command line, and that removal is offered only for packages that are installed. The contract was checked by breaking the stage three ways and confirming it caught each. It does not verify that akmod-nvidia builds, and says so where a reader would otherwise assume it did. The README's stage table listed three of seven stages; the interview and identity work never reached it. Corrected rather than extended, since a table that lists three of seven is worse than one that lists none. The Desktops section still describes a GNOME session nothing installs -- that is phase 5. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
200 lines
8.5 KiB
Bash
Executable File
200 lines
8.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# What install-hardware does, and — more importantly — what it does not.
|
|
#
|
|
# This stage cannot be verified the way the rest of Panama is. It installs a
|
|
# proprietary driver, rewrites kernel arguments and queues a Secure Boot
|
|
# enrolment, and the machine it was written on is an AMD desktop with no NVIDIA
|
|
# card in it. Running it to see what happens is not available.
|
|
#
|
|
# So every privileged command it can reach is stood in on PATH, and the contract
|
|
# asserts what was called with what. That verifies the decisions — which answer
|
|
# leads to which command — which is the part that can be wrong. It does not
|
|
# verify that akmod-nvidia builds, and nothing here should be read as claiming it
|
|
# does.
|
|
#
|
|
# The properties worth pinning:
|
|
#
|
|
# 1. Absent answers do nothing at all. Every stage in this repository is
|
|
# independently re-runnable while repairing one piece of a machine, and a
|
|
# hardware stage that acted on its own defaults would be the one that
|
|
# installed a driver nobody asked for.
|
|
# 2. The MOK password never appears in a command line or an environment. It is
|
|
# hashed in the interview and reaches mokutil through a file, which is what
|
|
# --generate-hash and --hash-file exist for.
|
|
# 3. Nothing opens an editor. sunhat stopped in the middle of a run so grub
|
|
# could be hand-corrected, and that single step is why walking away from an
|
|
# install did not work.
|
|
# 4. Removal is offered only for packages that are actually installed, and the
|
|
# list has exactly one home.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
stage="$repo_dir/setup/scripts/install-hardware"
|
|
interview="$repo_dir/setup/scripts/interview"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -x "$stage" ]] || { printf 'hardware contract: %s is not executable\n' "$stage" >&2; exit 1; }
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
# ── The stand-ins ────────────────────────────────────────────────────────────
|
|
#
|
|
# Each records its own name and arguments and succeeds. `sudo` records the
|
|
# command it was asked to run and then runs it through the same stubs, so a
|
|
# privileged call is visible whether or not it went through sudo.
|
|
|
|
stub_dir="$work/bin"
|
|
mkdir -p "$stub_dir"
|
|
calls="$work/calls"
|
|
|
|
for command in dnf grubby systemctl mokutil fwupdmgr rpm; do
|
|
cat >"$stub_dir/$command" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf '%s %s\n' "$command" "\$*" >>"\$PANAMA_CALLS"
|
|
exit \${STUB_${command^^}_STATUS:-0}
|
|
STUB
|
|
chmod +x "$stub_dir/$command"
|
|
done
|
|
|
|
cat >"$stub_dir/sudo" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf 'sudo %s\n' "$*" >>"$PANAMA_CALLS"
|
|
exec "$@"
|
|
STUB
|
|
chmod +x "$stub_dir/sudo"
|
|
|
|
# `rpm -q` decides what debloat has to work with, so it answers for exactly one
|
|
# package. Overrides the recording stub for that one query.
|
|
cat >"$stub_dir/rpm" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf 'rpm %s\n' "$*" >>"$PANAMA_CALLS"
|
|
if [[ "${1:-}" == "-q" ]]; then
|
|
[[ "${2:-}" == "${STUB_INSTALLED:-}" ]] && exit 0
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
STUB
|
|
chmod +x "$stub_dir/rpm"
|
|
|
|
# Runs the stage with the given answers and returns everything it invoked.
|
|
run_stage() {
|
|
: >"$calls"
|
|
env -i HOME="$HOME" PATH="$stub_dir:/usr/bin:/bin" PANAMA_CALLS="$calls" \
|
|
STUB_INSTALLED="${STUB_INSTALLED:-}" STUB_DNF_STATUS="${STUB_DNF_STATUS:-0}" \
|
|
"$@" bash "$stage" >/dev/null 2>&1
|
|
cat "$calls"
|
|
}
|
|
|
|
called() { grep -q -- "$2" <<<"$1"; }
|
|
|
|
# ── 1. Nothing asked for, nothing done ───────────────────────────────────────
|
|
|
|
quiet="$(run_stage)"
|
|
if [[ -n "$quiet" ]]; then
|
|
note "with no answers the stage still ran: $(head -1 <<<"$quiet")"
|
|
fi
|
|
|
|
# ── The NVIDIA path ──────────────────────────────────────────────────────────
|
|
|
|
nvidia="$(run_stage PANAMA_NVIDIA=yes)"
|
|
|
|
called "$nvidia" 'dnf install -y akmod-nvidia' \
|
|
|| note 'answering yes to NVIDIA does not install akmod-nvidia'
|
|
called "$nvidia" 'xorg-x11-drv-nvidia-cuda' \
|
|
|| note 'the CUDA driver is not installed alongside the kernel module'
|
|
called "$nvidia" 'grubby --update-kernel=ALL' \
|
|
|| note 'the kernel arguments are never set'
|
|
called "$nvidia" 'modprobe.blacklist=nouveau' \
|
|
|| note 'nouveau is not blacklisted, so it can bind the card before nvidia does'
|
|
called "$nvidia" 'nvidia-drm.modeset=1' \
|
|
|| note 'nvidia-drm.modeset is not set, which a Wayland session needs'
|
|
called "$nvidia" 'systemctl enable nvidia-hibernate.service' \
|
|
|| note 'the suspend and resume services are never enabled'
|
|
|
|
# A driver that failed to install must not be followed by arguments and services
|
|
# for a driver that is not there.
|
|
failed="$(STUB_DNF_STATUS=1 run_stage PANAMA_NVIDIA=yes)"
|
|
if called "$failed" 'grubby --update-kernel'; then
|
|
note 'kernel arguments are set even when the driver failed to install'
|
|
fi
|
|
|
|
# ── 2. The MOK password stays out of sight ───────────────────────────────────
|
|
|
|
cert="$work/public_key.der"
|
|
printf 'not a real certificate\n' >"$cert"
|
|
hash='$6$notarealsalt$notarealhashvalue'
|
|
|
|
mok="$(run_stage PANAMA_MOK_HASH="$hash" PANAMA_MOK_CERT="$cert")"
|
|
|
|
called "$mok" 'mokutil --import' \
|
|
|| note 'a recorded MOK hash does not queue an enrolment'
|
|
called "$mok" -- '--hash-file' \
|
|
|| note 'the enrolment does not pass a hash file, so mokutil would prompt for a password'
|
|
if grep -qF -- "$hash" <<<"$mok"; then
|
|
note 'the MOK hash is passed on a command line where any process can read it'
|
|
fi
|
|
|
|
# No certificate means akmods never generated a key. Requesting enrolment of a
|
|
# key that does not exist is worse than skipping: it queues a prompt at the next
|
|
# boot for nothing.
|
|
without_cert="$(run_stage PANAMA_MOK_HASH="$hash" PANAMA_MOK_CERT="$work/absent.der")"
|
|
if called "$without_cert" 'mokutil --import'; then
|
|
note 'enrolment is requested even with no akmods certificate to enrol'
|
|
fi
|
|
|
|
# ── 4. Removal is honest about what it removes ───────────────────────────────
|
|
|
|
removable="$("$stage" --debloat-list)"
|
|
[[ -n "$removable" ]] || note '--debloat-list prints nothing, so the interview cannot name what it removes'
|
|
|
|
# Exactly one of them is installed, so exactly one may be passed to dnf.
|
|
present="$(head -1 <<<"$removable")"
|
|
absent="$(tail -1 <<<"$removable")"
|
|
debloat="$(STUB_INSTALLED="$present" run_stage PANAMA_DEBLOAT=yes)"
|
|
|
|
called "$debloat" "dnf remove -y $present" \
|
|
|| note "an installed package ($present) is not removed"
|
|
removal="$(grep 'dnf remove' <<<"$debloat")"
|
|
if grep -q -- "$absent" <<<"$removal"; then
|
|
note "a package that is not installed ($absent) is still passed to dnf remove"
|
|
fi
|
|
|
|
# ── Firmware ─────────────────────────────────────────────────────────────────
|
|
|
|
firmware="$(run_stage PANAMA_FIRMWARE=yes)"
|
|
called "$firmware" 'fwupdmgr refresh' \
|
|
|| note 'firmware metadata is never refreshed'
|
|
called "$firmware" 'fwupdmgr update' \
|
|
|| note 'firmware updates are never applied'
|
|
called "$firmware" -- '--no-reboot-check' \
|
|
|| note 'the firmware update may reboot the machine in the middle of an install'
|
|
|
|
# ── 3. Nothing stops for a human ─────────────────────────────────────────────
|
|
|
|
if grep -qE 'sudoedit|EDITOR=|\bnvim\b|kitty .*-e|read -r? *-?p' "$stage"; then
|
|
note 'the stage opens an editor or waits for input, which is the failure the interview exists to prevent'
|
|
fi
|
|
|
|
# The interview must ask for every answer this stage reads. The interview
|
|
# contract checks that in general; what it cannot check is that the two agree on
|
|
# the list itself, which is the coupling that breaks when a question is renamed.
|
|
for key in PANAMA_NVIDIA PANAMA_MOK_HASH PANAMA_DEBLOAT PANAMA_FIRMWARE; do
|
|
grep -q "record $key " "$interview" \
|
|
|| note "install-hardware reads $key, but the interview never records it"
|
|
done
|
|
|
|
# ── Report ───────────────────────────────────────────────────────────────────
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
printf 'hardware contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'hardware contract: PASS\n'
|