WIP: Paused over-hardening fix wave (boot checkout verification, double-read package manifest)

This commit is contained in:
Gabriel Brown
2026-09-17 11:43:30 -04:00
parent 28e387868f
commit 442eec19fa
15 changed files with 2553 additions and 326 deletions
+91 -37
View File
@@ -88,40 +88,78 @@ source "$PANAMA_PATH/bin/ascii"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
PACKAGES_HASH="$STATE_DIR/packages-hash"
hash_packages() {
local file relative size fixed_input digest
for fixed_input in \
"$PANAMA_PATH/setup/scripts/install-packages" \
"$PANAMA_PATH/setup/lib/artifact-provenance"; do
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1
done
digest="$(
{
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1
printf '%s\0' \
"$PANAMA_PATH/setup/scripts/install-packages" \
"$PANAMA_PATH/setup/lib/artifact-provenance" || exit 1
find "$PANAMA_PATH/setup/provenance" -type f -print0 || exit 1
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
relative="${file#"$PANAMA_PATH"/}"
size="$(wc -c <"$file")" || exit 1
printf '%s\0%s\0' "$relative" "$size" || exit 1
cat -- "$file" || exit 1
printf '\0' || exit 1
done | sha256sum | cut -d' ' -f1
)" || return 1
printf '%s\n' "$digest"
_collect_package_inputs() {
local destination="$1" raw="${1}.raw"
[[ -d "$PANAMA_PATH/setup/packages" \
&& ! -L "$PANAMA_PATH/setup/packages" \
&& -d "$PANAMA_PATH/setup/provenance" \
&& ! -L "$PANAMA_PATH/setup/provenance" ]] || return 1
{
printf '%s\0' \
"$PANAMA_PATH/setup/scripts/install-packages" \
"$PANAMA_PATH/setup/lib/artifact-provenance" \
"$PANAMA_PATH/setup/lib/extras-catalog" \
"$PANAMA_PATH/setup/lib/machine-role" || exit 1
# extras/ is deliberately excluded. A symlink or other non-directory
# object at this level is still an input error, not something discovery may
# silently omit.
find "$PANAMA_PATH/setup/packages" -mindepth 1 -maxdepth 1 \
! -type d -print0 || exit 1
find "$PANAMA_PATH/setup/provenance" -mindepth 1 \
! -type d -print0 || exit 1
} >"$raw" || return 1
LC_ALL=C sort -z "$raw" >"$destination"
}
_write_package_manifest() {
local inputs="$1" destination="$2" file relative digest
: >"$destination" || return 1
while IFS= read -r -d '' file; do
[[ -f "$file" && ! -L "$file" && -r "$file" ]] || return 1
relative="${file#"$PANAMA_PATH"/}"
[[ "$relative" != "$file" ]] || return 1
digest="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
printf '%s\0%s\0' "$relative" "$digest" >>"$destination" || return 1
done <"$inputs"
}
# Read every input twice from the same enumerated set. A file or path that
# changes while the snapshot is built cannot produce a receipt.
hash_packages() (
local work="" inputs_before inputs_after manifest_before manifest_after
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work="$(mktemp -u -d -t panama-packages-hash.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$work"; then
work=""
exit 1
fi
inputs_before="$work/inputs-before"
inputs_after="$work/inputs-after"
manifest_before="$work/manifest-before"
manifest_after="$work/manifest-after"
_collect_package_inputs "$inputs_before" || exit 1
_write_package_manifest "$inputs_before" "$manifest_before" || exit 1
_collect_package_inputs "$inputs_after" || exit 1
cmp -s -- "$inputs_before" "$inputs_after" || exit 1
_write_package_manifest "$inputs_after" "$manifest_after" || exit 1
cmp -s -- "$manifest_before" "$manifest_after" || exit 1
sha256sum -- "$manifest_before" | awk '{ print $1 }'
)
PACKAGE_START_HASH=""
packages_needed() {
local current_hash recorded_hash
current_hash="$(hash_packages)" || return 2
PACKAGE_START_HASH="$current_hash"
(( FORCE_PACKAGES )) && return 0
(( UPGRADE )) || return 0
[[ -r "$PACKAGES_HASH" ]] || return 0
current_hash="$(hash_packages)" || return 2
recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
[[ "$current_hash" != "$recorded_hash" ]]
}
@@ -129,17 +167,25 @@ packages_needed() {
# Written only after the stage succeeds, mirroring the rule panama-migrate
# documents for its markers: a step that did not complete has not happened, and
# recording it as done hides it forever.
record_packages_hash() {
local temporary_hash
record_packages_hash() (
local expected_hash="$1" current_hash temporary_hash=""
trap '[[ -z "$temporary_hash" ]] || rm -f -- "$temporary_hash"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
current_hash="$(hash_packages)" || return 1
[[ "$current_hash" == "$expected_hash" ]] || return 1
mkdir -p "$STATE_DIR"
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
if hash_packages >"$temporary_hash"; then
mv -f -- "$temporary_hash" "$PACKAGES_HASH"
else
rm -f -- "$temporary_hash"
temporary_hash="$(mktemp -u "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
umask 077
if ! (set -o noclobber; : >"$temporary_hash") 2>/dev/null; then
temporary_hash=""
return 1
fi
}
printf '%s\n' "$expected_hash" >"$temporary_hash" || return 1
mv -f -- "$temporary_hash" "$PACKAGES_HASH" || return 1
temporary_hash=""
)
# Repository trust is checked before the installer can reach its bootstrap DNF.
# Status 78 is reserved for a trust-root failure and is propagated unchanged so
@@ -190,7 +236,8 @@ if (( ! UPGRADE )); then
fi
if (( ${#bootstrap[@]} > 0 )); then
echo "Installing what the setup questions are built on: ${bootstrap[*]}"
sudo dnf install -y "${bootstrap[@]}" >/dev/null || {
sudo dnf install -y --repo=fedora --repo=updates \
--from-repo=fedora,updates "${bootstrap[@]}" >/dev/null || {
echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2
exit 1
}
@@ -253,7 +300,12 @@ gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
# is unset and each stage takes the empty-answer path it already documents --
# which is why this is a flag rather than a rewrite of seven stage scripts.
if (( ! UPGRADE )); then
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)"
PANAMA_ANSWERS="$(mktemp -u -t panama-answers.XXXXXX)" || exit 1
umask 077
if ! (set -o noclobber; : >"$PANAMA_ANSWERS") 2>/dev/null; then
PANAMA_ANSWERS=""
exit 1
fi
export PANAMA_ANSWERS
if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then
@@ -343,8 +395,10 @@ for stage in "${STAGES[@]}"; do
[[ -x "$script" ]] || continue
printf '\n=== %s ===\n' "$stage"
if [[ "$stage" == install-packages ]]; then
package_start_hash=""
package_state_status=0
packages_needed || package_state_status=$?
package_start_hash="$PACKAGE_START_HASH"
if (( package_state_status == 1 )); then
echo "The package lists have not changed since the last run; skipping."
echo "Run with --packages to install them anyway."
@@ -357,7 +411,7 @@ for stage in "${STAGES[@]}"; do
fi
if "$script"; then
if [[ "$stage" == install-packages ]]; then
if ! record_packages_hash; then
if ! record_packages_hash "$package_start_hash"; then
failed+=("$stage")
printf '!!! %s could not record its tracked installation inputs\n' "$stage" >&2
fi