WIP: Paused over-hardening fix wave (boot checkout verification, double-read package manifest)
This commit is contained in:
@@ -45,6 +45,65 @@ checkout_command() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Git's index hints are performance promises, not trust evidence. In
|
||||
# particular, assume-unchanged and skip-worktree can make porcelain status
|
||||
# report a clean checkout whose files no longer match HEAD. Compare every
|
||||
# tracked blob and Git mode with the verified commit before handing control to
|
||||
# any file in the worktree.
|
||||
checkout_matches_verified_commit() (
|
||||
local checkout="$1" listing="" entry metadata mode type expected path actual
|
||||
local link_target_with_sentinel link_target
|
||||
|
||||
trap '[[ -z "$listing" ]] || rm -f -- "$listing"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
listing="$(mktemp -u -t panama-boot-tree.XXXXXX)" || exit 1
|
||||
umask 077
|
||||
if ! (set -o noclobber; : >"$listing") 2>/dev/null; then
|
||||
listing=""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkout_command git -C "$checkout" ls-tree -rz --full-tree \
|
||||
"$PANAMA_BOOT_REVISION" >"$listing" || exit 1
|
||||
while IFS= read -r -d '' entry; do
|
||||
[[ "$entry" == *$'\t'* ]] || exit 1
|
||||
metadata="${entry%%$'\t'*}"
|
||||
path="${entry#*$'\t'}"
|
||||
read -r mode type expected <<<"$metadata"
|
||||
[[ "$type" == blob && -n "$path" && "$path" != /* ]] || exit 1
|
||||
|
||||
case "$mode" in
|
||||
100644) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||
&& ! -x "$checkout/$path" ]] || exit 1 ;;
|
||||
100755) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||
&& -x "$checkout/$path" ]] || exit 1 ;;
|
||||
120000)
|
||||
[[ -L "$checkout/$path" ]] || exit 1
|
||||
# hash-object given a pathname follows a symlink. Git's 120000 blob is
|
||||
# the link text itself, including any trailing newlines, so preserve
|
||||
# those bytes with a sentinel and hash stdin instead.
|
||||
link_target_with_sentinel="$(
|
||||
readlink -n -- "$checkout/$path" && printf .
|
||||
)" || exit 1
|
||||
[[ "$link_target_with_sentinel" == *. ]] || exit 1
|
||||
link_target="${link_target_with_sentinel%.}"
|
||||
actual="$(
|
||||
printf '%s' "$link_target" \
|
||||
| checkout_command git -C "$checkout" hash-object --stdin
|
||||
)" || exit 1
|
||||
[[ "$actual" == "$expected" ]] || exit 1
|
||||
continue
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
|
||||
actual="$(checkout_command git -C "$checkout" hash-object --no-filters -- "$path")" \
|
||||
|| exit 1
|
||||
[[ "$actual" == "$expected" ]] || exit 1
|
||||
done <"$listing"
|
||||
)
|
||||
|
||||
prepare_panama_checkout() {
|
||||
local checkout="$1" actual_head checkout_status
|
||||
|
||||
@@ -110,6 +169,30 @@ for arg in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Keep the worktree comparison at the last possible boundary. Checkout
|
||||
# preparation may invoke several commands and return to the caller; performing
|
||||
# the byte/mode/link check here ensures a change in that interval is rejected
|
||||
# before any tracked file is executed.
|
||||
verified_install_handoff() {
|
||||
local use_tty="$1"
|
||||
if ! checkout_matches_verified_commit "$PANAMA_PATH"; then
|
||||
echo "boot: checkout files do not match PANAMA_BOOT_REVISION" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$BOOTSTRAP_USER" ]]; then
|
||||
if (( use_tty )); then
|
||||
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
fi
|
||||
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
fi
|
||||
if (( use_tty )); then
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
fi
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
}
|
||||
|
||||
# The public bootstrap contract runs this branch as an ordinary user with a
|
||||
# stubbed root identity. Keep its filesystem adapter unavailable to a real root
|
||||
# shell so it cannot redirect a real installation by accident.
|
||||
@@ -471,7 +554,7 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
dnf install -y git
|
||||
dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||
fi
|
||||
|
||||
# Create or advance the checkout as the target user. A root-owned .git in a
|
||||
@@ -481,15 +564,14 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
|
||||
echo "Handing off to install as $username"
|
||||
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" --server </dev/tty
|
||||
verified_install_handoff 1
|
||||
fi
|
||||
|
||||
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||
# included -- is bootstrapped by `install`.
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
sudo dnf install -y git
|
||||
sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||
fi
|
||||
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
@@ -500,7 +582,8 @@ prepare_panama_checkout "$PANAMA_PATH"
|
||||
# so itself.
|
||||
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||
# controlling terminal passes -r and then fails the redirect.
|
||||
handoff_tty=0
|
||||
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
handoff_tty=1
|
||||
fi
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
verified_install_handoff "$handoff_tty"
|
||||
|
||||
@@ -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="$(
|
||||
_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
|
||||
{
|
||||
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"
|
||||
"$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
|
||||
|
||||
@@ -7,26 +7,36 @@
|
||||
declare -gA INSTALLER_PROVENANCE=()
|
||||
|
||||
_primary_key_fingerprints() (
|
||||
local home
|
||||
home="$(mktemp -d)" || exit 1
|
||||
chmod 700 "$home"
|
||||
trap 'rm -rf -- "$home"' EXIT
|
||||
local home="" gpg_output
|
||||
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
GNUPGHOME="$home" gpg --batch --with-colons --import-options show-only --import "$1" 2>/dev/null \
|
||||
| awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }'
|
||||
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
|
||||
if ! mkdir -m 700 -- "$home"; then
|
||||
home=""
|
||||
exit 1
|
||||
fi
|
||||
gpg_output="$(GNUPGHOME="$home" gpg --batch --with-colons \
|
||||
--import-options show-only --import "$1" 2>/dev/null)" || exit 1
|
||||
awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }' \
|
||||
<<<"$gpg_output"
|
||||
)
|
||||
|
||||
key_fingerprint_matches() {
|
||||
local file="$1" expected="$2"
|
||||
local file="$1" expected="$2" output
|
||||
local -a primary_fingerprints=()
|
||||
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$file")
|
||||
output="$(_primary_key_fingerprints "$file")" || return 1
|
||||
[[ -n "$output" ]] || return 1
|
||||
mapfile -t primary_fingerprints <<<"$output"
|
||||
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
|
||||
}
|
||||
|
||||
_key_has_one_primary() {
|
||||
local output
|
||||
local -a primary_fingerprints=()
|
||||
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$1")
|
||||
output="$(_primary_key_fingerprints "$1")" || return 1
|
||||
[[ -n "$output" ]] || return 1
|
||||
mapfile -t primary_fingerprints <<<"$output"
|
||||
[[ ${#primary_fingerprints[@]} -eq 1 ]]
|
||||
}
|
||||
|
||||
@@ -34,11 +44,15 @@ verify_detached_signature() {
|
||||
local key="$1" signature="$2" content="$3" home
|
||||
_key_has_one_primary "$key" || return 1
|
||||
(
|
||||
home="$(mktemp -d)" || exit 1
|
||||
chmod 700 "$home"
|
||||
trap 'rm -rf -- "$home"' EXIT
|
||||
home=""
|
||||
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
|
||||
if ! mkdir -m 700 -- "$home"; then
|
||||
home=""
|
||||
exit 1
|
||||
fi
|
||||
GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \
|
||||
&& GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1
|
||||
)
|
||||
@@ -60,7 +74,11 @@ download_sha256() {
|
||||
[[ "$max_bytes" =~ ^[1-9][0-9]*$ ]] || exit 1
|
||||
[[ -n "$destination" && -d "$directory" ]] || exit 1
|
||||
umask 077
|
||||
part="$(mktemp "$directory/.${filename}.part.XXXXXX")" || exit 1
|
||||
part="$(mktemp -u "$directory/.${filename}.part.XXXXXX")" || exit 1
|
||||
if ! (set -o noclobber; : >"$part") 2>/dev/null; then
|
||||
part=""
|
||||
exit 1
|
||||
fi
|
||||
curl --fail --location --connect-timeout 10 --max-time 600 \
|
||||
--max-filesize "$max_bytes" --output "$part" "$url" \
|
||||
|| exit 1
|
||||
@@ -71,25 +89,25 @@ download_sha256() {
|
||||
)
|
||||
}
|
||||
|
||||
rpm_signature_matches() {
|
||||
local package="$1" key="$2" expected="$3" home db output status
|
||||
rpm_signature_matches() (
|
||||
local package="$1" key="$2" expected="$3" home="" db output
|
||||
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
key_fingerprint_matches "$key" "$expected" || return 1
|
||||
home="$(mktemp -d)" || return 1
|
||||
chmod 700 "$home"
|
||||
key_fingerprint_matches "$key" "$expected" || exit 1
|
||||
home="$(mktemp -u -d -t panama-rpm-signature.XXXXXX)" || exit 1
|
||||
if ! mkdir -m 700 -- "$home"; then
|
||||
home=""
|
||||
exit 1
|
||||
fi
|
||||
db="$home/rpmdb"
|
||||
mkdir -m 700 "$db" || {
|
||||
rm -rf -- "$home"
|
||||
return 1
|
||||
}
|
||||
|
||||
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 \
|
||||
&& output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)"
|
||||
status=$?
|
||||
rm -rf -- "$home"
|
||||
(( status == 0 )) || return 1
|
||||
mkdir -m 700 "$db" || exit 1
|
||||
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 || exit 1
|
||||
output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)" \
|
||||
|| exit 1
|
||||
grep -Eqi 'OpenPGP.*signature.*: OK' <<<"$output"
|
||||
}
|
||||
)
|
||||
|
||||
load_installer_provenance() {
|
||||
local file="$1" line name value required
|
||||
|
||||
+52
-31
@@ -11,17 +11,24 @@ content before the applicable verification succeeds.
|
||||
Each command below was run in a private temporary directory on 2026-08-27.
|
||||
The resulting armored public key is vendored under `keys/`; each output was
|
||||
checked with the listed complete primary fingerprint before it was committed.
|
||||
The verification commands use Panama's status-preserving helper: it captures
|
||||
GPG's output only after GPG succeeds, then requires exactly one primary key.
|
||||
|
||||
```bash
|
||||
source setup/lib/artifact-provenance
|
||||
key_fingerprint_matches KEY.asc EXPECTED_COMPLETE_PRIMARY_FINGERPRINT
|
||||
```
|
||||
|
||||
| Key | Source URL | Expected primary fingerprint | Verification command |
|
||||
| --- | --- | --- | --- |
|
||||
| Terra 44 | `https://repos.fyralabs.com/terra44/key.asc` | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` | `gpg --batch --with-colons --import-options show-only --import terra44.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| Anthropic Claude Code | `https://downloads.claude.ai/keys/claude-code.asc` | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` | `gpg --batch --with-colons --import-options show-only --import claude-code.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| Bun releases | `https://keys.openpgp.org/vks/v1/by-fingerprint/F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `gpg --batch --with-colons --import-options show-only --import bun.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| RPM Fusion free | `https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020` | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` | `gpg --batch --with-colons --import-options show-only --import rpmfusion-free.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| RPM Fusion nonfree | `https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020` | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | `gpg --batch --with-colons --import-options show-only --import rpmfusion-nonfree.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| lionheartp/Hyprland COPR | `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg` | `97E23476C89635135407C7D5E9BA41342C4B2995` | `gpg --batch --with-colons --import-options show-only --import hyprland-copr.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| Flathub | `https://flathub.org/repo/flathub.flatpakrepo` | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | `awk -F= '/^GPGKey=/{print $2}' flathub.flatpakrepo \| base64 --decode \| gpg --batch --with-colons --import-options show-only --import \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| Claude Desktop Extra | `https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc` | `825A7D15D78BABE45646D5DF382409F597908867` | `gpg --batch --with-colons --import-options show-only --import claude-desktop.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||
| Terra 44 | `https://repos.fyralabs.com/terra44/key.asc` | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` | `key_fingerprint_matches terra44.asc AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` |
|
||||
| Anthropic Claude Code | `https://downloads.claude.ai/keys/claude-code.asc` | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` | `key_fingerprint_matches claude-code.asc 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` |
|
||||
| Bun releases | `https://keys.openpgp.org/vks/v1/by-fingerprint/F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `key_fingerprint_matches bun.asc F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` |
|
||||
| RPM Fusion free | `https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020` | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` | `key_fingerprint_matches rpmfusion-free.asc E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` |
|
||||
| RPM Fusion nonfree | `https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020` | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | `key_fingerprint_matches rpmfusion-nonfree.asc 79BDB88F9BBF73910FD4095B6A2AF96194843C65` |
|
||||
| lionheartp/Hyprland COPR | `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg` | `97E23476C89635135407C7D5E9BA41342C4B2995` | `key_fingerprint_matches hyprland-copr.asc 97E23476C89635135407C7D5E9BA41342C4B2995` |
|
||||
| Flathub | `https://flathub.org/repo/flathub.flatpakrepo` | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | `key_fingerprint_matches flathub.asc 6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
|
||||
| Claude Desktop Extra | `https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc` | `825A7D15D78BABE45646D5DF382409F597908867` | `key_fingerprint_matches claude-desktop.asc 825A7D15D78BABE45646D5DF382409F597908867` |
|
||||
|
||||
The retrieval command for every direct key was:
|
||||
|
||||
@@ -98,16 +105,28 @@ digest, then update the command and this ledger in a second commit.
|
||||
|
||||
Do not replace a key on an automated update. A key rotation is a reviewed
|
||||
repository change: obtain the new key from the publisher record, independently
|
||||
confirm its complete primary fingerprint, update the vendored key and
|
||||
`installers.conf` together, refresh this retrieval record, and add a focused
|
||||
contract case if the verification behavior changes. Until that review lands,
|
||||
verification fails closed and preserves any known-good destination.
|
||||
confirm its complete primary fingerprint, and update every independent pin site
|
||||
in one review:
|
||||
|
||||
## Container-only Terra 44 signed-bootstrap proof
|
||||
- the armored key under `setup/provenance/keys/`;
|
||||
- its fingerprint in `setup/provenance/installers.conf`;
|
||||
- the matching `_require_policy_value` literal in
|
||||
`setup/scripts/install-packages`;
|
||||
- independent fingerprint expectations and command-log fixtures in
|
||||
`tests/setup/package-provenance-contract`;
|
||||
- this retrieval and evidence ledger at `setup/provenance/README.md`.
|
||||
|
||||
On 2026-08-27, a single disposable rootless Podman container proved the Terra
|
||||
bootstrap path without changing the host package database, host keyring, or
|
||||
host repository files. Podman reported `rootless=true`, `runtime=crun`, and a
|
||||
Until all sites agree, verification fails closed and preserves any known-good
|
||||
destination. Add or update a focused contract whenever verification behavior
|
||||
changes.
|
||||
|
||||
## Historical container-only Terra 44 signed-bootstrap proof
|
||||
|
||||
On 2026-08-27, a single disposable rootless Podman container validated Terra's
|
||||
then-reviewed signed bootstrap without changing the host package database,
|
||||
host keyring, or host repository files. This is retained historical publisher
|
||||
evidence; Panama's runtime installer no longer installs `terra-release`.
|
||||
Podman reported `rootless=true`, `runtime=crun`, and a
|
||||
user graph root. The fresh image was
|
||||
`registry.fedoraproject.org/fedora@sha256:62f199d1eb34170a7bb2277485676d89c0e91aae4086151c4043062cce51c77c`
|
||||
(`sha256:87d8a4a90c0457689db68624cac1026fb2201cbdc1e99cc5455a8f8876118498`).
|
||||
@@ -115,12 +134,13 @@ The container (`5fc8fa42bb85afb3b57b336ca29b58a32fad50d460583329a4e910cc29fb4d2d
|
||||
had no mounts and was removed automatically after `podman stop`.
|
||||
|
||||
Before copying the only host file admitted to the container,
|
||||
`keys/terra44.asc`, this exact host check reported the complete primary
|
||||
fingerprint `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F`:
|
||||
`keys/terra44.asc`, this status-preserving host check accepted the complete
|
||||
primary fingerprint `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F`:
|
||||
|
||||
```bash
|
||||
gpg --batch --with-colons --import-options show-only --import setup/provenance/keys/terra44.asc \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||
source setup/lib/artifact-provenance
|
||||
key_fingerprint_matches setup/provenance/keys/terra44.asc \
|
||||
AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
||||
```
|
||||
|
||||
Its SHA-256 was
|
||||
@@ -144,14 +164,16 @@ podman exec panama-terra-proof-20260827 /bin/bash -lc '
|
||||
'
|
||||
```
|
||||
|
||||
Inside the container the copied and installed key both had the recorded
|
||||
SHA-256 before and after installation. `terra-release-44-9.noarch` was
|
||||
installed. Its effective `terra` configuration reported `gpgcheck = 1`,
|
||||
`pkg_gpgcheck = 1`, and `repo_gpgcheck = 1`; no GPG-bypass option was used.
|
||||
The package's own `/etc/yum.repos.d/terra.repo` uses its Terra metalink and
|
||||
`RPM-GPG-KEY-terra44`. That differs from Panama's deliberately staged local
|
||||
key/base-URL file in `install-packages`, which replaces the release-generated
|
||||
file only after this verified bootstrap step.
|
||||
The retained command output records the copied key's SHA-256 and DNF's
|
||||
successful `terra-release-44-9.noarch` transaction. The command itself pins the
|
||||
temporary Terra base URL and local staged key and enables package and repository
|
||||
signature checks. It does not include a separate post-install fingerprint or
|
||||
effective-repository query, so this ledger makes no independent post-check
|
||||
claim. Production publishes the reviewed root-staged key/repository pair
|
||||
directly and commits it only after the effective-repository post-check
|
||||
succeeds; failure restores the prior pair. Publisher-only package transactions
|
||||
use a fresh command-line repository identity, the reviewed base URL, and a
|
||||
newly fingerprint-verified private root key snapshot.
|
||||
|
||||
Although the command runner returned after 30 seconds while DNF was still
|
||||
loading metadata, Podman's retained event log records the exact command's
|
||||
@@ -165,6 +187,5 @@ podman events --since '2026-08-27T10:55:00-04:00' --until '2026-08-27T11:02:00-0
|
||||
|
||||
The first `exec` event, at `timeNano=1787842633591543881`, is the documented
|
||||
key-install and DNF command. Its matching first `exec_died` event, at
|
||||
`timeNano=1787842671276003275`, records `ContainerExitCode:0`. The
|
||||
same-container post-check independently confirmed the installed package and
|
||||
effective signature settings above; no retry or second container was used.
|
||||
`timeNano=1787842671276003275`, records `ContainerExitCode:0`. No retry or
|
||||
second container was used, and no stronger post-check evidence is retained.
|
||||
|
||||
@@ -53,7 +53,12 @@ if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
|
||||
warn "Secure Boot question, or disable Secure Boot first."
|
||||
else
|
||||
log "Installing the NVIDIA driver"
|
||||
if sudo dnf install -y akmod-nvidia xorg-x11-drv-nvidia-cuda; then
|
||||
if sudo dnf install -y \
|
||||
--repo=fedora --repo=updates \
|
||||
--repo=rpmfusion-free --repo=rpmfusion-free-updates \
|
||||
--repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates \
|
||||
--from-repo=rpmfusion-nonfree,rpmfusion-nonfree-updates \
|
||||
akmod-nvidia xorg-x11-drv-nvidia-cuda; then
|
||||
# nouveau has to be out of the way before the kernel would otherwise
|
||||
# bind it, which is why these are kernel arguments and not a modprobe
|
||||
# drop-in. modeset=1 is what makes the Wayland session work at all.
|
||||
|
||||
+698
-173
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,75 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_collect_vicinae_inputs() {
|
||||
local extension="$1" output="$2"
|
||||
[[ -d "$extension" && ! -L "$extension" \
|
||||
&& -f "$extension/package.json" && ! -L "$extension/package.json" \
|
||||
&& -f "$extension/package-lock.json" && ! -L "$extension/package-lock.json" ]] \
|
||||
|| return 1
|
||||
|
||||
# Everything authored below the extension affects its build. npm's
|
||||
# dependency tree is the sole exception and is reproduced from the lock.
|
||||
find "$extension" -mindepth 1 \
|
||||
\( -path "$extension/node_modules" -prune \) -o \
|
||||
! -type d -print0 >"$output" || return 1
|
||||
LC_ALL=C sort -z -o "$output" "$output" || return 1
|
||||
}
|
||||
|
||||
_write_vicinae_manifest() {
|
||||
local extension="$1" inputs="$2" output="$3"
|
||||
local input relative digest
|
||||
: >"$output" || return 1
|
||||
while IFS= read -r -d '' input; do
|
||||
[[ -f "$input" && ! -L "$input" && -r "$input" ]] || return 1
|
||||
relative="${input#"$extension"/}"
|
||||
[[ "$relative" != "$input" && -n "$relative" ]] || return 1
|
||||
digest="$(sha256sum -- "$input" | awk '{ print $1 }')" || return 1
|
||||
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
|
||||
printf '%s\0%s\0' "$relative" "$digest" >>"$output" || return 1
|
||||
done <"$inputs"
|
||||
}
|
||||
|
||||
_vicinae_extension_digest() (
|
||||
local extension="${1%/}" work=""
|
||||
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
work="$(mktemp -u -d -t panama-vicinae-digest.XXXXXX)" || exit 1
|
||||
if ! mkdir -m 700 -- "$work"; then
|
||||
work=""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_collect_vicinae_inputs "$extension" "$work/inputs.before" || exit 1
|
||||
_write_vicinae_manifest \
|
||||
"$extension" "$work/inputs.before" "$work/manifest.before" || exit 1
|
||||
_collect_vicinae_inputs "$extension" "$work/inputs.after" || exit 1
|
||||
_write_vicinae_manifest \
|
||||
"$extension" "$work/inputs.after" "$work/manifest.after" || exit 1
|
||||
cmp -s -- "$work/inputs.before" "$work/inputs.after" || exit 1
|
||||
cmp -s -- "$work/manifest.before" "$work/manifest.after" || exit 1
|
||||
sha256sum -- "$work/manifest.before" | awk '{ print $1 }'
|
||||
)
|
||||
|
||||
_record_vicinae_digest() (
|
||||
local built="$1" digest="$2" receipt temporary=""
|
||||
trap '[[ -z "$temporary" ]] || rm -f -- "$temporary"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
[[ -d "$built" && ! -L "$built" ]] || exit 1
|
||||
receipt="$built/.panama-source-sha256"
|
||||
temporary="$(mktemp -u "$built/.panama-source-sha256.XXXXXX")" || exit 1
|
||||
umask 077
|
||||
if ! (set -o noclobber; : >"$temporary") 2>/dev/null; then
|
||||
temporary=""
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$digest" >"$temporary" || exit 1
|
||||
mv -f -- "$temporary" "$receipt" || exit 1
|
||||
temporary=""
|
||||
)
|
||||
|
||||
panama_path="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
|
||||
source_dir="$panama_path/config/local/share/vicinae/scripts"
|
||||
@@ -81,18 +150,36 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
||||
[[ -f "$extension/package.json" ]] || continue
|
||||
name="$(basename "$extension")"
|
||||
|
||||
# Skip a build that would produce what is already there. `npm ci`
|
||||
# alone takes long enough to be worth not repeating on every re-run of
|
||||
# a stage that is otherwise nearly instant.
|
||||
# Skip only when a prior successful build records the digest of both
|
||||
# manifests and every source byte. Directory mtimes do not change when
|
||||
# an existing source file is edited.
|
||||
built="$vicinae_data_dir/extensions/$name"
|
||||
if [[ -d "$built" && "$extension/src" -ot "$built" ]]; then
|
||||
receipt="$built/.panama-source-sha256"
|
||||
if ! source_digest="$(_vicinae_extension_digest "$extension")"; then
|
||||
printf 'Vicinae extension %s inputs could not be verified; skipping\n' \
|
||||
"$name" >&2
|
||||
continue
|
||||
fi
|
||||
if [[ -f "$receipt" && ! -L "$receipt" ]] \
|
||||
&& cmp -s <(printf '%s\n' "$source_digest") "$receipt"; then
|
||||
printf 'Vicinae extension %s is already built\n' "$name"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf 'Building Vicinae extension %s\n' "$name"
|
||||
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
||||
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 \
|
||||
&& npm run build >/dev/null 2>&1); then
|
||||
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
|
||||
continue
|
||||
fi
|
||||
if ! final_digest="$(_vicinae_extension_digest "$extension")" \
|
||||
|| [[ "$final_digest" != "$source_digest" ]]; then
|
||||
printf 'Vicinae extension %s changed while building; receipt withheld\n' \
|
||||
"$name" >&2
|
||||
continue
|
||||
fi
|
||||
if ! _record_vicinae_digest "$built" "$source_digest"; then
|
||||
printf 'Vicinae extension %s receipt could not be recorded\n' "$name" >&2
|
||||
fi
|
||||
done
|
||||
elif [[ -d "$extensions_source" ]]; then
|
||||
|
||||
@@ -15,6 +15,16 @@ note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; }
|
||||
|
||||
# Git is the only package boot can install before the verified checkout exists.
|
||||
# Both root-server and ordinary-user paths must exclude ambient third-party
|
||||
# repositories while still allowing Fedora dependencies.
|
||||
for git_install in \
|
||||
'dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git' \
|
||||
'sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git'; do
|
||||
grep -qF "$git_install" "$boot" \
|
||||
|| note "boot omits reviewed Fedora source binding: $git_install"
|
||||
done
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
@@ -103,6 +113,16 @@ case "\${1:-}" in
|
||||
[[ "\$#" -eq 4 && "\$4" == 'HEAD^{commit}' ]] || exit 97
|
||||
cat "$state/head-revision"
|
||||
;;
|
||||
ls-tree)
|
||||
[[ "\$#" -eq 6 && "\$4" == -rz && "\$5" == --full-tree \
|
||||
&& "\$6" == "$revision" ]] || exit 97
|
||||
printf '100755 blob aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\tinstall\0'
|
||||
;;
|
||||
hash-object)
|
||||
[[ "\$#" -eq 6 && "\$4" == --no-filters && "\$5" == -- \
|
||||
&& "\$6" == install ]] || exit 97
|
||||
printf '%s\n' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
;;
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
@@ -268,6 +288,132 @@ run_boot "$revision" "$boot_sha"
|
||||
(( run_status != 0 )) || note 'existing HEAD mismatch returned success'
|
||||
assert_no_install_or_rewrite 'existing HEAD mismatch'
|
||||
|
||||
# Git's porcelain status deliberately trusts index hints. The bootstrap cannot:
|
||||
# these two flags can hide changed executable bytes while HEAD still names the
|
||||
# reviewed commit. Exercise real Git so the contract cannot accidentally teach
|
||||
# its adapter to expose state that Git itself hides.
|
||||
real_git="$(command -v git)"
|
||||
hidden_root="$work/hidden-index"
|
||||
mkdir -p "$hidden_root/home"
|
||||
"$real_git" init -q "$hidden_root/source"
|
||||
"$real_git" -C "$hidden_root/source" config user.email contract@panama
|
||||
"$real_git" -C "$hidden_root/source" config user.name contract
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' >"$hidden_root/source/install"
|
||||
chmod +x "$hidden_root/source/install"
|
||||
printf 'trusted target bytes\n' >"$hidden_root/source/target"
|
||||
ln -s target "$hidden_root/source/trusted-link"
|
||||
"$real_git" -C "$hidden_root/source" add install target trusted-link
|
||||
"$real_git" -C "$hidden_root/source" commit -qm trusted
|
||||
hidden_revision="$("$real_git" -C "$hidden_root/source" rev-parse HEAD)"
|
||||
"$real_git" clone -q --bare "$hidden_root/source" "$hidden_root/origin.git"
|
||||
|
||||
# Exercise the exact boundary between checkout preparation and handoff. This
|
||||
# test-only copy inserts a same-UID replacement after prepare returns; the
|
||||
# production handoff must perform its complete comparison after that point.
|
||||
post_prepare_checkout="$hidden_root/post-prepare-swap"
|
||||
post_prepare_marker="$hidden_root/post-prepare-executed"
|
||||
post_prepare_hook_marker="$hidden_root/post-prepare-hook-fired"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$post_prepare_checkout"
|
||||
post_prepare_hook="$hidden_root/swap-install"
|
||||
cat >"$post_prepare_hook" <<'HOOK'
|
||||
#!/usr/bin/env bash
|
||||
: >"$PANAMA_BOOT_POST_PREPARE_HOOK_MARKER"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' \
|
||||
"$PANAMA_BOOT_POST_PREPARE_MARKER" >"$PANAMA_PATH/install"
|
||||
chmod +x "$PANAMA_PATH/install"
|
||||
HOOK
|
||||
chmod +x "$post_prepare_hook"
|
||||
hooked_boot="$hidden_root/boot-post-prepare-hook"
|
||||
awk '
|
||||
{
|
||||
print
|
||||
if ($0 == "prepare_panama_checkout \"$PANAMA_PATH\"") {
|
||||
prepare_count++
|
||||
if (prepare_count == 1) print "\"$PANAMA_BOOT_POST_PREPARE_FIXTURE\""
|
||||
}
|
||||
}
|
||||
' "$boot" >"$hooked_boot"
|
||||
hooked_boot_sha="$(sha256sum "$hooked_boot" | cut -d' ' -f1)"
|
||||
post_prepare_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$post_prepare_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$hooked_boot_sha" \
|
||||
PANAMA_BOOT_POST_PREPARE_FIXTURE="$post_prepare_hook" \
|
||||
PANAMA_BOOT_POST_PREPARE_MARKER="$post_prepare_marker" \
|
||||
PANAMA_BOOT_POST_PREPARE_HOOK_MARKER="$post_prepare_hook_marker" \
|
||||
bash "$hooked_boot" </dev/null >"$hidden_root/post-prepare.out" 2>&1 \
|
||||
|| post_prepare_status=$?
|
||||
[[ -e "$post_prepare_hook_marker" ]] \
|
||||
|| note 'post-prepare replacement hook did not exercise the boundary'
|
||||
(( post_prepare_status != 0 )) \
|
||||
|| note 'post-prepare worktree replacement returned success'
|
||||
[[ ! -e "$post_prepare_marker" ]] \
|
||||
|| note 'post-prepare worktree replacement executed unreviewed install bytes'
|
||||
|
||||
# A valid tracked symlink must compare its link text with Git's 120000 blob;
|
||||
# hashing the pathname would follow it and hash the target file instead.
|
||||
symlink_checkout="$hidden_root/tracked-symlink"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$symlink_checkout"
|
||||
symlink_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$symlink_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/tracked-symlink.out" 2>&1 \
|
||||
|| symlink_status=$?
|
||||
(( symlink_status == 0 )) \
|
||||
|| note 'a checkout with a valid tracked symlink was rejected'
|
||||
|
||||
for hidden_flag in assume-unchanged skip-worktree; do
|
||||
hidden_checkout="$hidden_root/$hidden_flag"
|
||||
hidden_marker="$hidden_root/$hidden_flag-executed"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' "$hidden_marker" \
|
||||
>"$hidden_checkout/install"
|
||||
chmod +x "$hidden_checkout/install"
|
||||
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" install
|
||||
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|
||||
|| note "$hidden_flag fixture was not hidden from porcelain status"
|
||||
|
||||
hidden_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/$hidden_flag.out" 2>&1 \
|
||||
|| hidden_status=$?
|
||||
(( hidden_status != 0 )) \
|
||||
|| note "$hidden_flag modified checkout returned success"
|
||||
[[ ! -e "$hidden_marker" ]] \
|
||||
|| note "$hidden_flag modified checkout executed unreviewed install bytes"
|
||||
done
|
||||
|
||||
# The same hidden-index state must not conceal a mode change or a different
|
||||
# symlink target; both are part of the reviewed Git tree, not metadata hints.
|
||||
for hidden_flag in assume-unchanged skip-worktree; do
|
||||
for hidden_change in mode symlink-target; do
|
||||
hidden_checkout="$hidden_root/$hidden_flag-$hidden_change"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
|
||||
case "$hidden_change" in
|
||||
mode)
|
||||
chmod -x "$hidden_checkout/install"
|
||||
hidden_path=install
|
||||
;;
|
||||
symlink-target)
|
||||
rm -- "$hidden_checkout/trusted-link"
|
||||
ln -s untrusted-target "$hidden_checkout/trusted-link"
|
||||
hidden_path=trusted-link
|
||||
;;
|
||||
esac
|
||||
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" "$hidden_path"
|
||||
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|
||||
|| note "$hidden_flag $hidden_change fixture was not hidden from porcelain status"
|
||||
|
||||
hidden_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/$hidden_flag-$hidden_change.out" 2>&1 \
|
||||
|| hidden_status=$?
|
||||
(( hidden_status != 0 )) \
|
||||
|| note "$hidden_flag concealed a tracked $hidden_change change"
|
||||
done
|
||||
done
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
|
||||
@@ -99,9 +99,10 @@ sed -n '/^if \[\[ "\$ROLE" == server \]\]; then/,/^fi/p' "$installer" | grep -q
|
||||
# Comments dropped and backslash continuations joined, so a `soft` invocation
|
||||
# wrapped across three lines reads as the one command it is.
|
||||
uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; }
|
||||
uncommented_installer="$(uncommented)"
|
||||
|
||||
while read -r command; do
|
||||
uncommented | grep -q "soft .*$command" \
|
||||
grep -q "soft .*$command" <<<"$uncommented_installer" \
|
||||
|| note "'$command' runs without soft, so its failure still ends the stage"
|
||||
done <<'FRAGILE'
|
||||
dnf swap -y 'ffmpeg-free'
|
||||
@@ -129,7 +130,7 @@ if "rpm -q hyprland" not in after or "exit 1" not in after:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
uncommented | grep -q 'soft .*HYPR_PACKAGES' \
|
||||
grep -q 'soft .*HYPR_PACKAGES' <<<"$uncommented_installer" \
|
||||
&& note 'the Hyprland install is tolerated, so a machine with no desktop reports success'
|
||||
|
||||
# ── Soft failures are reported ──────────────────────────────────────────────
|
||||
|
||||
@@ -103,10 +103,13 @@ fi
|
||||
|
||||
nvidia="$(run_stage PANAMA_NVIDIA=yes)"
|
||||
|
||||
called "$nvidia" 'dnf install -y akmod-nvidia' \
|
||||
called "$nvidia" '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'
|
||||
expected_nvidia='sudo dnf install -y --repo=fedora --repo=updates --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates --from-repo=rpmfusion-nonfree,rpmfusion-nonfree-updates akmod-nvidia xorg-x11-drv-nvidia-cuda'
|
||||
grep -Fxq -- "$expected_nvidia" <<<"$nvidia" \
|
||||
|| note 'the NVIDIA transaction is not limited to reviewed Fedora and RPM Fusion repositories'
|
||||
called "$nvidia" 'grubby --update-kernel=ALL' \
|
||||
|| note 'the kernel arguments are never set'
|
||||
called "$nvidia" 'modprobe.blacklist=nouveau' \
|
||||
|
||||
@@ -123,7 +123,7 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|
||||
|| note 'the extension build never sources nvm, so npm is missing on any machine without a system node'
|
||||
|
||||
# node_modules is a dependency tree, not configuration.
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules/" 2>/dev/null \
|
||||
|| note 'the extension node_modules is not gitignored'
|
||||
|
||||
# npm must honour the committed dependency graph. This disposable fixture
|
||||
@@ -167,6 +167,131 @@ stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
|
||||
cmp -s -- "$lock_before" "$lockfile" \
|
||||
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
||||
|
||||
# Successful builds carry a digest receipt over both manifests and every
|
||||
# source file. Directory mtimes do not change when an existing file is edited,
|
||||
# so each byte class must independently invalidate the build.
|
||||
digest_root="$fixture_root/digest"
|
||||
digest_extension="$digest_root/config/local/share/vicinae/extensions/panama-search"
|
||||
digest_data="$digest_root/vicinae-data"
|
||||
mkdir -p "$digest_root/config/local/share/vicinae/scripts" \
|
||||
"$digest_extension/src" "$digest_extension/assets" "$digest_root/bin"
|
||||
cp -- "$manifest" "$digest_extension/package.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/package-lock.json" \
|
||||
"$digest_extension/package-lock.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/src/search.tsx" \
|
||||
"$digest_extension/src/search.tsx"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/tsconfig.json" \
|
||||
"$digest_extension/tsconfig.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/assets/extension_icon.svg" \
|
||||
"$digest_extension/assets/extension_icon.svg"
|
||||
cat >"$digest_root/bin/npm" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >>"${NPM_LOG:?}"
|
||||
case "${1:-}:${2:-}" in
|
||||
ci:--silent) exit 0 ;;
|
||||
run:build)
|
||||
mkdir -p "$VICINAE_DATA_DIR/extensions/$(basename "$PWD")"
|
||||
printf 'built\n' >"$VICINAE_DATA_DIR/extensions/$(basename "$PWD")/bundle"
|
||||
;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
EOF
|
||||
cat >"$digest_root/bin/find" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
status=0
|
||||
/usr/bin/find "$@" || status=$?
|
||||
[[ "${STUB_FIND_FAIL:-0}" != 1 ]] || exit 74
|
||||
exit "$status"
|
||||
EOF
|
||||
chmod +x "$digest_root/bin/npm" "$digest_root/bin/find"
|
||||
|
||||
run_digest_stage() {
|
||||
: >"$digest_root/npm.log"
|
||||
PATH="$digest_root/bin:$PATH" PANAMA_PATH="$digest_root" \
|
||||
VICINAE_DATA_DIR="$digest_data" NPM_LOG="$digest_root/npm.log" \
|
||||
STUB_FIND_FAIL="${STUB_FIND_FAIL:-0}" \
|
||||
bash "$stage" >"$digest_root/stage.out" 2>&1
|
||||
}
|
||||
|
||||
run_digest_stage || note 'the Vicinae digest fixture initial build failed'
|
||||
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|
||||
|| note 'the Vicinae digest fixture did not perform its initial locked build'
|
||||
run_digest_stage || note 'the unchanged Vicinae digest fixture failed'
|
||||
[[ ! -s "$digest_root/npm.log" ]] \
|
||||
|| note 'an unchanged Vicinae extension rebuilt despite its matching receipt'
|
||||
|
||||
for digest_input in src/search.tsx package.json package-lock.json tsconfig.json \
|
||||
assets/extension_icon.svg; do
|
||||
printf '\n// digest mutation: %s\n' "$digest_input" >>"$digest_extension/$digest_input"
|
||||
run_digest_stage || note "the Vicinae digest fixture failed after changing $digest_input"
|
||||
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|
||||
|| note "changing existing $digest_input bytes did not rebuild the Vicinae extension"
|
||||
done
|
||||
|
||||
# A traversal can emit valid-looking partial output and still fail. Sorting
|
||||
# that output must not hide find's producer status or replace the successful
|
||||
# build receipt with a digest over an incomplete source tree.
|
||||
digest_receipt="$digest_data/extensions/panama-search/.panama-source-sha256"
|
||||
cp -- "$digest_receipt" "$digest_root/receipt.before-find-failure"
|
||||
STUB_FIND_FAIL=1 run_digest_stage \
|
||||
|| note 'the Vicinae stage made a digest traversal failure fatal'
|
||||
[[ ! -s "$digest_root/npm.log" ]] \
|
||||
|| note 'a failed Vicinae digest traversal still rebuilt the extension'
|
||||
grep -q 'inputs could not be verified; skipping' "$digest_root/stage.out" \
|
||||
|| note 'a failed Vicinae digest traversal was accepted as verified input'
|
||||
cmp -s -- "$digest_root/receipt.before-find-failure" "$digest_receipt" \
|
||||
|| note 'a failed Vicinae digest traversal replaced the successful receipt'
|
||||
|
||||
# Helper writes run in conditional contexts in production, where Bash disables
|
||||
# implicit errexit inside the whole function. Each producer therefore has to
|
||||
# return its own write/publication failure and remove its temporary receipt.
|
||||
vicinae_helpers="$digest_root/vicinae-helpers"
|
||||
sed '/^panama_path=/,$d' "$stage" >"$vicinae_helpers"
|
||||
: >"$digest_root/empty-inputs"
|
||||
mkdir "$digest_root/manifest-output-directory"
|
||||
manifest_status=0
|
||||
bash -c 'source "$1"; set +e; _write_vicinae_manifest "$2" "$3" "$4"' bash \
|
||||
"$vicinae_helpers" "$digest_extension" "$digest_root/empty-inputs" \
|
||||
"$digest_root/manifest-output-directory" >/dev/null 2>&1 \
|
||||
|| manifest_status=$?
|
||||
[[ "$manifest_status" -ne 0 ]] \
|
||||
|| note 'a failed Vicinae manifest initialization returned success'
|
||||
|
||||
receipt_failure_root="$digest_root/receipt-publication-failure"
|
||||
mkdir -p "$receipt_failure_root/built" "$receipt_failure_root/bin"
|
||||
printf 'prior receipt\n' >"$receipt_failure_root/built/.panama-source-sha256"
|
||||
cat >"$receipt_failure_root/bin/mv" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
destination="${!#}"
|
||||
[[ "$destination" != */.panama-source-sha256 ]] || exit 75
|
||||
exec /usr/bin/mv "$@"
|
||||
EOF
|
||||
chmod +x "$receipt_failure_root/bin/mv"
|
||||
receipt_status=0
|
||||
PATH="$receipt_failure_root/bin:$PATH" bash -c \
|
||||
'source "$1"; set +e; _record_vicinae_digest "$2" "$3"' bash \
|
||||
"$vicinae_helpers" "$receipt_failure_root/built" "$(printf 'a%.0s' {1..64})" \
|
||||
>/dev/null 2>&1 || receipt_status=$?
|
||||
[[ "$receipt_status" -ne 0 ]] \
|
||||
|| note 'a failed Vicinae receipt publication returned success'
|
||||
cmp -s <(printf 'prior receipt\n') \
|
||||
"$receipt_failure_root/built/.panama-source-sha256" \
|
||||
|| note 'a failed Vicinae receipt publication replaced the prior receipt'
|
||||
[[ -z "$(find "$receipt_failure_root/built" \
|
||||
-name '.panama-source-sha256.*' -print -quit)" ]] \
|
||||
|| note 'a failed Vicinae receipt publication left a temporary receipt'
|
||||
|
||||
# Prove the directory-only ignore rule in a repository where node_modules does
|
||||
# not already exist. The trailing slash is part of the query contract.
|
||||
ignore_root="$fixture_root/ignore-repository"
|
||||
mkdir -p "$ignore_root/config/local/share/vicinae/extensions/panama-search"
|
||||
cp -- "$repo_dir/.gitignore" "$ignore_root/.gitignore"
|
||||
git -C "$ignore_root" init -q
|
||||
git -C "$ignore_root" check-ignore -q \
|
||||
'config/local/share/vicinae/extensions/panama-search/node_modules/' \
|
||||
|| note 'a fresh clone with no node_modules directory does not match the ignore rule'
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -187,6 +187,18 @@ case "${1:-}" in
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
ls-tree)
|
||||
[[ "$#" -eq 6 && "$4" == -rz && "$5" == --full-tree \
|
||||
&& "$6" == "$PANAMA_BOOT_REVISION" ]] || exit 97
|
||||
object_id="$(/usr/bin/git hash-object --no-filters -- \
|
||||
"$PANAMA_BOOT_FIXTURE_ROOT/stub-install")" || exit 97
|
||||
printf '100755 blob %s\tinstall\0' "$object_id"
|
||||
;;
|
||||
hash-object)
|
||||
[[ "$#" -eq 6 && "$4" == --no-filters && "$5" == -- \
|
||||
&& "$6" == install ]] || exit 97
|
||||
/usr/bin/git hash-object --no-filters -- "$2/$6"
|
||||
;;
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
@@ -51,7 +51,9 @@ copy_hash_inputs() {
|
||||
find "$repo_dir/setup/provenance" -type f -print0
|
||||
)
|
||||
mkdir -p "$root/setup/lib"
|
||||
cp -- "$repo_dir/setup/lib/artifact-provenance" "$root/setup/lib/artifact-provenance"
|
||||
cp -- "$repo_dir/setup/lib/artifact-provenance" \
|
||||
"$repo_dir/setup/lib/extras-catalog" \
|
||||
"$repo_dir/setup/lib/machine-role" "$root/setup/lib/"
|
||||
}
|
||||
|
||||
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
||||
@@ -61,7 +63,7 @@ build_fixture() {
|
||||
rm -rf "$root"
|
||||
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
||||
"$root/setup/lib" "$root/setup/provenance/keys" \
|
||||
"$root/config/dot/quickshell/scripts"
|
||||
"$root/config/dot/quickshell/scripts" "$root/tmp"
|
||||
|
||||
cp "$installer" "$root/install"
|
||||
: >"$root/bin/ascii"
|
||||
@@ -126,6 +128,46 @@ EOF
|
||||
#!/usr/bin/env bash
|
||||
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$root/shim/mv" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
destination="${!#}"
|
||||
if [[ "${STUB_SIGNAL_PACKAGES_HASH:-0}" == 1 \
|
||||
&& "$destination" == */state/panama/packages-hash ]]; then
|
||||
printf 'signal:packages-receipt\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mv "$@"
|
||||
EOF
|
||||
cat >"$root/shim/mktemp" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 && "${1:-}" == -d ]]; then
|
||||
directory="$(/usr/bin/mktemp "$@")"
|
||||
printf '%s\n' "$directory"
|
||||
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mktemp "$@"
|
||||
EOF
|
||||
cat >"$root/shim/mkdir" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
target="${!#}"
|
||||
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 \
|
||||
&& "$(basename -- "$target")" == panama-packages-hash.* ]]; then
|
||||
/usr/bin/mkdir "$@"
|
||||
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mkdir "$@"
|
||||
EOF
|
||||
for prerequisite in gum lspci mokutil fwupdmgr; do
|
||||
ln -s gsettings "$root/shim/$prerequisite"
|
||||
@@ -139,7 +181,8 @@ run_install() {
|
||||
local status=0
|
||||
: >"$root/ran"
|
||||
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
|
||||
XDG_STATE_HOME="$root/state" TMPDIR="$root/tmp" \
|
||||
/usr/bin/setsid bash "$root/install" "$@" \
|
||||
>"$root/out" 2>&1 || status=$?
|
||||
cat "$root/ran"
|
||||
return "$status"
|
||||
@@ -147,7 +190,8 @@ run_install() {
|
||||
|
||||
run_hash() {
|
||||
local root="$1"
|
||||
sed -n '/^hash_packages() {/,/^}$/p' "$root/install" >"$root/hash-only"
|
||||
sed -n '/^_collect_package_inputs() {/,/^PACKAGE_START_HASH=/p' \
|
||||
"$root/install" >"$root/hash-only"
|
||||
printf 'set -uo pipefail\nhash_packages\n' >>"$root/hash-only"
|
||||
PANAMA_PATH="$root" bash "$root/hash-only" 2>"$root/hash-only.err"
|
||||
}
|
||||
@@ -255,7 +299,8 @@ grep -qx 'install-packages' <<<"$ran_forced" \
|
||||
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||
# added but omitted from hash_packages.
|
||||
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
||||
'setup/scripts/install-packages' 'setup/lib/artifact-provenance'; do
|
||||
'setup/scripts/install-packages' 'setup/lib/artifact-provenance' \
|
||||
'setup/lib/extras-catalog' 'setup/lib/machine-role'; do
|
||||
printf 'changed %s\n' "$relative" >>"$tmp/a/$relative"
|
||||
install_status=0
|
||||
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||
@@ -282,7 +327,8 @@ done
|
||||
# Fixed hash inputs must not silently disappear or degrade into a directory or
|
||||
# link. An unreadable package input also proves a failed content read cannot be
|
||||
# hidden by the final digest command.
|
||||
for fixed_input in setup/scripts/install-packages setup/lib/artifact-provenance; do
|
||||
for fixed_input in setup/scripts/install-packages setup/lib/artifact-provenance \
|
||||
setup/lib/extras-catalog setup/lib/machine-role; do
|
||||
for case_name in missing directory symlink unreadable; do
|
||||
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
|
||||
build_fixture "$case_root"
|
||||
@@ -305,6 +351,27 @@ build_fixture "$read_failure_root"
|
||||
chmod 000 "$read_failure_root/${package_inputs[0]}"
|
||||
assert_hash_failure "$read_failure_root" "${package_inputs[0]} unreadable"
|
||||
|
||||
# Discovery must reject a symlink instead of silently dropping it from the
|
||||
# receipt while a later consumer follows it.
|
||||
for discovered_root in setup/packages setup/provenance; do
|
||||
case_root="$tmp/hash-${discovered_root//\//-}-symlink"
|
||||
build_fixture "$case_root"
|
||||
printf 'linked installer input\n' >"$case_root/symlink-target"
|
||||
ln -s "$case_root/symlink-target" "$case_root/$discovered_root/symlink-input"
|
||||
assert_hash_failure "$case_root" "$discovered_root symlink input"
|
||||
done
|
||||
|
||||
# Discovery roots are behavior inputs too. GNU find -P treats a symlink passed
|
||||
# as its starting path as an empty traversal, so checking only descendants can
|
||||
# silently erase a whole package or provenance tree from the receipt.
|
||||
for discovered_root in setup/packages setup/provenance; do
|
||||
case_root="$tmp/hash-${discovered_root//\//-}-root-symlink"
|
||||
build_fixture "$case_root"
|
||||
mv -- "$case_root/$discovered_root" "$case_root/$discovered_root.real"
|
||||
ln -s "$case_root/$discovered_root.real" "$case_root/$discovered_root"
|
||||
assert_hash_failure "$case_root" "$discovered_root discovery-root symlink"
|
||||
done
|
||||
|
||||
# A hash failure is an installer failure, not a reason to skip the package
|
||||
# stage and retain a stale stamp.
|
||||
build_fixture "$tmp/hash-failure"
|
||||
@@ -320,6 +387,61 @@ grep -qx 'install-packages' <<<"$ran_hash_failure" \
|
||||
cmp -s -- "$tmp/hash-failure/stamp-before" "$tmp/hash-failure/state/panama/packages-hash" \
|
||||
|| note 'a failed package-state hash wrote a new packages-hash stamp'
|
||||
|
||||
# The stage may race its own input receipt. A successful stage that changes a
|
||||
# sourced behavior file must not stamp the new digest as though it were the
|
||||
# bytes used to decide this run.
|
||||
build_fixture "$tmp/hash-drift"
|
||||
cat >"$tmp/hash-drift/setup/scripts/install-packages" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "${1:-}" == --trust-preflight ]]; then
|
||||
printf 'trust-preflight\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
fi
|
||||
printf 'install-packages\n' >>"$PANAMA_RAN"
|
||||
printf '# changed during package stage\n' >>"$PANAMA_PATH/setup/lib/machine-role"
|
||||
EOF
|
||||
chmod +x "$tmp/hash-drift/setup/scripts/install-packages"
|
||||
install_status=0
|
||||
run_install "$tmp/hash-drift" --upgrade >/dev/null || install_status=$?
|
||||
[[ "$install_status" -ne 0 ]] \
|
||||
|| note 'mid-stage package input drift returned success'
|
||||
[[ ! -e "$tmp/hash-drift/state/panama/packages-hash" ]] \
|
||||
|| note 'mid-stage package input drift stamped bytes the stage did not start with'
|
||||
|
||||
# The hash workspace exists before command substitution publishes its pathname.
|
||||
# A process-group signal in that window must still remove the private tree.
|
||||
build_fixture "$tmp/hash-work-signal"
|
||||
install_status=0
|
||||
signal_run="$(STUB_SIGNAL_HASH_WORK=1 \
|
||||
run_install "$tmp/hash-work-signal" --upgrade)" || install_status=$?
|
||||
[[ "$install_status" -eq 143 ]] \
|
||||
|| note "package hash workspace signal returned $install_status instead of 143"
|
||||
grep -qx 'signal:packages-hash-work' <<<"$signal_run" \
|
||||
|| note 'package hash workspace adapter did not deliver a real process-group signal'
|
||||
[[ -z "$(find "$tmp/hash-work-signal/tmp" -mindepth 1 -print -quit)" ]] \
|
||||
|| note 'package hash workspace signal left a private temporary directory'
|
||||
|
||||
# A real process-group signal at the final receipt rename must preserve the
|
||||
# prior stamp and remove the private temporary receipt.
|
||||
build_fixture "$tmp/hash-receipt-signal"
|
||||
run_install "$tmp/hash-receipt-signal" --upgrade >/dev/null
|
||||
cp -- "$tmp/hash-receipt-signal/state/panama/packages-hash" \
|
||||
"$tmp/hash-receipt-signal/stamp-before"
|
||||
install_status=0
|
||||
signal_run="$(STUB_SIGNAL_PACKAGES_HASH=1 \
|
||||
run_install "$tmp/hash-receipt-signal" --upgrade --packages)" \
|
||||
|| install_status=$?
|
||||
[[ "$install_status" -eq 143 ]] \
|
||||
|| note "package receipt signal returned $install_status instead of 143"
|
||||
grep -qx 'signal:packages-receipt' <<<"$signal_run" \
|
||||
|| note 'package receipt signal adapter did not deliver a real process-group signal'
|
||||
cmp -s -- "$tmp/hash-receipt-signal/stamp-before" \
|
||||
"$tmp/hash-receipt-signal/state/panama/packages-hash" \
|
||||
|| note 'package receipt signal replaced the prior hash stamp'
|
||||
[[ -z "$(find "$tmp/hash-receipt-signal/state/panama" \
|
||||
-name '.packages-hash.*' -print -quit)" ]] \
|
||||
|| note 'package receipt signal left a temporary hash stamp'
|
||||
|
||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||
build_fixture "$tmp/c" 1
|
||||
install_status=0
|
||||
@@ -368,6 +490,64 @@ for suppressed in link-dotfiles link-skills link-user change-settings install-ha
|
||||
&& note "stage-time Terra trust failure still ran $suppressed"
|
||||
done
|
||||
|
||||
# Exercise the complete real package entrypoint at the second boundary. The
|
||||
# outer preflight sees no Terra repository; the same DNF adapter exposes an
|
||||
# unsafe enabled Terra identity to the package stage's own preflight. Removing
|
||||
# that production call would reach the transaction marker below.
|
||||
real_preflight_root="$tmp/real-second-preflight"
|
||||
build_fixture "$real_preflight_root"
|
||||
cp -- "$repo_dir/setup/scripts/install-packages" \
|
||||
"$real_preflight_root/setup/scripts/install-packages"
|
||||
chmod +x "$real_preflight_root/setup/scripts/install-packages"
|
||||
mkdir -p "$real_preflight_root/state/panama"
|
||||
printf 'server\n' >"$real_preflight_root/state/panama/role"
|
||||
cat >"$real_preflight_root/shim/dnf" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$*" == '--quiet --no-plugins --dump-repo-config=*' ]]; then
|
||||
count=0
|
||||
[[ ! -f "$PANAMA_DNF_DUMP_COUNT" ]] || read -r count <"$PANAMA_DNF_DUMP_COUNT"
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" >"$PANAMA_DNF_DUMP_COUNT"
|
||||
printf 'dnf-dump\n' >>"$PANAMA_RAN"
|
||||
printf '======== "fedora" repository configuration: ========\n'
|
||||
printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n'
|
||||
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-44-primary\n'
|
||||
printf 'metalink = https://mirrors.fedoraproject.org/metalink\nmirrorlist\n'
|
||||
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
||||
if (( count == 2 )); then
|
||||
printf '======== "terra" repository configuration: ========\n'
|
||||
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 0\n'
|
||||
printf 'gpgkey = https://evil.invalid/key\n'
|
||||
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$real_preflight_root/shim/dnf"
|
||||
printf '0\n' >"$real_preflight_root/dnf-dump-count"
|
||||
: >"$real_preflight_root/ran"
|
||||
real_preflight_status=0
|
||||
PATH="$real_preflight_root/shim:$PATH" \
|
||||
PANAMA_PATH="$real_preflight_root" PANAMA_RAN="$real_preflight_root/ran" \
|
||||
PANAMA_DNF_DUMP_COUNT="$real_preflight_root/dnf-dump-count" \
|
||||
XDG_STATE_HOME="$real_preflight_root/state" \
|
||||
bash "$real_preflight_root/install" --upgrade --packages \
|
||||
>"$real_preflight_root/out" 2>&1 || real_preflight_status=$?
|
||||
[[ "$real_preflight_status" -eq 78 ]] \
|
||||
|| note "real second repository preflight returned $real_preflight_status instead of 78"
|
||||
[[ "$(<"$real_preflight_root/dnf-dump-count")" == 2 ]] \
|
||||
|| note "real package entrypoint executed $(<"$real_preflight_root/dnf-dump-count") repository preflights instead of two: $(tr '\n' ' ' <"$real_preflight_root/out")"
|
||||
[[ "$(grep -c '^dnf-dump$' "$real_preflight_root/ran")" -eq 2 ]] \
|
||||
|| note "real second preflight fixture log was: $(tr '\n' ',' <"$real_preflight_root/ran")"
|
||||
for suppressed in dnf-transaction link-dotfiles link-skills link-user change-settings \
|
||||
install-hardware; do
|
||||
grep -qx "$suppressed" "$real_preflight_root/ran" \
|
||||
&& note "real second repository preflight still ran $suppressed"
|
||||
done
|
||||
|
||||
# A full install always runs the stage, whatever any recorded hash says.
|
||||
build_fixture "$tmp/d"
|
||||
install_status=0
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Machine-local MCP tokens. Ignored by git on purpose: Panama is public.
|
||||
# Each name matches the token variable column in servers.
|
||||
NANOKVM_FEDORA_TOKEN='Bearer nag_mcp_hXDIbCWCAHPVcTNb5sgAU-91BVsRL3k_WDYe4ZmqDAQ'
|
||||
NANOKVM_MAC_TOKEN='Bearer nag_mcp_yzUyyioyirCAZvpkku5LVL0Riocbr1imZlxf_-JEcPI'
|
||||
Reference in New Issue
Block a user